Compare commits

...
Author SHA1 Message Date
m dd7fd2cd68 fix: disable agent dry-run without executor
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 24s
2026-08-22 16:43:32 +03:00
m 7030a51b69 Merge pull request 'feat: expose authenticated model fleet status' (#120) from codex/wp-805-model-ui into main
CI / test (push) Successful in 32s
2026-08-22 13:37:24 +00:00
m 6f71b73ac2 feat: expose authenticated model fleet status
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 25s
2026-08-22 16:36:11 +03:00
m 641a8cd8fb Merge pull request 'fix: persist all graph dispatch bindings before fan-out' (#119) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 37s
2026-08-22 13:25:23 +00:00
m 70007c0da0 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-runtime
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 27s
2026-08-22 16:23:59 +03:00
m 0e62ab7684 fix: persist all graph dispatch bindings before fan-out
CI / test (push) Successful in 26s
2026-08-22 16:23:45 +03:00
m 6f7421167b Merge pull request 'fix: serialize graph dispatch contracts' (#118) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 37s
2026-08-22 13:20:20 +00:00
m 122dab2a9d Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-runtime
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 26s
2026-08-22 16:18:54 +03:00
m 7896404b6d chore: declare graph contract serialization scope
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 25s
2026-08-22 16:16:04 +03:00
m a232d88bcf fix: serialize dispatch intent for graph runtime
CI / test (push) Failing after 20s
2026-08-22 16:15:03 +03:00
m e023491e8c Merge pull request 'fix: enqueue graph join after fan-in' (#117) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 42s
2026-08-22 13:10:40 +00:00
m 798b124fd4 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-runtime
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 27s
2026-08-22 16:09:09 +03:00
m c7fc8d6a63 feat: enqueue graph join after fan-in
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 27s
2026-08-22 16:06:33 +03:00
m 91e867f468 Merge pull request 'feat: add durable work graph runtime' (#116) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 39s
2026-08-22 13:04:35 +00:00
m 785441f2a0 feat: add durable work graph runtime
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 25s
2026-08-22 16:03:16 +03:00
m 16d714238e Merge pull request 'feat: connect agent worker to execution bridge' (#115) from codex/wp-302-agent-bridge into main
CI / test (push) Successful in 36s
2026-08-22 12:57:01 +00:00
m 7e703fe4b4 chore: format bridge tests
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 24s
2026-08-22 15:55:26 +03:00
m e935aadc94 feat: connect agent worker to execution bridge
CI / test (push) Failing after 20s
CI / test (pull_request) Failing after 18s
2026-08-22 15:53:44 +03:00
m 7b06cd64a2 Merge pull request 'feat: expose cognitive fleet readiness' (#114) from codex/wp-601-fleet-status into main
CI / test (push) Successful in 25s
2026-08-22 12:46:45 +00:00
m b3afe5236f chore: align fleet task lineage
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 24s
2026-08-22 15:45:30 +03:00
m cef7bd0226 feat: expose cognitive fleet readiness
CI / test (push) Failing after 22s
CI / test (pull_request) Failing after 17s
2026-08-22 15:44:27 +03:00
m 23b04aafd0 Merge pull request 'feat(research): add allowlisted HTTPS evidence connector' (#113) from codex/wp-102-research-orchestration into main
CI / test (push) Successful in 29s
2026-08-22 12:38:57 +00:00
m 6ee00af54b Merge remote-tracking branch 'origin/main' into codex/wp-102-research-orchestration
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 25s
2026-08-22 15:37:49 +03:00
m 6f37194724 feat(research): add allowlisted HTTPS evidence connector
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 24s
2026-08-22 15:36:48 +03:00
m 82a0052470 Merge pull request 'feat(research): add guarded repository evidence connector' (#112) from codex/wp-102-research-orchestration into main
CI / test (push) Successful in 29s
2026-08-22 12:33:02 +00:00
m d563db9e57 Merge remote-tracking branch 'origin/main' into codex/wp-102-research-orchestration
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 24s
2026-08-22 15:31:55 +03:00
m c1bc9c35d1 feat(research): add guarded repository evidence connector
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 23s
2026-08-22 15:30:41 +03:00
m 5a8a368153 Merge pull request 'feat(research): run private evidence persistence runtime' (#111) from codex/wp-102-research-orchestration into main
CI / test (push) Successful in 26s
2026-08-22 12:24:52 +00:00
m 7a14973e9a feat(research): run private evidence persistence runtime
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 23s
2026-08-22 15:23:43 +03:00
m f9e21737a5 Merge pull request 'feat(epistemics): persist truth bundles with source provenance' (#110) from codex/wp-102-truth-persistence into main
CI / test (push) Successful in 23s
2026-08-22 12:18:02 +00:00
m 53f86e4176 feat(epistemics): persist truth bundles with source provenance
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 21s
2026-08-22 15:16:42 +03:00
m 0b321fe115 Merge pull request 'fix(readiness): allow lifeboat when primary CPU is unavailable' (#109) from codex/wp-002-recovery into main
CI / test (push) Successful in 23s
Reviewed-on: #109
2026-08-22 12:10:37 +00:00
m 94ae8589ce Merge remote-tracking branch 'origin/main' into codex/wp-002-recovery
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 15:09:50 +03:00
m 8d8f7cb097 fix(readiness): allow lifeboat when primary CPU is unavailable
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 15:08:39 +03:00
m 6e9111dd0c Merge pull request 'fix(readiness): degrade optional fleet providers without blocking fallback' (#108) from codex/wp-002-recovery into main
CI / test (push) Successful in 23s
2026-08-22 12:06:15 +00:00
m 8c5386ec34 fix(readiness): degrade optional fleet providers without blocking fallback
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 23s
2026-08-22 15:05:09 +03:00
m 38cff16f45 Merge pull request 'fix(deploy): use shell-safe migration history values' (#107) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 29s
2026-08-22 11:54:55 +00:00
m 2d6baef7a9 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:53:32 +03:00
m 89924a417a fix(deploy): use shell-safe migration history values
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:51:59 +03:00
m 6735bca317 Merge pull request 'fix(deploy): normalize migration env and executable mode' (#106) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 26s
2026-08-22 11:50:33 +00:00
m 3c83e2ba5c Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:49:14 +03:00
m f2cc059e4b fix(deploy): normalize migration env and executable mode
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:47:42 +03:00
m 91c60b8c0c Merge pull request 'ops(deploy): automate idempotent migrations' (#105) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 26s
2026-08-22 11:46:24 +00:00
m 1687dad063 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:45:06 +03:00
m 37560816aa ops(deploy): automate idempotent migrations
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 21s
2026-08-22 14:43:33 +03:00
m f92406da68 Merge pull request 'fix(cognitive): preserve admission JSON fields' (#104) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 28s
2026-08-22 11:40:31 +00:00
m 0131a96de8 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 14:39:13 +03:00
m 0d4e9ee1b4 fix(cognitive): preserve admission JSON fields
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:37:32 +03:00
m 9282fc5bf6 Merge pull request 'feat(cognitive): enforce durable admission leases' (#103) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 27s
2026-08-22 11:35:27 +00:00
m 4c4a85aec6 feat(cognitive): enforce durable admission leases
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 22s
2026-08-22 14:33:53 +03:00
m ebcae4cc9c Add cognitive admission policy 2026-08-22 14:27:21 +03:00
m 9b262609b7 Merge pull request 'feat(epistemics): evaluate read-only research evidence' (#102) from codex/wp-102-research-runtime-impl into main
CI / test (push) Successful in 29s
2026-08-22 11:24:45 +00:00
64 changed files with 2110 additions and 84 deletions
+61
View File
@@ -0,0 +1,61 @@
package agentbridge
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrUnavailable = errors.New("agent execution bridge unavailable")
type HTTPExecutor struct {
URL string
Client *http.Client
}
func NewHTTPExecutor(url string, client *http.Client) (*HTTPExecutor, error) {
url = strings.TrimSpace(url)
if url == "" || !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return nil, ErrUnavailable
}
if client == nil {
client = http.DefaultClient
}
return &HTTPExecutor{URL: strings.TrimRight(url, "/"), Client: client}, nil
}
func (e *HTTPExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
body, err := json.Marshal(request)
if err != nil {
return work.AgentExecutionResult{}, err
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, e.URL+"/v1/execute", bytes.NewReader(body))
if err != nil {
return work.AgentExecutionResult{}, err
}
httpRequest.Header.Set("Content-Type", "application/json")
response, err := e.Client.Do(httpRequest)
if err != nil {
return work.AgentExecutionResult{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return work.AgentExecutionResult{}, fmt.Errorf("%w: status=%s", ErrUnavailable, response.Status)
}
decoder := json.NewDecoder(io.LimitReader(response.Body, 1<<20))
var result work.AgentExecutionResult
if err := decoder.Decode(&result); err != nil {
return work.AgentExecutionResult{}, err
}
if result.State == "" || result.Summary == "" || result.ObservedAtUnixMillis < 1 {
return work.AgentExecutionResult{}, errors.New("agent execution bridge returned invalid result")
}
return result, nil
}
+43
View File
@@ -0,0 +1,43 @@
package agentbridge
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/work"
)
func TestHTTPExecutorCarriesTypedDispatchAndDecodesResult(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/execute" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var request work.AgentExecutionRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
if request.WorkerID != "worker-1" || request.FencingToken != 7 || request.Intent.DispatchID != "dispatch-1" {
t.Fatalf("request=%+v", request)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "completed", ObservedAtUnixMillis: 2})
}))
defer server.Close()
executor, err := NewHTTPExecutor(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1"}, WorkerID: "worker-1", FencingToken: 7})
if err != nil || result.State != work.AgentExecutionCompleted || result.Summary != "completed" {
t.Fatalf("result=%+v err=%v", result, err)
}
}
func TestHTTPExecutorRejectsUnavailableEndpoint(t *testing.T) {
if _, err := NewHTTPExecutor("", nil); err != ErrUnavailable {
t.Fatalf("expected unavailable, got %v", err)
}
}
@@ -28,6 +28,89 @@ func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRe
func (r *CapabilityRepository) Close() { r.pool.Close() }
func (r *CapabilityRepository) AdmitCognitive(ctx context.Context, leaseID string, policy capability.AdmissionPolicy, request capability.AdmissionRequest, nowUnixMillis int64) (capability.AdmissionDecision, capability.AdmissionLease, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
if leaseID == "" || nowUnixMillis < 1 {
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonInputBudget, PolicyID: policy.ID}, capability.AdmissionLease{}, nil
}
var existingState string
var existingPolicy string
err = tx.QueryRow(ctx, `SELECT state, policy_id FROM capability.cognitive_admission_leases WHERE lease_id=$1`, leaseID).Scan(&existingState, &existingPolicy)
if err == nil {
if existingState == "ACTIVE" {
return capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: existingPolicy}, capability.AdmissionLease{ID: leaseID, PolicyID: existingPolicy}, tx.Commit(ctx)
}
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: existingPolicy}, capability.AdmissionLease{}, tx.Commit(ctx)
}
if !errors.Is(err, pgx.ErrNoRows) {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
windowStart := nowUnixMillis - 60*60*1000
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_counters(policy_id,window_start_unix_millis,accepted_count,concurrent_count,updated_at_unix_millis) VALUES($1,$2,0,0,$3) ON CONFLICT(policy_id) DO NOTHING`, policy.ID, windowStart, nowUnixMillis)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
var storedWindow int64
var accepted, concurrent int
if err = tx.QueryRow(ctx, `SELECT window_start_unix_millis,accepted_count,concurrent_count FROM capability.cognitive_admission_counters WHERE policy_id=$1 FOR UPDATE`, policy.ID).Scan(&storedWindow, &accepted, &concurrent); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
if nowUnixMillis-storedWindow >= 60*60*1000 {
storedWindow, accepted = windowStart, 0
}
var expired int64
if err = tx.QueryRow(ctx, `WITH expired AS (UPDATE capability.cognitive_admission_leases SET state='EXPIRED' WHERE policy_id=$1 AND state='ACTIVE' AND expires_at_unix_millis <= $2 RETURNING lease_id) SELECT count(*) FROM expired`, policy.ID, nowUnixMillis).Scan(&expired); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
concurrent -= int(expired)
if concurrent < 0 {
concurrent = 0
}
decision := capability.Admit(request, policy, capability.AdmissionCounters{Concurrent: concurrent, AcceptedLastHour: accepted}, nowUnixMillis)
if decision.State != capability.AdmissionAdmitted {
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted, concurrent, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return decision, capability.AdmissionLease{}, tx.Commit(ctx)
}
expires := nowUnixMillis + 60*1000
if request.DeadlineMillis > nowUnixMillis && request.DeadlineMillis < expires {
expires = request.DeadlineMillis
}
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted+1, concurrent+1, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_leases(lease_id,policy_id,state,expires_at_unix_millis,created_at_unix_millis) VALUES($1,$2,'ACTIVE',$3,$4)`, leaseID, policy.ID, expires, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return decision, capability.AdmissionLease{ID: leaseID, PolicyID: policy.ID}, tx.Commit(ctx)
}
func (r *CapabilityRepository) ReleaseCognitiveAdmission(ctx context.Context, lease capability.AdmissionLease, nowUnixMillis int64) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
var policyID string
err = tx.QueryRow(ctx, `UPDATE capability.cognitive_admission_leases SET state='RELEASED' WHERE lease_id=$1 AND state='ACTIVE' RETURNING policy_id`, lease.ID).Scan(&policyID)
if errors.Is(err, pgx.ErrNoRows) {
return tx.Commit(ctx)
}
if err != nil {
return err
}
_, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET concurrent_count=GREATEST(0,concurrent_count-1),updated_at_unix_millis=$2 WHERE policy_id=$1`, policyID, nowUnixMillis)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveHealth(ctx context.Context, health capability.Health) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.health (capability_id, state, reason, revision, semantic_version, observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (capability_id) DO UPDATE SET state=EXCLUDED.state, reason=EXCLUDED.reason, revision=EXCLUDED.revision, semantic_version=EXCLUDED.semantic_version, observed_at_unix_millis=EXCLUDED.observed_at_unix_millis WHERE capability.health.revision < EXCLUDED.revision`, health.CapabilityID, health.State, health.Reason, health.Revision, health.SemanticVersion, health.ObservedAtUnixMillis)
return err
@@ -21,6 +21,10 @@ func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRe
func (r *EpistemicsRepository) Close() { r.pool.Close() }
func (r *EpistemicsRepository) Ping(ctx context.Context) error {
return r.pool.Ping(ctx)
}
func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value epistemics.SystemExpectation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.system_expectations (
expectation_id, original_intent_id, source_utterance_id, expected_outcome, expected_effect,
@@ -161,3 +165,21 @@ func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, valu
value.Summary, value.EvidenceRefs, value.RequiresCognition, value.RecordedAtUnixMillis, value.Revision)
return err
}
func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value epistemics.TruthBundle, recordedAtUnixMillis int64) error {
if value.Source.ID == "" || value.Snapshot.ID == "" || value.Excerpt.ID == "" || value.Claim.ID == "" || value.Belief.ID == "" || value.Citation.ID == "" || recordedAtUnixMillis < 1 {
return fmt.Errorf("invalid truth bundle")
}
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.truth_bundles (
bundle_id,source_id,source_kind,source_locator,source_title,source_publisher,snapshot_id,snapshot_source_id,snapshot_content_digest,snapshot_captured_at_unix_millis,snapshot_freshness,
excerpt_id,excerpt_snapshot_id,excerpt_text,excerpt_start,excerpt_end,observation_id,observation_observed_at_unix_millis,claim_id,claim_statement,claim_observation_id,claim_citation_id,claim_verification,
belief_id,belief_claim_id,belief_state,citation_id,citation_excerpt_id,citation_evidence,citation_coverage,citation_conflict,citation_resolved_at_unix_millis,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
ON CONFLICT (bundle_id) DO UPDATE SET recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`,
"bundle:"+string(value.Citation.ID), value.Source.ID, value.Source.Kind, value.Source.Locator, value.Source.Title, value.Source.Publisher,
value.Snapshot.ID, value.Snapshot.SourceID, value.Snapshot.ContentDigest, value.Snapshot.CapturedAtUnixMillis, value.Snapshot.Freshness,
value.Excerpt.ID, value.Excerpt.SnapshotID, value.Excerpt.Text, value.Excerpt.Start, value.Excerpt.End,
value.Observation.ID, value.Observation.ObservedAt, value.Claim.ID, value.Claim.Statement, value.Claim.ObservationID, value.Claim.CitationID, value.Claim.Verification,
value.Belief.ID, value.Belief.ClaimID, value.Belief.State, value.Citation.ID, value.Citation.ExcerptID, value.Citation.Evidence, value.Citation.Coverage, value.Citation.Conflict, value.Citation.ResolvedAtUnixMillis, recordedAtUnixMillis)
return err
}
+5 -4
View File
@@ -26,7 +26,8 @@ func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, er
}
return &WorkRepository{pool}, nil
}
func (r *WorkRepository) Close() { r.pool.Close() }
func (r *WorkRepository) Close() { r.pool.Close() }
func (r *WorkRepository) Ping(ctx context.Context) error { return r.pool.Ping(ctx) }
func (r *WorkRepository) Create(ctx context.Context, target work.TaskTarget) error {
_, err := r.pool.Exec(ctx, `INSERT INTO work.tasks (task_id,goal_id,scope_id,revision) VALUES ($1,$2,$3,$4) ON CONFLICT (task_id) DO NOTHING`, target.TaskID, target.GoalID, target.ScopeID, target.Revision)
return err
@@ -143,7 +144,7 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
return err
}
for _, node := range graph.Nodes {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
return err
}
}
@@ -161,13 +162,13 @@ func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revi
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
rows, err := r.pool.Query(ctx, `SELECT node_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for rows.Next() {
var node work.GraphNode
if err := rows.Scan(&node.ID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
+74
View File
@@ -0,0 +1,74 @@
package research
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var ErrHTTPSourceRejected = errors.New("http research source rejected")
type HTTPConnector struct {
Client *http.Client
AllowedHosts map[string]struct{}
MaxBytes int64
}
func (c HTTPConnector) Collect(ctx context.Context, request epistemics.WebResearchRequest, nowUnixMillis int64) (epistemics.ResearchCandidate, error) {
if request.Plan.Validate() != nil || request.SourceID == "" || strings.TrimSpace(request.Statement) == "" || nowUnixMillis < 1 {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
u, err := url.Parse(request.URL)
if err != nil || u.Scheme != "https" || u.User != nil || u.Host == "" || !c.allowedHost(strings.ToLower(u.Hostname())) {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
client := c.Client
if client == nil {
client = &http.Client{}
}
maxBytes := c.MaxBytes
if maxBytes <= 0 || maxBytes > 4<<20 {
maxBytes = 256 << 10
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
response, err := client.Do(req)
if err != nil {
return epistemics.ResearchCandidate{}, fmt.Errorf("read web source: %w", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return epistemics.ResearchCandidate{}, fmt.Errorf("web source status: %s", response.Status)
}
body, err := io.ReadAll(io.LimitReader(response.Body, maxBytes+1))
if err != nil || int64(len(body)) > maxBytes || len(body) == 0 {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
digest := sha256.Sum256(body)
digestText := "sha256:" + hex.EncodeToString(digest[:])
key := sha256.Sum256([]byte(u.String() + "\x00" + digestText))
keyText := hex.EncodeToString(key[:])
snapshotID := epistemics.SourceSnapshotID("snapshot:" + keyText)
return epistemics.ResearchCandidate{
PropositionKey: request.PropositionKey, Statement: request.Statement,
Source: epistemics.Source{ID: request.SourceID, Kind: "web", Locator: u.String(), Title: request.Title, Publisher: u.Hostname()},
Snapshot: epistemics.SourceSnapshot{ID: snapshotID, SourceID: request.SourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID("excerpt:" + keyText), SnapshotID: snapshotID, Text: string(body), Start: 0, End: int64(len(body))},
Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
}, nil
}
func (c HTTPConnector) allowedHost(host string) bool {
_, ok := c.AllowedHosts[host]
return ok
}
+63
View File
@@ -0,0 +1,63 @@
package research
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var ErrRepositoryPathRejected = errors.New("repository source path rejected")
type RepositoryConnector struct{ Root string }
func (c RepositoryConnector) Collect(ctx context.Context, request epistemics.RepositoryResearchRequest, nowUnixMillis int64) (epistemics.ResearchCandidate, error) {
if err := request.Plan.Validate(); err != nil {
return epistemics.ResearchCandidate{}, err
}
if request.SourceID == "" || strings.TrimSpace(request.Statement) == "" || nowUnixMillis < 1 {
return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected
}
root, err := filepath.Abs(c.Root)
if err != nil || root == "" {
return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected
}
path, err := filepath.Abs(filepath.Join(root, filepath.FromSlash(request.Path)))
if err != nil || !withinRoot(root, path) {
return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected
}
select {
case <-ctx.Done():
return epistemics.ResearchCandidate{}, ctx.Err()
default:
}
content, err := os.ReadFile(path)
if err != nil || len(content) == 0 {
return epistemics.ResearchCandidate{}, fmt.Errorf("read repository source: %w", err)
}
digest := sha256.Sum256(content)
digestText := "sha256:" + hex.EncodeToString(digest[:])
key := sha256.Sum256([]byte(filepath.ToSlash(path) + "\x00" + digestText))
keyText := hex.EncodeToString(key[:])
return epistemics.ResearchCandidate{
PropositionKey: request.PropositionKey,
Statement: request.Statement,
Source: epistemics.Source{ID: request.SourceID, Kind: "repository", Locator: "file://" + filepath.ToSlash(path), Title: filepath.Base(path), Publisher: "local-repository"},
Snapshot: epistemics.SourceSnapshot{ID: epistemics.SourceSnapshotID("snapshot:" + keyText), SourceID: request.SourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID("excerpt:" + keyText), SnapshotID: epistemics.SourceSnapshotID("snapshot:" + keyText), Text: string(content), Start: 0, End: int64(len(content))},
Evidence: epistemics.EvidenceObserved,
Coverage: epistemics.CoverageComplete,
Conflict: epistemics.ConflictNone,
}, nil
}
func withinRoot(root, path string) bool {
rel, err := filepath.Rel(root, path)
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && !filepath.IsAbs(rel)
}
+21 -2
View File
@@ -5,20 +5,23 @@ import (
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"time"
"git.cin.su/m/Assistant/adapters/agentbridge"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
"git.cin.su/m/Assistant/modules/work/ports"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "dry-run lease duration")
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "executor lease duration")
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
@@ -34,9 +37,25 @@ func main() {
os.Exit(1)
}
defer repository.Close()
var executor ports.AgentExecutor
if bridgeURL := os.Getenv("AGENT_EXECUTION_BRIDGE_URL"); bridgeURL != "" {
executor, err = agentbridge.NewHTTPExecutor(bridgeURL, &http.Client{Timeout: *leaseTTL})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} else {
fmt.Fprintln(os.Stderr, "agent execution bridge is not configured; dispatch claims are disabled; recovery-only mode")
}
ctx := context.Background()
for {
result, cycleErr := application.ExecuteWorkerCycle(ctx, repository, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
var result application.WorkerCycleResult
var cycleErr error
if executor == nil {
result, cycleErr = application.RecoverOnlyWorkerCycle(ctx, repository, time.Now().UTC(), *staleAfter)
} else {
result, cycleErr = application.ExecuteWorkerCycleWithExecutor(ctx, repository, executor, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
}
if cycleErr != nil {
fmt.Fprintln(os.Stderr, cycleErr)
if *once {
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
type httpAdmissionClient struct {
baseURL string
client *http.Client
}
func newHTTPAdmissionClient(baseURL string, client *http.Client) *httpAdmissionClient {
return &httpAdmissionClient{baseURL: strings.TrimRight(baseURL, "/"), client: client}
}
func (c *httpAdmissionClient) Admit(ctx context.Context, leaseID string, request capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
payload, err := json.Marshal(struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}{leaseID, request})
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission", bytes.NewReader(payload))
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, fmt.Errorf("admission status %d", resp.StatusCode)
}
var result struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return result.Decision, result.Lease, nil
}
func (c *httpAdmissionClient) Release(ctx context.Context, lease capability.AdmissionLease) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission/"+lease.ID+"/release", nil)
if err != nil {
return err
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("admission release status %d", resp.StatusCode)
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type admissionTestProvider struct{ calls int }
func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
p.calls++
return cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted}, nil
}
type admissionTestClient struct {
decision capability.AdmissionDecision
releases int
}
func (c *admissionTestClient) Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
return c.decision, capability.AdmissionLease{ID: "lease-1", PolicyID: "policy-1"}, nil
}
func (c *admissionTestClient) Release(context.Context, capability.AdmissionLease) error {
c.releases++
return nil
}
func TestInvokeAdmittedDoesNotCallProviderWhenRejected(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: "policy-1"}}
gateway := cognitiveGateway{admission: client}
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if err != nil || response.FinishReason != cognitive.FinishRateLimited || provider.calls != 0 || client.releases != 0 {
t.Fatalf("rejected admission leaked provider call: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
gateway := cognitiveGateway{admission: client}
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if err != nil || response.FinishReason != cognitive.FinishCompleted || provider.calls != 1 || client.releases != 1 {
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
fleetdomain "git.cin.su/m/Assistant/modules/capability/domain"
)
type fleetEndpoint struct {
ProviderID string
ModelID string
Tier capability.FleetTier
URL string
}
type fleetStatusResponse struct {
Readiness capability.FleetReadiness `json:"readiness"`
Preferred capability.FleetTier `json:"preferred_tier"`
Selected capability.FleetTier `json:"selected_tier,omitempty"`
Error string `json:"error,omitempty"`
}
func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, endpoints []fleetEndpoint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
members := make([]capability.FleetMember, 0, len(endpoints))
for _, endpoint := range endpoints {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
members = append(members, member)
continue
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
if err == nil {
response, requestErr := client.Do(request)
if requestErr == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
} else {
member.Reason = requestErr.Error()
}
}
members = append(members, member)
}
readiness, err := fleetdomain.BuildFleetReadiness(time.Now().UnixMilli(), members)
response := fleetStatusResponse{Readiness: readiness, Preferred: preferred}
if err == nil {
response.Selected, err = fleetdomain.SelectFleetTier(readiness, preferred)
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
if response.Selected == "" {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(response)
})
}
func fleetContext(ctx context.Context) context.Context { return ctx }
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierHeavy, []fleetEndpoint{
{ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL},
{ProviderID: "medium", ModelID: "medium-model", Tier: capability.FleetTierMedium},
{ProviderID: "heavy", ModelID: "heavy-model", Tier: capability.FleetTierHeavy},
{ProviderID: "lifeboat", ModelID: "lifeboat-model", Tier: capability.FleetTierLifeboat, URL: server.URL},
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"LIFEBOAT"`) || !strings.Contains(recorder.Body.String(), `"tier":"MEDIUM"`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+26 -4
View File
@@ -26,6 +26,11 @@ type bindingResolver interface {
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
}
type admissionClient interface {
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
Release(context.Context, capability.AdmissionLease) error
}
type providerRegistration struct {
ProviderInstanceID capability.ProviderInstanceID
Kind capability.ProviderKind
@@ -46,6 +51,7 @@ type cognitiveGateway struct {
now func() time.Time
allowCanary bool
canaryMaxOutputBytes int
admission admissionClient
}
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
@@ -95,9 +101,9 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
}
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
started := g.now()
response, invokeErr := provider.Advise(r.Context(), invocation)
response, invokeErr := g.invokeAdmitted(r.Context(), string(request.RequestID), provider, invocation, maxOutputBytes)
if invokeErr != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
}
g.observe(r.Context(), selected, response, started)
if fallbackEligible(response.FinishReason) {
@@ -109,9 +115,9 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
fallbackRequest.ProviderBindingID, fallbackRequest.ProviderBindingRevision = fallback.Binding.ID, fallback.Binding.Revision
fallbackRequest.ProviderInstanceID, fallbackRequest.ModelDescriptorID, fallbackRequest.ProviderModelID = fallback.Provider.ID, fallback.Model.ID, fallback.Model.ProviderModelID
fallbackStarted := g.now()
fallbackResponse, fallbackErr := fallbackProvider.Advise(r.Context(), fallbackRequest)
fallbackResponse, fallbackErr := g.invokeAdmitted(r.Context(), string(request.RequestID)+":fallback", fallbackProvider, fallbackRequest, maxOutputBytes)
if fallbackErr != nil {
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
}
g.observe(r.Context(), fallback, fallbackResponse, fallbackStarted)
response = fallbackResponse
@@ -124,6 +130,22 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
}
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
var lease capability.AdmissionLease
if g.admission != nil {
decision, admittedLease, err := g.admission.Admit(ctx, leaseID, capability.AdmissionRequest{InputBytes: len(invocation.Message), MaxOutputBytes: maxOutputBytes, DeadlineMillis: 0})
if err != nil {
return cognitive.AdvisoryResponse{}, err
}
if decision.State != capability.AdmissionAdmitted {
return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil
}
lease = admittedLease
defer func() { _ = g.admission.Release(ctx, lease) }()
}
return provider.Advise(ctx, invocation)
}
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
lower := strings.ToLower(message)
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
+11 -3
View File
@@ -70,16 +70,24 @@ func main() {
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second})}
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
health.HTTPProbe(readinessClient, lifeboatURL),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
}})
fleetClient := &http.Client{Timeout: 2 * time.Second}
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierHeavy)))
mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, []fleetEndpoint{
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"},
{ProviderID: "local-medium-unconfigured", ModelID: "medium-unconfigured", Tier: capability.FleetTierMedium},
{ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/") + "/api/tags"},
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
}))
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
+36
View File
@@ -141,6 +141,34 @@ func main() {
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
}})
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
var request struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&request); err != nil || request.LeaseID == "" {
http.Error(w, "invalid admission request", http.StatusBadRequest)
return
}
decision, lease, err := capabilityRepository.AdmitCognitive(r.Context(), request.LeaseID, admissionPolicy, request.Request, time.Now().UnixMilli())
if err != nil {
http.Error(w, "admission unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}{decision, lease})
})
mux.HandleFunc("POST /v1/internal/cognitive/admission/{lease}/release", func(w http.ResponseWriter, r *http.Request) {
if err := capabilityRepository.ReleaseCognitiveAdmission(r.Context(), capability.AdmissionLease{ID: r.PathValue("lease")}, time.Now().UnixMilli()); err != nil {
http.Error(w, "admission release unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
@@ -1159,6 +1187,14 @@ func envOr(name, fallback string) string {
return fallback
}
func envInt(name string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(name))
if err != nil || value < 1 {
return fallback
}
return value
}
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
if inventoryErr != nil {
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
+42
View File
@@ -126,6 +126,10 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.handleChatMessage(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/api/fleet/readiness" {
g.handleFleetReadiness(w, r)
return
}
g.forwardAPI(w, r)
return
}
@@ -248,6 +252,44 @@ func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(policy)
}
func (g *gateway) handleFleetReadiness(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
var session identity.CreatorSession
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusBadGateway)
return
}
if status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
if status == 0 {
status = http.StatusUnauthorized
}
http.Error(w, "creator session denied", status)
return
}
target := *g.cognitive
target.Path = "/v1/fleet/readiness"
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target.String(), nil)
if err != nil {
http.Error(w, "fleet readiness unavailable", http.StatusBadGateway)
return
}
response, err := g.client.Do(request)
if err != nil {
http.Error(w, "fleet readiness unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(response.StatusCode)
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
}
func (g *gateway) handleAdvisory(w http.ResponseWriter, r *http.Request) {
var browserRequest browserAdvisoryRequest
if err := decodeStrictJSON(r.Body, maxAdvisoryRequestBytes, &browserRequest); err != nil || browserRequest.Message == "" || len(browserRequest.Message) > 16*1024 || !validID(browserRequest.SessionID) || !validID(browserRequest.WorkspaceID) {
+34
View File
@@ -23,6 +23,40 @@ func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
}
}
func TestFleetReadinessRequiresCreatorSessionAndProxiesCognitiveState(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" {
t.Fatalf("unexpected session request path=%s auth=%q", r.URL.Path, r.Header.Get("Authorization"))
}
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/fleet/readiness" {
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"readiness":{"members":[{"provider_instance_id":"lifeboat","model_descriptor_id":"qwen","tier":"LIFEBOAT","ready":true,"reason":"AVAILABLE"}],"ready_tiers":["LIFEBOAT"],"fallback_tier":"LIFEBOAT"},"preferred_tier":"HEAVY","selected_tier":"LIFEBOAT"}`)
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
denied := httptest.NewRecorder()
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/fleet/readiness", nil))
if denied.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated status=%d", denied.Code)
}
request := httptest.NewRequest(http.MethodGet, "/api/fleet/readiness", nil)
request.Header.Set("Authorization", "Bearer bounded-session")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"selected_tier":"LIFEBOAT"`) || response.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String())
}
}
const legacyExternalMode = "legacy-external-test-mode"
func newHandlerWithAdvisoryMode(coreURL, continuityURL, commitURL string, client *http.Client, mode string, cognitiveURLs ...string) (http.Handler, error) {
+22
View File
@@ -253,6 +253,24 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
.activity-reconcile:disabled { opacity: .55; cursor: wait; }
.activity-error { color: var(--bad); }
.fleet-panel { margin-bottom: 18px; border: 1px solid rgba(201,168,95,.32); background: linear-gradient(135deg, rgba(31,27,18,.92), rgba(13,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.45); }
.fleet-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.fleet-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.fleet-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.fleet-overview { padding: 16px 18px; display: flex; align-items: end; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line-soft); }
.fleet-overview span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.fleet-overview strong { display: block; margin-top: 6px; color: var(--signal); font: 600 16px/1 var(--mono); }
.fleet-overview p { max-width: 620px; margin: 0; color: var(--muted); font: 10px/1.45 var(--mono); }
.fleet-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.fleet-list[aria-busy="true"] { opacity: .72; }
.fleet-row { min-width: 0; padding: 14px 18px; display: grid; gap: 7px; border-right: 1px solid var(--line-soft); }
.fleet-row:last-child { border-right: 0; }
.fleet-row strong, .fleet-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fleet-row strong { color: var(--ink); font: 600 11px/1.3 var(--mono); }
.fleet-row span { color: var(--muted); font: 9px/1.4 var(--mono); }
.fleet-row .learning-state[data-state="READY"] { color: var(--good); }
.fleet-row .learning-state[data-state="DEGRADED"] { color: var(--warn); }
.learning-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.learning-grid article { min-width: 0; border-right: 1px solid var(--line-soft); }
.learning-grid article:last-child { border-right: 0; }
@@ -404,6 +422,10 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
.learning-grid { grid-template-columns: 1fr; }
.learning-grid article { border-right: 0; border-bottom: 1px solid var(--line-soft); }
.learning-grid article:last-child { border-bottom: 0; }
.fleet-overview { align-items: flex-start; flex-direction: column; }
.fleet-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fleet-row:nth-child(2n) { border-right: 0; }
.fleet-row { border-bottom: 1px solid var(--line-soft); }
.activity-row { padding: 14px 16px; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; }
.activity-state { grid-column: 2; grid-row: 1; }
.activity-operation { grid-column: 1; grid-row: 1; }
+47
View File
@@ -172,6 +172,7 @@ async function showConsole() {
else {
loadActionHistory();
loadLearning();
loadFleetReadiness();
}
runDiagnostics();
}
@@ -353,6 +354,7 @@ byId("reset-settings").addEventListener("click", () => {
byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("refresh-learning").addEventListener("click", loadLearning);
byId("refresh-fleet").addEventListener("click", loadFleetReadiness);
byId("logout").addEventListener("click", () => endSession());
byId("run-diagnostics").addEventListener("click", runDiagnostics);
@@ -442,6 +444,7 @@ async function loadDashboard() {
work.push(loadProspectiveRegister(context["prospective-id"]));
work.push(loadActionHistory());
work.push(loadLearning());
work.push(loadFleetReadiness());
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
else setSignal("capability", "не выбрана", "idle");
@@ -500,6 +503,50 @@ function createDiagnosticItem(label, key, state, detail = "Проверяю…")
return item;
}
async function loadFleetReadiness() {
const list = byId("fleet-list");
list.setAttribute("aria-busy", "true");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Проверяю доступность model fleet…";
list.append(loading);
try {
const result = await request("/api/fleet/readiness");
const readiness = result.readiness || {};
const members = Array.isArray(readiness.members) ? readiness.members : [];
setText("fleet-selected", result.selected_tier || "не выбран");
setText("fleet-summary", `${members.filter((member) => member.ready).length}/${members.length} ГОТОВЫ`);
setText("fleet-explanation", result.selected_tier ? `Активный маршрут: ${result.selected_tier}. Остальные tier остаются видимыми с их фактической причиной.` : "Ни один tier не выбран: проверьте конфигурацию и health провайдеров.");
list.replaceChildren(...members.map(createFleetRow));
} catch (error) {
setText("fleet-selected", "недоступен");
setText("fleet-summary", "ОШИБКА");
setText("fleet-explanation", safeClientError(error));
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = safeClientError(error);
list.replaceChildren(failed);
} finally {
list.setAttribute("aria-busy", "false");
}
}
function createFleetRow(member) {
const row = document.createElement("article");
row.className = "fleet-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = member.ready ? "READY" : "DEGRADED";
state.textContent = member.ready ? "READY" : "DEGRADED";
const title = document.createElement("strong");
title.textContent = member.tier || "UNKNOWN TIER";
const detail = document.createElement("span");
detail.textContent = `${member.model_descriptor_id || "model"} · ${member.reason || "причина не указана"}`;
row.append(state, title, detail);
return row;
}
async function loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
+9
View File
@@ -181,6 +181,15 @@
</div>
</section>
<section class="fleet-panel" aria-labelledby="fleet-title" data-page="operations">
<header>
<div><p>Cognitive / capability health</p><h2 id="fleet-title">Модели и fallback</h2></div>
<div class="activity-tools"><span id="fleet-summary">СОСТОЯНИЕ НЕ ЗАГРУЖЕНО</span><button id="refresh-fleet" class="icon-button" type="button" aria-label="Обновить состояние моделей" title="Обновить состояние моделей">↻</button></div>
</header>
<div class="fleet-overview"><div><span>Выбранный tier</span><strong id="fleet-selected">—</strong></div><p id="fleet-explanation">Проверка доступности провайдеров ещё не выполнялась.</p></div>
<div id="fleet-list" class="fleet-list" aria-live="polite" aria-busy="false"><p class="register-empty">Состояние fleet ещё не загружено.</p></div>
</section>
<section class="activity-panel" aria-labelledby="activity-title" data-page="operations">
<header>
<div><p>Commit / владелец</p><h2 id="activity-title">Журнал действий</h2></div>
+126
View File
@@ -0,0 +1,126 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
researchadapter "git.cin.su/m/Assistant/adapters/research"
"git.cin.su/m/Assistant/contracts/epistemics"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/modules/epistemics/application"
"git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
)
func main() {
ctx := context.Background()
password, err := resolveDatabasePassword()
if err != nil {
log.Fatal(err)
}
repository, err := postgres.OpenEpistemics(ctx, postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password})
if err != nil {
log.Fatal(err)
}
defer repository.Close()
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "research-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "research-runtime", Probes: []health.Probe{{Name: "epistemics-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
mux.HandleFunc("POST /v1/research/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.ResearchEvaluationRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&request); err != nil {
http.Error(w, "invalid research evidence", http.StatusBadRequest)
return
}
result, err := application.EvaluateAndPersist(r.Context(), request, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
connector := researchadapter.RepositoryConnector{Root: os.Getenv("RESEARCH_SOURCE_ROOT")}
mux.HandleFunc("POST /v1/research/repository/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.RepositoryResearchRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10)).Decode(&request); err != nil {
http.Error(w, "invalid repository research request", http.StatusBadRequest)
return
}
candidate, err := connector.Collect(r.Context(), request, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: []epistemics.ResearchCandidate{candidate}}, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
webConnector := researchadapter.HTTPConnector{AllowedHosts: allowedHosts(os.Getenv("RESEARCH_HTTP_ALLOWED_HOSTS")), Client: &http.Client{Timeout: 5 * time.Second}}
mux.HandleFunc("POST /v1/research/web/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.WebResearchRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10)).Decode(&request); err != nil {
http.Error(w, "invalid web research request", http.StatusBadRequest)
return
}
candidate, err := webConnector.Collect(r.Context(), request, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: []epistemics.ResearchCandidate{candidate}}, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8087"
}
log.Printf("research-runtime listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
func allowedHosts(value string) map[string]struct{} {
result := map[string]struct{}{}
for _, host := range strings.Split(value, ",") {
if host = strings.ToLower(strings.TrimSpace(host)); host != "" {
result[host] = struct{}{}
}
}
return result
}
func resolveDatabasePassword() (string, error) {
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv("EPISTEMICS_POSTGRES_PASSWORD_FILE"), os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if path == "" || ref == "" {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD_FILE and EPISTEMICS_POSTGRES_CREDENTIAL_REF are required")
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := secrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/modules/work/application"
"git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
)
func main() {
ctx := context.Background()
password, err := resolveDatabasePassword()
if err != nil {
log.Fatal(err)
}
repository, err := postgres.OpenWork(ctx, postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
if err != nil {
log.Fatal(err)
}
defer repository.Close()
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "work-graph-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "work-graph-runtime", Probes: []health.Probe{{Name: "work-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
mux.HandleFunc("POST /v1/work-graphs/schedule", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphScheduleRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
http.Error(w, "invalid graph schedule request", http.StatusBadRequest)
return
}
graph, ready, err := application.AdmitAndSchedule(r.Context(), repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
})
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphCompleteRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
http.Error(w, "invalid graph completion request", http.StatusBadRequest)
return
}
graph, err := application.CompleteGraphNode(r.Context(), repository, request.GraphID, request.Revision, request.NodeID, request.Succeeded)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, graph)
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8088"
}
log.Printf("work-graph-runtime listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func resolveDatabasePassword() (string, error) {
if _, ok := os.LookupEnv("WORK_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("WORK_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv("WORK_POSTGRES_PASSWORD_FILE"), os.Getenv("WORK_POSTGRES_CREDENTIAL_REF")
if path == "" || ref == "" {
return "", fmt.Errorf("WORK_POSTGRES_PASSWORD_FILE and WORK_POSTGRES_CREDENTIAL_REF are required")
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := secrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerWorkWorker, Path: path})
}
+12 -7
View File
@@ -27,10 +27,10 @@ type AdmissionPolicy struct {
}
type AdmissionRequest struct {
Complexity AdmissionComplexity
InputBytes int
MaxOutputBytes int
DeadlineMillis int64
Complexity AdmissionComplexity `json:"complexity"`
InputBytes int `json:"input_bytes"`
MaxOutputBytes int `json:"max_output_bytes"`
DeadlineMillis int64 `json:"deadline_millis"`
}
type AdmissionCounters struct {
@@ -39,9 +39,14 @@ type AdmissionCounters struct {
}
type AdmissionDecision struct {
State AdmissionDecisionState
Reason AdmissionReason
PolicyID string
State AdmissionDecisionState `json:"state"`
Reason AdmissionReason `json:"reason"`
PolicyID string `json:"policy_id"`
}
type AdmissionLease struct {
ID string `json:"id"`
PolicyID string `json:"policy_id"`
}
func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision {
+9 -9
View File
@@ -10,16 +10,16 @@ const (
)
type FleetMember struct {
ProviderInstanceID ProviderInstanceID
ModelDescriptorID ModelDescriptorID
Tier FleetTier
Ready bool
Reason string
ProviderInstanceID ProviderInstanceID `json:"provider_instance_id"`
ModelDescriptorID ModelDescriptorID `json:"model_descriptor_id"`
Tier FleetTier `json:"tier"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
}
type FleetReadiness struct {
ObservedAtUnixMillis int64
Members []FleetMember
ReadyTiers []FleetTier
FallbackTier FleetTier
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Members []FleetMember `json:"members"`
ReadyTiers []FleetTier `json:"ready_tiers"`
FallbackTier FleetTier `json:"fallback_tier"`
}
+25
View File
@@ -106,6 +106,31 @@ type ResearchResult struct {
EvaluatedAtUnixMillis int64
}
// ResearchEvaluationRequest carries immutable evidence from a read-only
// connector to the evaluator. The runtime never fetches or mutates a source
// from this contract.
type ResearchEvaluationRequest struct {
Plan ResearchPlan
Candidates []ResearchCandidate
}
type RepositoryResearchRequest struct {
Plan ResearchPlan
PropositionKey string
SourceID SourceID
Path string
Statement string
}
type WebResearchRequest struct {
Plan ResearchPlan
PropositionKey string
SourceID SourceID
URL string
Title string
Statement string
}
// ResolveCitation creates evidence only when all identifiers form one exact
// source chain. It deliberately does not fetch, infer or rewrite source text.
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
+3
View File
@@ -31,6 +31,9 @@ type Belief struct {
}
type TruthBundle struct {
Source Source
Snapshot SourceSnapshot
Excerpt SourceExcerpt
Observation Observation
Claim Claim
Belief Belief
+32
View File
@@ -0,0 +1,32 @@
package work
import "time"
// AgentExecutionRequest is the serializable hand-off from the durable worker
// to an executor. The bridge receives the already-authorized dispatch context;
// it does not receive database credentials or ambient authority.
type AgentExecutionRequest struct {
Intent AgentDispatchIntent `json:"intent"`
WorkerID string `json:"worker_id"`
FencingToken AgentFencingToken `json:"fencing_token"`
LeaseExpiresAt time.Time `json:"lease_expires_at"`
}
type AgentExecutionState string
const (
AgentExecutionCompleted AgentExecutionState = "COMPLETED"
AgentExecutionWaiting AgentExecutionState = "WAITING"
AgentExecutionFailed AgentExecutionState = "FAILED"
)
// AgentExecutionResult is an acknowledgement of the external execution
// attempt. The worker persists it through the work owner before completing a
// dispatch, so a bridge timeout never becomes an unrecorded success.
type AgentExecutionResult struct {
State AgentExecutionState `json:"state"`
Summary string `json:"summary"`
OutcomeRef string `json:"outcome_ref"`
ArtifactRefs []string `json:"artifact_refs"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
}
+6 -6
View File
@@ -10,10 +10,10 @@ const (
)
type AgentDispatchQueueItem struct {
DispatchID string
State AgentDispatchQueueState
AvailableAtUnixMillis int64
Attempts int
ClaimedBy string
UpdatedAtUnixMillis int64
DispatchID string `json:"dispatch_id"`
State AgentDispatchQueueState `json:"state"`
AvailableAtUnixMillis int64 `json:"available_at_unix_millis"`
Attempts int `json:"attempts"`
ClaimedBy string `json:"claimed_by"`
UpdatedAtUnixMillis int64 `json:"updated_at_unix_millis"`
}
+15 -14
View File
@@ -26,29 +26,30 @@ const (
)
type GraphBudget struct {
MaxParallel int
MaxAttempts int
MaxParallel int `json:"max_parallel"`
MaxAttempts int `json:"max_attempts"`
}
type GraphNode struct {
ID GraphNodeID
IdempotencyKey string
State GraphNodeState
Attempts int
ID GraphNodeID `json:"id"`
DispatchID string `json:"dispatch_id"`
IdempotencyKey string `json:"idempotency_key"`
State GraphNodeState `json:"state"`
Attempts int `json:"attempts"`
}
type GraphEdge struct {
From GraphNodeID
To GraphNodeID
From GraphNodeID `json:"from"`
To GraphNodeID `json:"to"`
}
type WorkGraph struct {
ID string
Revision int64
State GraphState
Nodes []GraphNode
Edges []GraphEdge
Budget GraphBudget
ID string `json:"id"`
Revision int64 `json:"revision"`
State GraphState `json:"state"`
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
Budget GraphBudget `json:"budget"`
}
type ResultCandidate struct {
+10 -10
View File
@@ -50,14 +50,14 @@ type IndependentVerification struct {
}
type AgentDispatchIntent struct {
DispatchID string
IdempotencyKey string
DelegationID DelegationID
RunID AgentRunID
ImplementationID string
ImplementationRevision int64
SourceRevision string
WorktreeID string
VerificationPlanRef string
CreatedAtUnixMillis int64
DispatchID string `json:"dispatch_id"`
IdempotencyKey string `json:"idempotency_key"`
DelegationID DelegationID `json:"delegation_id"`
RunID AgentRunID `json:"run_id"`
ImplementationID string `json:"implementation_id"`
ImplementationRevision int64 `json:"implementation_revision"`
SourceRevision string `json:"source_revision"`
WorktreeID string `json:"worktree_id"`
VerificationPlanRef string `json:"verification_plan_ref"`
CreatedAtUnixMillis int64 `json:"created_at_unix_millis"`
}
@@ -0,0 +1,30 @@
BEGIN;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.cognitive_admission_counters (
policy_id TEXT PRIMARY KEY,
window_start_unix_millis BIGINT NOT NULL,
accepted_count INTEGER NOT NULL DEFAULT 0,
concurrent_count INTEGER NOT NULL DEFAULT 0,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (policy_id <> ''), CHECK (window_start_unix_millis > 0),
CHECK (accepted_count >= 0), CHECK (concurrent_count >= 0),
CHECK (updated_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.cognitive_admission_leases (
lease_id TEXT PRIMARY KEY,
policy_id TEXT NOT NULL REFERENCES capability.cognitive_admission_counters(policy_id),
state TEXT NOT NULL,
expires_at_unix_millis BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
CHECK (lease_id <> ''), CHECK (state IN ('ACTIVE','RELEASED','EXPIRED')),
CHECK (expires_at_unix_millis > 0), CHECK (created_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS cognitive_admission_lease_expiry_idx
ON capability.cognitive_admission_leases(policy_id, state, expires_at_unix_millis);
RESET ROLE;
COMMIT;
@@ -0,0 +1,49 @@
BEGIN;
SET ROLE epistemics_role;
CREATE TABLE IF NOT EXISTS epistemics.truth_bundles (
bundle_id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
source_kind TEXT NOT NULL,
source_locator TEXT NOT NULL,
source_title TEXT NOT NULL,
source_publisher TEXT NOT NULL,
snapshot_id TEXT NOT NULL,
snapshot_source_id TEXT NOT NULL,
snapshot_content_digest TEXT NOT NULL,
snapshot_captured_at_unix_millis BIGINT NOT NULL,
snapshot_freshness TEXT NOT NULL,
excerpt_id TEXT NOT NULL,
excerpt_snapshot_id TEXT NOT NULL,
excerpt_text TEXT NOT NULL,
excerpt_start BIGINT NOT NULL,
excerpt_end BIGINT NOT NULL,
observation_id TEXT NOT NULL,
observation_observed_at_unix_millis BIGINT NOT NULL,
claim_id TEXT NOT NULL,
claim_statement TEXT NOT NULL,
claim_observation_id TEXT NOT NULL,
claim_citation_id TEXT NOT NULL,
claim_verification TEXT NOT NULL,
belief_id TEXT NOT NULL,
belief_claim_id TEXT NOT NULL,
belief_state TEXT NOT NULL,
citation_id TEXT NOT NULL,
citation_excerpt_id TEXT NOT NULL,
citation_evidence TEXT NOT NULL,
citation_coverage TEXT NOT NULL,
citation_conflict TEXT NOT NULL,
citation_resolved_at_unix_millis BIGINT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
UNIQUE (snapshot_id, excerpt_id, claim_id),
CHECK (bundle_id <> ''), CHECK (source_id <> ''), CHECK (source_locator <> ''),
CHECK (excerpt_start >= 0), CHECK (excerpt_end >= excerpt_start),
CHECK (recorded_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS truth_bundles_source_time_idx
ON epistemics.truth_bundles (source_id, recorded_at_unix_millis DESC);
RESET ROLE;
COMMIT;
@@ -0,0 +1,8 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.graph_nodes ADD COLUMN IF NOT EXISTS dispatch_id TEXT NOT NULL DEFAULT '';
RESET ROLE;
COMMIT;
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-assistant-postgres-1}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "migration runner: env file is missing" >&2
exit 1
fi
set -a
# The deployment env file is controlled by the production operator. Values
# are used only by the local Docker/psql boundary and are never logged.
. "$ENV_FILE"
set +a
: "${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required}"
POSTGRES_SUPERUSER="${POSTGRES_SUPERUSER//$'\r'/}"
DATABASE="${POSTGRES_DB:-assistant}"
DATABASE="${DATABASE//$'\r'/}"
psql_exec() {
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
}
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
if [[ ! "$MIGRATION_BASELINE" =~ ^[0-9]{3}$ ]]; then
echo "migration runner: invalid MIGRATION_BASELINE" >&2
exit 1
fi
now_ms="$(date +%s%3N)"
for file in "$REPO_DIR"/database/migrations/[0-9][0-9][0-9]_*.sql; do
migration_id="$(basename "$file" .sql)"
number="${migration_id:0:3}"
if (( 10#$number <= 10#$MIGRATION_BASELINE )); then
psql_exec -c "INSERT INTO public.assistant_migration_history(migration_id, applied_at_unix_millis) VALUES ('$migration_id', $now_ms) ON CONFLICT (migration_id) DO NOTHING;"
fi
done
fi
for file in "$REPO_DIR"/database/migrations/[0-9][0-9][0-9]_*.sql; do
[[ -f "$file" ]] || continue
migration_id="$(basename "$file" .sql)"
applied="$(psql_exec -At -c "SELECT 1 FROM public.assistant_migration_history WHERE migration_id='$migration_id' LIMIT 1;")"
[[ "$applied" == "1" ]] && continue
psql_exec < "$file"
now_ms="$(date +%s%3N)"
psql_exec -c "INSERT INTO public.assistant_migration_history(migration_id, applied_at_unix_millis) VALUES ('$migration_id', $now_ms);"
done
+43 -1
View File
@@ -80,6 +80,11 @@ services:
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
SECRET_BACKEND_ROOT: /run/secrets
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR: ${COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR:-60}
COGNITIVE_ADMISSION_MAX_INPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_INPUT_BYTES:-81920}
COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES:-16384}
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
@@ -113,6 +118,22 @@ services:
volumes:
- assistant_core_secrets:/run/secrets:ro
work-graph-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: work-graph-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: :8088
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
reflection-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: reflection-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -129,6 +150,25 @@ services:
volumes:
- assistant_core_secrets:/run/secrets:ro
research-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: research-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: :8087
RESEARCH_SOURCE_ROOT: /var/lib/research-sources
RESEARCH_HTTP_ALLOWED_HOSTS: assistant.cin.su,git.cin.su
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -237,5 +277,7 @@ volumes:
assistant_notification_reader_secret:
external: true
llm_local_models:
external: true
name: llm-local-models
external: true
assistant_research_sources:
name: assistant_research_sources
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000140-research-evaluation-runtime.json",
"task_id": "DEV-000140",
"task_file": "dev/tasks/DEV-000148-agent-worker-no-dry-run.json",
"task_id": "DEV-000148",
"task_revision": 1
}
@@ -1,23 +1,23 @@
{
"task_id": "DEV-000115",
"task_revision": 1,
"task_revision": 6,
"architecture_revision": "ADR-0018",
"base_repo_revision": "e83cf35",
"base_repo_revision": "9b26260",
"risk_tier": "E",
"intent": "Add a deterministic cognitive admission policy for concurrency, accepted/hour, deadline and input/output budgets without changing provider routing or bindings.",
"requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Explainable admission decision"],
"allowed_change_scope": {"modules": ["cognitive-admission"], "paths": ["contracts/capability/**", "modules/capability/domain/**", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "migrations/**", "deploy/**", "security/**", "cmd/**", "runtime/**", "adapters/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing cognitive capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary"], "may_change": ["Typed admission request/policy/decision contract"]},
"intent": "Run bounded cognitive admission as a durable capability-owner lease with restart recovery, while preserving provider routing and explicit fallback.",
"requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Durable admission lease", "Explainable admission decision"],
"allowed_change_scope": {"modules": ["cognitive-admission", "core-runtime", "cognitive-worker", "deployment"], "paths": ["contracts/capability/**", "adapters/postgres/capability_repository.go", "database/migrations/033_cognitive_admission_leases.sql", "cmd/core-runtime/main.go", "cmd/cognitive-worker/**", "deploy/docker-compose.prod.yml", "deploy/apply-migrations.sh", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/authority/**", "modules/work/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["Existing cognitive capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary", "capability-owned PostgreSQL schema"], "may_change": ["Typed admission request/policy/decision and lease contract"]},
"semantic_owner": "capability-cognitive-admission",
"canonical_state": [],
"state_transitions": ["request + policy + observed counters -> admitted or typed rejection"],
"side_effects_and_privileges": ["Pure policy evaluation; no provider call, queue, persistence, binding mutation or external mutation"],
"persistence_and_migrations": ["No schema or migration changes"],
"failure_retry_restart": ["Rejected requests carry a reason; retry requires a fresh policy evaluation"],
"assumptions": ["Counters are supplied by a later durable admission implementation", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"],
"acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "Simple/normal/heavy complexity remains a routing input, not authority"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "admission policy tests"],
"canonical_state": ["capability.cognitive_admission_counters", "capability.cognitive_admission_leases"],
"state_transitions": ["request + policy + durable counters -> admitted lease or typed rejection", "active lease -> released/expired"],
"side_effects_and_privileges": ["Capability database role only; internal core endpoint; provider calls remain behind cognitive gateway"],
"persistence_and_migrations": ["database/migrations/033_cognitive_admission_leases.sql", "deploy/apply-migrations.sh records applied migration IDs and is idempotent"],
"failure_retry_restart": ["Expired leases are recovered on next admission transaction; duplicate request IDs are idempotent; rejected requests fall back locally"],
"assumptions": ["Core Runtime remains the capability-owner admission boundary", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"],
"acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "expired lease does not permanently block after restart", "cognitive worker uses admission before provider and releases lease"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "compose config", "admission policy and HTTP boundary tests"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000141",
"task_revision": 2,
"architecture_revision": "ADR-0018",
"base_repo_revision": "38cff16",
"risk_tier": "E",
"intent": "Keep cognitive readiness available when an optional opportunistic fleet member is unavailable while preserving typed degraded evidence.",
"requirement_ids": ["WP-003", "WP-203", "Fleet failover readiness"],
"allowed_change_scope": {"modules": ["runtime-health", "cognitive-worker"], "paths": ["runtime/health/**", "cmd/cognitive-worker/main.go", "runtime/health/server_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000141-optional-fleet-readiness.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing readiness Probe and typed readiness response"], "may_change": ["Optional dependency probe behavior"]},
"semantic_owner": "runtime-health and cognitive-worker",
"canonical_state": [],
"state_transitions": ["optional provider unavailable -> readiness remains ready with typed degraded check", "required dependency unavailable -> readiness remains degraded"],
"side_effects_and_privileges": ["No external mutation; no provider invocation"],
"persistence_and_migrations": ["None"],
"failure_retry_restart": ["Optional provider can recover on subsequent readiness probe"],
"assumptions": ["CPU/Lifeboat remains a qualified fallback when opportunistic GPU is unavailable"],
"acceptance_scenarios": ["GPU timeout does not hide degraded evidence or make fallback unavailable", "required dependency failure still returns 503"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000142",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "0b321fe",
"risk_tier": "E",
"intent": "Persist typed Truth/Research bundles with exact source, snapshot, excerpt and citation provenance in the epistemics owner schema.",
"requirement_ids": ["WP-102", "Truth runtime", "Research provenance persistence"],
"allowed_change_scope": {"modules": ["epistemics", "database"], "paths": ["contracts/epistemics/truth.go", "modules/epistemics/domain/**", "adapters/postgres/epistemics_repository.go", "database/migrations/034_epistemics_truth_bundles.sql", "tests/research_contracts/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000142-truth-bundle-persistence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "deploy/**", "security/**", "cmd/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["TruthBundle, Source, SourceSnapshot, SourceExcerpt and Citation contracts"], "may_change": ["TruthBundle carries exact source evidence needed for durable provenance"]},
"semantic_owner": "epistemics",
"canonical_state": ["epistemics.truth_bundles"],
"state_transitions": ["compiled truth bundle -> durable append/update by exact citation identity"],
"side_effects_and_privileges": ["epistemics_role writes only epistemics.truth_bundles"],
"persistence_and_migrations": ["Migration 034; idempotent table/index creation"],
"failure_retry_restart": ["bundle write is idempotent by citation-derived bundle id"],
"assumptions": ["external connectors remain read-only and are a separate follow-up"],
"acceptance_scenarios": ["exact source chain is persisted", "duplicate bundle write is idempotent", "cross-owner schemas are untouched"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000143",
"task_revision": 3,
"architecture_revision": "ADR-0018",
"base_repo_revision": "f9e2173",
"risk_tier": "E",
"intent": "Run a private research runtime that evaluates immutable connector evidence and durably persists exact TruthBundle provenance.",
"requirement_ids": ["WP-102", "Truth runtime", "Research persistence orchestration"],
"allowed_change_scope": {"modules": ["epistemics", "research-runtime", "deployment"], "paths": ["contracts/epistemics/research.go", "modules/epistemics/application/**", "adapters/postgres/epistemics_repository.go", "adapters/research/**", "cmd/research-runtime/**", "deploy/docker-compose.prod.yml", "tests/research_runtime/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000143-research-runtime-orchestration.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "security/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["ResearchEvaluationRequest, ResearchResult, TruthBundle and epistemics repository"], "may_change": ["Private persistence orchestration endpoint"]},
"semantic_owner": "epistemics",
"canonical_state": ["epistemics.truth_bundles"],
"state_transitions": ["repository/web source -> immutable digest/excerpt -> evaluated finding -> persisted provenance"],
"side_effects_and_privileges": ["epistemics_role writes through repository only; no source mutation or provider call"],
"persistence_and_migrations": ["Consumes migration 034 already in main"],
"failure_retry_restart": ["idempotent repository writes; runtime restart-safe"],
"assumptions": ["source fetching connectors are read-only callers and remain a separate package"],
"acceptance_scenarios": ["runtime ready with epistemics DB", "valid evidence is persisted", "invalid evidence is rejected", "no public compose port is exposed"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000144",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "23b04aa",
"risk_tier": "E",
"intent": "Expose explainable runtime fleet readiness and explicit fallback tier from live provider probes.",
"requirement_ids": ["WP-203", "WP-601", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["capability", "cognitive-worker"], "paths": ["contracts/capability/fleet.go", "cmd/cognitive-worker/**", "tests/cognitive_registry/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000144-fleet-readiness-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["FleetReadiness and SelectFleetTier"], "may_change": ["JSON projection and live probe endpoint"]},
"semantic_owner": "capability and cognitive-worker",
"canonical_state": [],
"state_transitions": ["live provider probes -> typed member states -> preferred tier or explicit Lifeboat fallback"],
"side_effects_and_privileges": ["Read-only HTTP probes only"],
"persistence_and_migrations": ["None"],
"failure_retry_restart": ["Each request re-probes providers; no in-memory health authority"],
"assumptions": ["Lifeboat is the explicit fallback tier"],
"acceptance_scenarios": ["unavailable CPU/GPU selects Lifeboat", "unconfigured Medium is visible", "no fallback returns typed 503"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000145",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "7b06cd64a279db5aec67ada5f3c705f114ffd86b",
"risk_tier": "E",
"intent": "Connect the durable agent worker to an explicit typed external execution bridge with lease and callback evidence.",
"requirement_ids": ["WP-301", "Agent Execution runtime and recovery"],
"allowed_change_scope": {"modules": ["work", "agent-worker", "agentbridge"], "paths": ["contracts/work/bridge.go", "adapters/agentbridge/**", "modules/work/application/**", "modules/work/ports/**", "cmd/agent-worker/**", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000145-agent-execution-bridge-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", ".gitea/**", "contracts/authority/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["AgentDispatchIntent", "AgentLease", "AgentCallback"], "may_change": ["serializable bridge request/result contract"]},
"semantic_owner": "work",
"canonical_state": ["work agent lease and callback head"],
"state_transitions": ["durable dispatch -> lease/heartbeat -> bridge result -> result callback -> completed or explicit waiting/failure"],
"side_effects_and_privileges": ["outbound HTTP call to configured execution bridge only; no credentials forwarded"],
"persistence_and_migrations": ["reuse existing work lease/callback persistence; no migration"],
"failure_retry_restart": ["bridge timeout is failure of the current attempt; durable dispatch remains evidence-bearing and stale claims are recoverable"],
"assumptions": ["an independently operated bridge will implement POST /v1/execute", "bridge URL is deployment configuration and absent URL preserves legacy dry-run until bridge capability is deployed"],
"acceptance_scenarios": ["typed bridge request reaches executor", "completed bridge result persists callback evidence", "invalid/unavailable bridge fails closed", "existing stale-claim recovery remains intact"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000146",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "16d714238e6e77a105731311eaa54f3f486d3431",
"risk_tier": "E",
"intent": "Provide a durable work-graph runtime with bounded fan-out scheduling, dispatch queue binding, and revisioned fan-in completion.",
"requirement_ids": ["WP-400", "Multi-Agent Work Graph"],
"allowed_change_scope": {"modules": ["work", "work-graph-runtime"], "paths": ["contracts/work/graph.go", "contracts/work/lease.go", "contracts/work/dispatch_queue.go", "modules/work/application/**", "modules/work/ports/**", "adapters/postgres/work_repository.go", "database/migrations/035_work_graph_dispatch_binding.sql", "cmd/work-graph-runtime/**", "deploy/docker-compose.prod.yml", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000146-work-graph-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "contracts/authority/**", "modules/identity/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["WorkGraph", "AgentDispatchIntent", "AgentDispatchQueueItem"], "may_change": ["GraphNode dispatch binding serialization"]},
"semantic_owner": "work",
"canonical_state": ["work graph revision, graph nodes, graph edges, dispatch intent and queue"],
"state_transitions": ["DRAFT -> ADMITTED -> RUNNING -> PARTIAL/COMPLETED or FAILED", "BLOCKED_PREREQUISITE -> LEASED -> SUCCEEDED/FAILED", "each transition writes a new graph revision"],
"side_effects_and_privileges": ["work-role local graph and dispatch writes only"],
"persistence_and_migrations": ["035 adds work.graph_nodes.dispatch_id; migration runner applies it before runtime start"],
"failure_retry_restart": ["revisioned graph history survives process restart; dispatch queue claims remain recoverable by agent-worker"],
"assumptions": ["dispatch intents supplied with schedule request are already authorized by work-owned planning flow"],
"acceptance_scenarios": ["two independent nodes fan out under MaxParallel", "dependent join remains blocked until both predecessors succeed", "join completion yields graph COMPLETED", "cycle and missing dispatch bindings fail closed"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000147",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "641a8cd8fbed4ccb70838f73c0cbec3e38108088",
"risk_tier": "E",
"intent": "Expose truthful model fleet and fallback readiness in the authenticated creator cabinet without widening public access.",
"requirement_ids": ["WP-601", "WP-800", "model-fleet-operations"],
"allowed_change_scope": {"modules": ["interaction", "capability"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/server_test.go", "cmd/creator-web/static/index.html", "cmd/creator-web/static/assets/console.js", "cmd/creator-web/static/assets/console.css", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000147-model-fleet-operations-ui.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "database/**", "modules/**", "contracts/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["identity.CreatorSession", "cognitive fleet readiness HTTP response"], "may_change": []},
"semantic_owner": "interaction",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["authenticated read-only proxy only"],
"persistence_and_migrations": [],
"failure_retry_restart": ["UI renders unavailable/degraded state; no retrying mutation"],
"assumptions": ["cognitive runtime remains the only model/provider boundary", "authenticated cabinet already has a valid creator session"],
"acceptance_scenarios": ["unauthenticated fleet request returns 401", "invalid creator session is denied", "authenticated readiness is proxied without exposing public route", "FAST/MEDIUM/HEAVY/LIFEBOAT member states render with their observed reasons", "cabinet layout remains usable at mobile width"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000148",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "7030a51b69a4d7345c5475d2fcced0e6d0441916",
"risk_tier": "E",
"intent": "Prevent agent-worker from claiming or completing dispatches without a configured real executor while preserving durable stale-lease recovery.",
"requirement_ids": ["WP-301", "WP-302", "INV-AUTH-001", "INV-CONT-001"],
"allowed_change_scope": {"modules": ["work", "agent-worker"], "paths": ["cmd/agent-worker/main.go", "modules/work/application/worker.go", "modules/work/application/worker_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000148-agent-worker-no-dry-run.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "database/**", "contracts/**", "adapters/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["WorkerStore", "AgentExecutor"], "may_change": []},
"semantic_owner": "work",
"canonical_state": ["dispatch queue claims and stale lease recovery"],
"state_transitions": ["executor absent -> recovery-only; no new dispatch claim"],
"side_effects_and_privileges": ["work-owner stale claim recovery only when executor is absent"],
"persistence_and_migrations": [],
"failure_retry_restart": ["worker remains restartable and resumes recovery-only mode until bridge is configured"],
"assumptions": ["AGENT_EXECUTION_BRIDGE_URL is the only production activation path for external execution"],
"acceptance_scenarios": ["missing bridge never claims a queued dispatch", "missing bridge never marks work completed", "stale claims are still recovered", "configured bridge path remains unchanged"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,40 @@
package application
import (
"context"
"git.cin.su/m/Assistant/contracts/epistemics"
domain "git.cin.su/m/Assistant/modules/epistemics/domain"
)
type TruthBundleStore interface {
SaveTruthBundle(context.Context, epistemics.TruthBundle, int64) error
}
// EvaluateAndPersist evaluates only supplied immutable connector evidence and
// persists the exact source chain used by each candidate. It never performs a
// network fetch and never promotes a belief to verified state.
func EvaluateAndPersist(ctx context.Context, request epistemics.ResearchEvaluationRequest, store TruthBundleStore, evaluatedAtUnixMillis int64) (epistemics.ResearchResult, error) {
result, err := (ResearchService{Connector: staticConnector{candidates: request.Candidates}}).Evaluate(ctx, request.Plan, evaluatedAtUnixMillis)
if err != nil {
return epistemics.ResearchResult{}, err
}
for _, candidate := range request.Candidates {
bundle, err := domain.CompileObservedClaim(candidate.Source, candidate.Snapshot, candidate.Excerpt, candidate.Statement, candidate.Snapshot.CapturedAtUnixMillis, candidate.Conflict)
if err != nil {
return epistemics.ResearchResult{}, err
}
if err := store.SaveTruthBundle(ctx, bundle, evaluatedAtUnixMillis); err != nil {
return epistemics.ResearchResult{}, err
}
}
return result, nil
}
type staticConnector struct {
candidates []epistemics.ResearchCandidate
}
func (c staticConnector) Collect(context.Context, epistemics.ResearchPlan) ([]epistemics.ResearchCandidate, error) {
return c.candidates, nil
}
+1 -1
View File
@@ -106,5 +106,5 @@ func compileResearchCandidate(candidate epistemics.ResearchCandidate) (epistemic
}
observation := epistemics.Observation{ID: epistemics.ObservationID("observation:" + string(candidate.Excerpt.ID)), SourceSnapshot: candidate.Snapshot.ID, Excerpt: candidate.Excerpt.ID, Text: candidate.Excerpt.Text, ObservedAt: candidate.Snapshot.CapturedAtUnixMillis}
claim := epistemics.Claim{ID: epistemics.ClaimID("claim:" + string(candidate.Excerpt.ID)), Statement: candidate.Statement, ObservationID: observation.ID, CitationID: citation.ID, Verification: epistemics.VerificationUnverified}
return epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(candidate.Excerpt.ID)), ClaimID: claim.ID, State: epistemics.BeliefProvisional}, Citation: citation}, nil
return epistemics.TruthBundle{Source: candidate.Source, Snapshot: candidate.Snapshot, Excerpt: candidate.Excerpt, Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(candidate.Excerpt.ID)), ClaimID: claim.ID, State: epistemics.BeliefProvisional}, Citation: citation}, nil
}
+1 -1
View File
@@ -23,7 +23,7 @@ func CompileObservedClaim(source epistemics.Source, snapshot epistemics.SourceSn
if conflict == epistemics.ConflictPresent || conflict == epistemics.ConflictUnknown {
state = epistemics.BeliefConflicted
}
bundle := epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(excerpt.ID)), ClaimID: claim.ID, State: state}, Citation: citation}
bundle := epistemics.TruthBundle{Source: source, Snapshot: snapshot, Excerpt: excerpt, Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(excerpt.ID)), ClaimID: claim.ID, State: state}, Citation: citation}
if bundle.Observation.SourceSnapshot != snapshot.ID || bundle.Claim.ObservationID != bundle.Observation.ID || bundle.Claim.CitationID != bundle.Citation.ID || bundle.Belief.ClaimID != bundle.Claim.ID {
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
}
@@ -0,0 +1,52 @@
package application
import (
"context"
"time"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
)
func ExecuteAgentDispatch(ctx context.Context, store ports.ExecutionStore, executor ports.AgentExecutor, intent work.AgentDispatchIntent, workerID string, token work.AgentFencingToken, now time.Time, ttl time.Duration) (work.AgentExecutionResult, error) {
if executor == nil {
return work.AgentExecutionResult{}, ports.ErrAgentExecutorUnavailable
}
if err := domain.ValidateDispatchIntent(intent); err != nil {
return work.AgentExecutionResult{}, err
}
lease, err := domain.AcquireAgentLease(intent.RunID, workerID, token, now, ttl)
if err != nil {
return work.AgentExecutionResult{}, err
}
if err := store.SaveDispatchIntent(ctx, intent); err != nil {
return work.AgentExecutionResult{}, err
}
if err := store.SaveAgentLease(ctx, lease); err != nil {
return work.AgentExecutionResult{}, err
}
heartbeat := work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: 1, Kind: work.AgentCallbackHeartbeat, ObservedAt: now}
if err := domain.ValidateAgentCallback(heartbeat, lease, 0); err != nil {
return work.AgentExecutionResult{}, err
}
if err := store.AcceptAgentCallback(ctx, heartbeat); err != nil {
return work.AgentExecutionResult{}, err
}
result, err := executor.Execute(ctx, work.AgentExecutionRequest{Intent: intent, WorkerID: workerID, FencingToken: token, LeaseExpiresAt: lease.ExpiresAt})
if err != nil {
return work.AgentExecutionResult{}, err
}
observedAt := time.UnixMilli(result.ObservedAtUnixMillis)
callback := work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: 2, Kind: work.AgentCallbackResult, ObservedAt: observedAt}
if result.State == work.AgentExecutionWaiting {
callback.Kind = work.AgentCallbackCheckpoint
}
if err := domain.ValidateAgentCallback(callback, lease, heartbeat.Sequence); err != nil {
return work.AgentExecutionResult{}, err
}
if err := store.AcceptAgentCallback(ctx, callback); err != nil {
return work.AgentExecutionResult{}, err
}
return result, nil
}
@@ -0,0 +1,42 @@
package application
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
type bridgeStore struct {
callbacks []work.AgentCallback
lease work.AgentLease
}
func (s *bridgeStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error { return nil }
func (s *bridgeStore) SaveAgentLease(_ context.Context, lease work.AgentLease) error {
s.lease = lease
return nil
}
func (s *bridgeStore) AcceptAgentCallback(_ context.Context, callback work.AgentCallback) error {
s.callbacks = append(s.callbacks, callback)
return nil
}
type bridgeExecutor struct{}
func (bridgeExecutor) Execute(context.Context, work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "done", ObservedAtUnixMillis: 2}, nil
}
func TestExecuteAgentDispatchPersistsLeaseAndBridgeCallbacks(t *testing.T) {
store := &bridgeStore{}
now := time.UnixMilli(1)
result, err := ExecuteAgentDispatch(context.Background(), store, bridgeExecutor{}, work.AgentDispatchIntent{DispatchID: "d", IdempotencyKey: "i", DelegationID: "del", RunID: "run", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: 1}, "worker", 1, now, time.Minute)
if err != nil || result.State != work.AgentExecutionCompleted {
t.Fatalf("result=%+v err=%v", result, err)
}
if len(store.callbacks) != 2 || store.callbacks[0].Kind != work.AgentCallbackHeartbeat || store.callbacks[1].Kind != work.AgentCallbackResult {
t.Fatalf("callbacks=%+v", store.callbacks)
}
}
+143
View File
@@ -0,0 +1,143 @@
package application
import (
"context"
"errors"
"time"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
)
var ErrGraphDispatchBinding = errors.New("graph node dispatch binding is missing")
type GraphScheduleRequest struct {
Graph work.WorkGraph `json:"graph"`
Intents []work.AgentDispatchIntent `json:"intents"`
Now int64 `json:"now_unix_millis"`
}
type GraphCompleteRequest struct {
GraphID string `json:"graph_id"`
Revision int64 `json:"revision"`
NodeID work.GraphNodeID `json:"node_id"`
Succeeded bool `json:"succeeded"`
}
func AdmitAndSchedule(ctx context.Context, store ports.GraphStore, request GraphScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) {
if request.Now < 1 {
request.Now = time.Now().UnixMilli()
}
admitted, err := domain.AdmitGraph(request.Graph)
if err != nil {
return work.WorkGraph{}, nil, err
}
if err := store.SaveWorkGraph(ctx, admitted); err != nil {
return work.WorkGraph{}, nil, err
}
ready, err := domain.ScheduleReadyNodes(admitted, 0)
if err != nil {
return work.WorkGraph{}, nil, err
}
intentByDispatch := make(map[string]work.AgentDispatchIntent, len(request.Intents))
for _, intent := range request.Intents {
intentByDispatch[intent.DispatchID] = intent
}
for _, node := range admitted.Nodes {
if node.DispatchID == "" {
return work.WorkGraph{}, nil, ErrGraphDispatchBinding
}
if _, ok := intentByDispatch[node.DispatchID]; !ok {
return work.WorkGraph{}, nil, ErrGraphDispatchBinding
}
}
for _, intent := range request.Intents {
if err := store.SaveDispatchIntent(ctx, intent); err != nil {
return work.WorkGraph{}, nil, err
}
}
next := admitted
next.Revision++
next.State = work.GraphRunning
selected := make(map[work.GraphNodeID]bool, len(ready))
for _, nodeID := range ready {
selected[nodeID] = true
}
for i := range next.Nodes {
if !selected[next.Nodes[i].ID] {
continue
}
if next.Nodes[i].DispatchID == "" {
return work.WorkGraph{}, nil, ErrGraphDispatchBinding
}
intent := intentByDispatch[next.Nodes[i].DispatchID]
if err := store.EnqueueDispatch(ctx, work.AgentDispatchQueueItem{DispatchID: intent.DispatchID, State: work.AgentDispatchQueued, AvailableAtUnixMillis: request.Now, UpdatedAtUnixMillis: request.Now}); err != nil {
return work.WorkGraph{}, nil, err
}
next.Nodes[i].State = work.GraphNodeLeased
}
if err := store.SaveWorkGraph(ctx, next); err != nil {
return work.WorkGraph{}, nil, err
}
return next, ready, nil
}
func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID string, revision int64, nodeID work.GraphNodeID, success bool) (work.WorkGraph, error) {
graph, err := store.LoadWorkGraph(ctx, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
found := false
for i := range graph.Nodes {
if graph.Nodes[i].ID == nodeID {
found = true
if success {
graph.Nodes[i].State = work.GraphNodeSuccess
} else {
graph.Nodes[i].State = work.GraphNodeFailed
}
}
}
if !found {
return work.WorkGraph{}, domain.ErrInvalidGraph
}
if !success {
graph.State = work.GraphFailed
} else {
ready, err := domain.ReadyGraphNodes(graph)
if err != nil {
return work.WorkGraph{}, err
}
for i := range graph.Nodes {
for _, candidate := range ready {
if graph.Nodes[i].ID == candidate && graph.Nodes[i].State == work.GraphNodeBlocked {
graph.Nodes[i].State = work.GraphNodeReady
if graph.Nodes[i].DispatchID == "" {
return work.WorkGraph{}, ErrGraphDispatchBinding
}
if err := store.EnqueueDispatch(ctx, work.AgentDispatchQueueItem{DispatchID: graph.Nodes[i].DispatchID, State: work.AgentDispatchQueued, AvailableAtUnixMillis: time.Now().UnixMilli(), UpdatedAtUnixMillis: time.Now().UnixMilli()}); err != nil {
return work.WorkGraph{}, err
}
}
}
}
allSuccess := true
for _, node := range graph.Nodes {
if node.State != work.GraphNodeSuccess {
allSuccess = false
break
}
}
if allSuccess {
graph.State = work.GraphCompleted
} else {
graph.State = work.GraphPartial
}
}
graph.Revision++
if err := store.SaveWorkGraph(ctx, graph); err != nil {
return work.WorkGraph{}, err
}
return graph, nil
}
+39
View File
@@ -15,6 +15,14 @@ type WorkerCycleResult struct {
Idle bool
}
// RecoverOnlyWorkerCycle keeps durable recovery alive while the real executor
// capability is absent. It intentionally does not claim a dispatch: a missing
// executor must never be represented as successful work.
func RecoverOnlyWorkerCycle(ctx context.Context, store ports.WorkerStore, now time.Time, staleAfter time.Duration) (WorkerCycleResult, error) {
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
return WorkerCycleResult{RecoveredClaims: recovered, Idle: err == nil}, err
}
func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID string, token work.AgentFencingToken, now time.Time, staleAfter, leaseTTL time.Duration) (WorkerCycleResult, error) {
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
if err != nil {
@@ -41,3 +49,34 @@ func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID s
}
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID, Completed: true}, nil
}
func ExecuteWorkerCycleWithExecutor(ctx context.Context, store ports.WorkerStore, executor ports.AgentExecutor, workerID string, token work.AgentFencingToken, now time.Time, staleAfter, leaseTTL time.Duration) (WorkerCycleResult, error) {
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
if err != nil {
return WorkerCycleResult{}, err
}
item, err := store.ClaimNextDispatch(ctx, workerID, now.UnixMilli())
if err != nil {
return WorkerCycleResult{RecoveredClaims: recovered}, err
}
if item.DispatchID == "" {
return WorkerCycleResult{RecoveredClaims: recovered, Idle: true}, nil
}
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
if err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
result, err := ExecuteAgentDispatch(ctx, store, executor, intent, workerID, token, now, leaseTTL)
if err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
if result.State != work.AgentExecutionCompleted {
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, nil
}
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID, Completed: true}, nil
}
+46
View File
@@ -0,0 +1,46 @@
package application
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
type recoveryOnlyStore struct {
recovered int64
claimed bool
}
func (s *recoveryOnlyStore) RecoverStaleClaims(context.Context, int64, int64) (int64, error) {
return s.recovered, nil
}
func (s *recoveryOnlyStore) ClaimNextDispatch(context.Context, string, int64) (work.AgentDispatchQueueItem, error) {
s.claimed = true
return work.AgentDispatchQueueItem{}, nil
}
func (s *recoveryOnlyStore) LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error) {
return work.AgentDispatchIntent{}, nil
}
func (s *recoveryOnlyStore) CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error {
return nil
}
func (s *recoveryOnlyStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
return nil
}
func (s *recoveryOnlyStore) SaveAgentLease(context.Context, work.AgentLease) error { return nil }
func (s *recoveryOnlyStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
return nil
}
func TestRecoverOnlyWorkerCycleNeverClaimsDispatch(t *testing.T) {
store := &recoveryOnlyStore{recovered: 2}
result, err := RecoverOnlyWorkerCycle(context.Background(), store, time.UnixMilli(1_700_000_000_000), time.Minute)
if err != nil {
t.Fatal(err)
}
if result.RecoveredClaims != 2 || !result.Idle || store.claimed {
t.Fatalf("unexpected recovery-only result: %+v claimed=%t", result, store.claimed)
}
}
+4
View File
@@ -6,6 +6,10 @@ import (
"git.cin.su/m/Assistant/contracts/work"
)
type AgentExecutor interface {
Execute(context.Context, work.AgentExecutionRequest) (work.AgentExecutionResult, error)
}
type ExecutionStore interface {
SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error
SaveAgentLease(context.Context, work.AgentLease) error
+14
View File
@@ -0,0 +1,14 @@
package ports
import (
"context"
"git.cin.su/m/Assistant/contracts/work"
)
type GraphStore interface {
SaveWorkGraph(context.Context, work.WorkGraph) error
LoadWorkGraph(context.Context, string, int64) (work.WorkGraph, error)
SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error
EnqueueDispatch(context.Context, work.AgentDispatchQueueItem) error
}
+3
View File
@@ -2,10 +2,13 @@ package ports
import (
"context"
"errors"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrAgentExecutorUnavailable = errors.New("agent executor unavailable")
type WorkerStore interface {
ExecutionStore
LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error)
+16 -4
View File
@@ -26,8 +26,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
type Probe struct {
Name string
Check func(context.Context) error
Name string
Check func(context.Context) error
Optional bool
}
type Result struct {
@@ -59,11 +60,13 @@ func (h ReadinessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := probe.Check(ctx)
cancel()
if err != nil {
ready = false
result.State, result.Reason, result.Detail = "DEGRADED", "DEPENDENCY_UNAVAILABLE", err.Error()
if !probe.Optional {
ready = false
}
}
}
if result.State != "READY" {
if result.State != "READY" && !probe.Optional {
ready = false
}
results = append(results, result)
@@ -96,6 +99,15 @@ func HTTPProbe(client *http.Client, endpoint string) Probe {
}}
}
// OptionalHTTPProbe reports dependency degradation without making the whole
// runtime unavailable. This is used for opportunistic fleet members when a
// qualified local fallback remains available.
func OptionalHTTPProbe(client *http.Client, endpoint string) Probe {
probe := HTTPProbe(client, endpoint)
probe.Optional = true
return probe
}
func TCPProbe(name, host, port string) Probe {
return Probe{Name: name, Check: func(ctx context.Context) error {
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(host, port))
+13
View File
@@ -26,3 +26,16 @@ func TestReadinessReturns200WhenAllProbesPass(t *testing.T) {
t.Fatalf("readiness response = %d %s", recorder.Code, recorder.Body.String())
}
}
func TestReadinessStaysAvailableWithOptionalDependencyDegraded(t *testing.T) {
handler := ReadinessHandler{Runtime: "test", Probes: []Probe{
{Name: "lifeboat", Check: func(context.Context) error { return nil }},
{Name: "opportunistic-gpu", Optional: true, Check: func(context.Context) error { return errors.New("timeout") }},
}}
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil))
body := recorder.Body.String()
if recorder.Code != http.StatusOK || !strings.Contains(body, `"status":"ready"`) || !strings.Contains(body, `"state":"DEGRADED"`) {
t.Fatalf("readiness response = %d %s", recorder.Code, body)
}
}
@@ -0,0 +1,60 @@
package agent_execution_test
import (
"context"
"strconv"
"testing"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
)
type graphStore struct {
graphs map[string]work.WorkGraph
queued []string
intents []string
}
func (s *graphStore) SaveWorkGraph(_ context.Context, graph work.WorkGraph) error {
if s.graphs == nil {
s.graphs = map[string]work.WorkGraph{}
}
s.graphs[graph.ID+":"+itoa(graph.Revision)] = graph
return nil
}
func (s *graphStore) LoadWorkGraph(_ context.Context, id string, revision int64) (work.WorkGraph, error) {
return s.graphs[id+":"+itoa(revision)], nil
}
func (s *graphStore) SaveDispatchIntent(_ context.Context, intent work.AgentDispatchIntent) error {
s.intents = append(s.intents, intent.DispatchID)
return nil
}
func (s *graphStore) EnqueueDispatch(_ context.Context, item work.AgentDispatchQueueItem) error {
s.queued = append(s.queued, item.DispatchID)
return nil
}
func itoa(value int64) string {
return strconv.FormatInt(value, 10)
}
func TestGraphRuntimeSchedulesFanOutAndCompletesFanIn(t *testing.T) {
store := &graphStore{}
graph, ready, err := application.AdmitAndSchedule(context.Background(), store, application.GraphScheduleRequest{Graph: work.WorkGraph{ID: "g", Revision: 1, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 1}, Nodes: []work.GraphNode{{ID: "a", DispatchID: "d-a", IdempotencyKey: "a", State: work.GraphNodeReady}, {ID: "b", DispatchID: "d-b", IdempotencyKey: "b", State: work.GraphNodeReady}, {ID: "join", DispatchID: "d-join", IdempotencyKey: "join", State: work.GraphNodeBlocked}}, Edges: []work.GraphEdge{{From: "a", To: "join"}, {From: "b", To: "join"}}}, Intents: []work.AgentDispatchIntent{{DispatchID: "d-a"}, {DispatchID: "d-b"}, {DispatchID: "d-join"}}, Now: 1})
if err != nil || len(ready) != 2 || len(store.queued) != 2 {
t.Fatalf("graph=%+v ready=%v queued=%v err=%v", graph, ready, store.queued, err)
}
if _, err := application.CompleteGraphNode(context.Background(), store, "g", 2, "a", true); err != nil {
t.Fatal(err)
}
if _, err := application.CompleteGraphNode(context.Background(), store, "g", 3, "b", true); err != nil {
t.Fatal(err)
}
if len(store.queued) != 3 || store.queued[2] != "d-join" {
t.Fatalf("fan-in dispatch was not enqueued: %v", store.queued)
}
completed, err := application.CompleteGraphNode(context.Background(), store, "g", 4, "join", true)
if err != nil || completed.State != work.GraphCompleted {
t.Fatalf("completed=%+v err=%v", completed, err)
}
}
@@ -1,6 +1,8 @@
package cognitive_admission_test
import (
"encoding/json"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
@@ -26,6 +28,13 @@ func TestAdmitReturnsExplainableDecision(t *testing.T) {
}
}
func TestAdmissionRequestUsesStableWireFields(t *testing.T) {
raw, err := json.Marshal(capability.AdmissionRequest{Complexity: capability.AdmissionNormal, InputBytes: 10, MaxOutputBytes: 20, DeadlineMillis: 30})
if err != nil || !strings.Contains(string(raw), `"input_bytes":10`) || !strings.Contains(string(raw), `"max_output_bytes":20`) || !strings.Contains(string(raw), `"deadline_millis":30`) {
t.Fatalf("admission wire shape drifted: %s %v", raw, err)
}
}
func TestAdmitRejectsBudgetAndDeadline(t *testing.T) {
policy := capability.AdmissionPolicy{ID: "local-v1", MaxConcurrent: 1, MaxAcceptedPerHour: 1, MaxInputBytes: 10, MaxOutputBytes: 10}
if decision := capability.Admit(capability.AdmissionRequest{InputBytes: 11, MaxOutputBytes: 1}, policy, capability.AdmissionCounters{}, 1); decision.Reason != capability.AdmissionReasonInputBudget {
@@ -0,0 +1,26 @@
package research_contracts_test
import (
"os"
"strings"
"testing"
)
func TestTruthBundlePersistenceMigrationIsOwnedAndIdempotent(t *testing.T) {
payload, err := os.ReadFile("../../database/migrations/034_epistemics_truth_bundles.sql")
if err != nil {
t.Fatal(err)
}
script := string(payload)
for _, required := range []string{
"SET ROLE epistemics_role",
"CREATE TABLE IF NOT EXISTS epistemics.truth_bundles",
"UNIQUE (snapshot_id, excerpt_id, claim_id)",
"CREATE INDEX IF NOT EXISTS truth_bundles_source_time_idx",
"RESET ROLE",
} {
if !strings.Contains(script, required) {
t.Fatalf("migration missing %q", required)
}
}
}
@@ -0,0 +1,20 @@
package research_runtime_test
import (
"context"
"testing"
researchadapter "git.cin.su/m/Assistant/adapters/research"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestHTTPConnectorRejectsNonAllowlistedOrNonHTTPSSources(t *testing.T) {
connector := researchadapter.HTTPConnector{AllowedHosts: map[string]struct{}{"assistant.cin.su": {}}}
base := epistemics.WebResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "q", SourceClasses: []string{"web"}, FreshnessPolicy: "current"}, SourceID: "s", Statement: "statement"}
for _, url := range []string{"http://assistant.cin.su/healthz", "https://127.0.0.1/secret", "https://evil.example/"} {
base.URL = url
if _, err := connector.Collect(context.Background(), base, 10); err != researchadapter.ErrHTTPSourceRejected {
t.Fatalf("url=%s err=%v", url, err)
}
}
}
@@ -0,0 +1,29 @@
package research_runtime_test
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/application"
)
type store struct{ saved []epistemics.TruthBundle }
func (s *store) SaveTruthBundle(_ context.Context, value epistemics.TruthBundle, _ int64) error {
s.saved = append(s.saved, value)
return nil
}
func TestEvaluateAndPersistStoresExactCandidateChain(t *testing.T) {
candidate := epistemics.ResearchCandidate{
PropositionKey: "p1", Statement: "fact", Source: epistemics.Source{ID: "source-1", Kind: "repo", Locator: "file:///repo/doc", Title: "doc"},
Snapshot: epistemics.SourceSnapshot{ID: "snapshot-1", SourceID: "source-1", ContentDigest: "sha256:x", CapturedAtUnixMillis: 10, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: "excerpt-1", SnapshotID: "snapshot-1", Text: "fact", Start: 0, End: 4}, Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
}
store := &store{}
result, err := application.EvaluateAndPersist(context.Background(), epistemics.ResearchEvaluationRequest{Plan: epistemics.ResearchPlan{ID: "plan-1", Question: "q", SourceClasses: []string{"repo"}, FreshnessPolicy: "current"}, Candidates: []epistemics.ResearchCandidate{candidate}}, store, 20)
if err != nil || len(result.Findings) != 1 || len(store.saved) != 1 || store.saved[0].Excerpt.ID != candidate.Excerpt.ID || store.saved[0].Source.Locator != candidate.Source.Locator {
t.Fatalf("result=%#v saved=%#v err=%v", result, store.saved, err)
}
}
@@ -0,0 +1,28 @@
package research_runtime_test
import (
"context"
"os"
"path/filepath"
"testing"
researchadapter "git.cin.su/m/Assistant/adapters/research"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestRepositoryConnectorReturnsDigestAndExactExcerpt(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "evidence.txt"), []byte("exact repository evidence"), 0600); err != nil {
t.Fatal(err)
}
candidate, err := (researchadapter.RepositoryConnector{Root: root}).Collect(context.Background(), epistemics.RepositoryResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "q", SourceClasses: []string{"repository"}, FreshnessPolicy: "current"}, PropositionKey: "p", SourceID: "s", Path: "evidence.txt", Statement: "statement"}, 10)
if err != nil || candidate.Excerpt.Text != "exact repository evidence" || candidate.Snapshot.ContentDigest[:7] != "sha256:" || candidate.Excerpt.SnapshotID != candidate.Snapshot.ID {
t.Fatalf("candidate=%#v err=%v", candidate, err)
}
}
func TestRepositoryConnectorRejectsPathTraversal(t *testing.T) {
if _, err := (researchadapter.RepositoryConnector{Root: t.TempDir()}).Collect(context.Background(), epistemics.RepositoryResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "q", SourceClasses: []string{"repository"}, FreshnessPolicy: "current"}, SourceID: "s", Path: "../secret", Statement: "statement"}, 10); err != researchadapter.ErrRepositoryPathRejected {
t.Fatalf("path traversal error=%v", err)
}
}