Compare commits

...
Author SHA1 Message Date
m c81e2269e1 feat(capability): consume qualification evidence in selection
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m26s
2026-08-23 19:28:13 +03:00
m 47327cc6d8 Merge pull request 'feat(capability): persist model qualification observations' (#219) from codex/dev-000242-durable-qualification-observations into main
CI / test (push) Successful in 1m16s
feat(capability): persist model qualification observations
2026-08-23 16:22:58 +00:00
m d0ede46352 feat(capability): persist model qualification observations
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m15s
2026-08-23 19:21:04 +03:00
m e4ba1cb5fa Merge pull request 'docs(fleet): record medium and gpu qualification' (#218) from codex/dev-000241-fleet-qualification-evidence into main
CI / test (push) Successful in 1m15s
docs(fleet): record medium and gpu qualification
2026-08-23 16:13:58 +00:00
m 90104d2553 docs(fleet): record medium and gpu qualification
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m7s
2026-08-23 19:11:41 +03:00
m ca0d002073 Merge pull request 'docs(fleet): record CPU to lifeboat failover' (#217) from codex/dev-000240-cpu-lifeboat-failover-evidence into main
CI / test (push) Successful in 52s
docs(fleet): record CPU to lifeboat failover
2026-08-23 16:07:31 +00:00
m 20db36f52e docs(fleet): record CPU to lifeboat failover
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 1m11s
2026-08-23 19:05:49 +03:00
m 383d4c6e4f Merge pull request 'fix(cognitive): resolve providers by stable instance identity' (#216) from codex/dev-000239-stable-provider-resolution into main
CI / test (push) Successful in 50s
fix(cognitive): resolve providers by stable instance identity
2026-08-23 16:03:07 +00:00
m f3ddb45fe5 fix(cognitive): resolve providers by stable instance identity
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m12s
2026-08-23 19:01:18 +03:00
m 6061eb5c13 Merge pull request 'docs(runtime): record local fallback gateway evidence' (#215) from codex/dev-000238-local-fallback-evidence into main
CI / test (push) Successful in 1m16s
docs(runtime): record local fallback gateway evidence
2026-08-23 15:54:47 +00:00
m 0e46a64702 docs(runtime): record local fallback gateway evidence
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 47s
2026-08-23 18:52:27 +03:00
m 0609561dfe Merge pull request 'docs(ui): record authenticated console section smoke' (#214) from codex/dev-000237-ui-authenticated-section-smoke into main
CI / test (push) Successful in 1m0s
docs(ui): record authenticated console section smoke
2026-08-23 15:46:29 +00:00
m 1b88a35b82 docs(ui): record authenticated console section smoke
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m14s
2026-08-23 18:44:36 +03:00
m be01b6b883 Merge pull request 'fix(cognitive): satisfy qualification advisory contract' (#213) from codex/dev-000236-qualification-locale-fix into main
CI / test (push) Successful in 1m13s
fix(cognitive): satisfy qualification advisory contract
2026-08-23 15:37:10 +00:00
m deaddbca12 fix(cognitive): satisfy qualification advisory contract
CI / test (push) Successful in 1m19s
CI / test (pull_request) Successful in 1m8s
2026-08-23 18:34:45 +03:00
m b20611590a Merge pull request 'feat(cognitive): add bounded model qualification probe' (#212) from codex/dev-000235-model-qualification-probe into main
CI / test (push) Successful in 51s
feat(cognitive): add bounded model qualification probe
2026-08-23 15:30:02 +00:00
m d4972afd11 feat(cognitive): add bounded model qualification probe
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 45s
2026-08-23 18:27:22 +03:00
m 60e8292c1d Merge pull request 'feat(work): persist orchestration snapshot for replan' (#211) from codex/dev-000234-orchestration-snapshot-replan into main
CI / test (push) Successful in 1m15s
feat(work): persist orchestration snapshot for replan
2026-08-23 15:16:43 +00:00
m da251e1aa8 feat(work): persist orchestration snapshot for replan
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m11s
2026-08-23 18:13:07 +03:00
m eac39a3fd4 Merge pull request 'feat(work): require verified graph result fan-in' (#210) from codex/dev-000233-graph-result-fanin into main
CI / test (push) Successful in 1m0s
feat(work): require verified graph result fan-in
2026-08-23 15:07:44 +00:00
m 1f8af98f31 feat(work): require verified graph result fan-in
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 44s
2026-08-23 18:05:23 +03:00
m ee16103e03 Merge pull request 'feat(work): materialize durable solution waves' (#209) from codex/dev-000231-solution-orchestrator into main
CI / test (push) Successful in 1m11s
feat(work): materialize durable solution waves
2026-08-23 13:59:42 +00:00
m 09d53adfd1 feat(work): materialize durable solution waves
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 44s
2026-08-23 16:56:58 +03:00
m 89694047b1 Merge pull request 'feat(work): add persistent solution search orchestration policy' (#208) from codex/dev-000231-solution-orchestrator into main
CI / test (push) Successful in 1m20s
feat(work): add persistent solution search orchestration policy
2026-08-23 13:48:58 +00:00
m 23e0525c08 feat(work): add persistent solution search orchestration policy
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 58s
2026-08-23 16:45:58 +03:00
m 45874f2186 Merge pull request 'feat: expose governed development gate controller' (#207) from codex/dev-000230-development-gate-controller into main
CI / test (push) Successful in 1m16s
feat: expose governed development gate controller
2026-08-23 13:40:04 +00:00
m ae5848d799 feat: expose governed development gate controller
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m10s
2026-08-23 16:36:48 +03:00
m ec79f19a79 Merge pull request 'feat: connect creator research to truth runtime' (#206) from codex/dev-000229-research-bridge into main
CI / test (push) Successful in 1m53s
feat: connect creator research to truth runtime
2026-08-23 13:30:35 +00:00
m f63c4bb40c feat: connect creator research to truth runtime
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m8s
2026-08-23 16:27:00 +03:00
m 22f839df99 Merge pull request 'feat: expose controlled provider discovery status' (#205) from codex/dev-000228-controlled-discovery into main
CI / test (push) Successful in 1m9s
feat: expose controlled provider discovery status
2026-08-23 12:57:48 +00:00
m c41634a065 feat: expose controlled provider discovery status
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m2s
2026-08-23 15:54:30 +03:00
m 2609f31e59 Merge pull request 'feat: expose bounded local agent executor' (#204) from codex/dev-000227-local-executor into main
CI / test (push) Successful in 1m10s
feat: expose bounded local agent executor
2026-08-23 12:48:40 +00:00
m 7aa41b3be2 feat: expose bounded local agent executor
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m6s
2026-08-23 15:45:00 +03:00
m 1f6ca363ee docs: record authenticated UI acceptance evidence
CI / test (push) Successful in 1m14s
Merge DEV-000226 authenticated UI acceptance evidence
2026-08-23 12:31:16 +00:00
m 02471990ef docs: record authenticated UI acceptance
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 1m13s
2026-08-23 15:29:04 +03:00
m 40b56798e2 fix: serve login shell at direct route
CI / test (push) Successful in 1m9s
Merge DEV-000225 login route fix
2026-08-23 12:21:44 +00:00
m c53d7ded06 fix: serve login shell at direct route
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m13s
2026-08-23 15:19:32 +03:00
m e080c34a77 docs: record live fleet routing evidence
CI / test (push) Successful in 1m23s
Merge DEV-000224 live fleet evidence
2026-08-23 11:56:15 +00:00
m 306f53d484 docs: record live fleet routing evidence
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 57s
2026-08-23 14:53:50 +03:00
m c4411afaaf docs: reconcile current audit status
CI / test (push) Successful in 1m4s
Merge DEV-000223 status reconciliation
2026-08-23 11:42:59 +00:00
m c508941a8a docs: reconcile current audit status
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 59s
2026-08-23 14:41:14 +03:00
m ee68e2207e feat: extend task verifier and unresolved case builder
CI / test (push) Successful in 56s
Merge DEV-000222 verifier modes and case builder
2026-08-23 11:32:03 +00:00
m 3d9a5bccba feat: extend task verifier and unresolved case builder
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 51s
2026-08-23 14:30:45 +03:00
m d736500d98 feat(work): add independent task verifier runtime
CI / test (push) Successful in 1m10s
feat(work): add independent task verifier runtime (#198)
2026-08-23 11:21:10 +00:00
m 33b51a4445 feat(work): add independent task verifier runtime
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m7s
2026-08-23 14:19:38 +03:00
m 0aab10863c feat(capability): require verifier evidence for quality
CI / test (push) Successful in 1m5s
feat(capability): require verifier evidence for quality (#197)
2026-08-23 11:10:02 +00:00
m b17caa3ea9 feat(capability): require verifier evidence for quality
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 1m8s
2026-08-23 14:08:29 +03:00
m 66ed523248 fix(cognitive): separate reachability from quality evidence
CI / test (push) Successful in 1m9s
fix(cognitive): separate reachability from quality evidence (#196)
2026-08-23 11:03:41 +00:00
m c5b5055ff1 fix(cognitive): separate reachability from quality evidence
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 59s
2026-08-23 14:02:02 +03:00
m 7476f43ffa feat(capability): gate additional model evaluations
CI / test (push) Successful in 1m5s
feat(capability): gate additional model evaluations (#195)
2026-08-23 10:57:52 +00:00
m be29ed5776 feat(capability): gate additional model evaluations
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 59s
2026-08-23 13:56:16 +03:00
m ab840878fe docs(status): refresh current runtime evidence
CI / test (push) Successful in 58s
docs(status): refresh current runtime evidence (#194)
2026-08-23 10:52:12 +00:00
m aebc932b97 docs(status): refresh current runtime evidence
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m7s
2026-08-23 13:50:44 +03:00
m 05a246ea3f fix(agent-worker): report local fallback truthfully
CI / test (push) Successful in 1m5s
fix(agent-worker): report local fallback truthfully (#193)
2026-08-23 10:42:50 +00:00
m c847988630 fix(agent-worker): report local fallback truthfully
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m3s
2026-08-23 13:41:22 +03:00
m 62f0c529b3 feat(work): persist governed development candidates
CI / test (push) Successful in 53s
feat(work): persist governed development candidates (#192)
2026-08-23 10:29:05 +00:00
m 54958d0902 feat(work): persist governed development candidates
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 58s
2026-08-23 13:26:41 +03:00
m 8d3e7f7cac feat(work): make agent execution recovery durable
CI / test (push) Successful in 1m4s
DEV-000214 durable Agent Execution recovery
2026-08-23 10:15:19 +00:00
m c9eb398857 chore(governance): declare canary scope for recovery
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m8s
2026-08-23 13:12:25 +03:00
m 284ea88553 feat(work): make agent execution recovery durable
CI / test (push) Failing after 33s
CI / test (pull_request) Failing after 42s
2026-08-23 13:10:18 +03:00
m f9b9765100 feat(work): link task evaluation worker execution
CI / test (push) Successful in 1m7s
DEV-000213 worker evaluator link
2026-08-23 09:59:00 +00:00
m 863a379048 feat(work): link task evaluation worker execution
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 1m2s
2026-08-23 12:56:01 +03:00
m a86d1994c3 Merge pull request 'feat: execute bounded task evaluation cases' (#189) from codex/wp-212-task-evaluator-invocation into main
CI / test (push) Successful in 49s
2026-08-23 09:41:37 +00:00
m 699eac00eb feat(cognitive): execute bounded task evaluation cases
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 45s
2026-08-23 12:39:58 +03:00
m dff6c91c8e Merge pull request 'feat: accept task evaluation evidence callbacks' (#188) from codex/wp-211-task-evaluator-callback into main
CI / test (push) Successful in 1m4s
2026-08-23 09:30:22 +00:00
m 7afe3ff48f feat(work): accept task evaluation evidence callbacks
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m4s
2026-08-23 12:28:42 +03:00
m 6dec47c791 Merge pull request 'feat: persist task evaluation worker context' (#187) from codex/wp-210-task-evaluator-worker into main
CI / test (push) Successful in 1m2s
2026-08-23 09:20:26 +00:00
m def0e4aff0 feat(work): persist task evaluation worker context
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 59s
2026-08-23 12:18:53 +03:00
m 9a6ab6a9ca Merge pull request 'feat: schedule task evaluation through Work Graph' (#186) from codex/wp-209-task-evaluator into main
CI / test (push) Successful in 1m1s
2026-08-23 09:10:32 +00:00
m 8f20326c07 feat(work): schedule task evaluation through work graph
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m7s
2026-08-23 12:09:07 +03:00
m a82592a130 Merge pull request 'feat: task-aware model evaluation foundation' (#185) from codex/wp-208-task-model-evaluation into main
CI / test (push) Successful in 1m11s
2026-08-23 08:59:18 +00:00
m feea909d69 feat(capability): add task-aware model evaluation evidence
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 1m0s
2026-08-23 11:57:47 +03:00
m b78a026ee1 Merge pull request 'docs: record gpu adapter invocation qualification' (#184) from codex/wp-207-gpu-qualification into main
CI / test (push) Successful in 1m9s
docs: record gpu adapter invocation qualification
2026-08-23 08:43:15 +00:00
m cf35c0ae25 docs: record gpu adapter invocation qualification
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 56s
2026-08-23 11:40:55 +03:00
m 9f96393fb9 Merge pull request 'docs: refresh audit completion matrix' (#183) from codex/wp-206-completion-matrix into main
CI / test (push) Successful in 1m8s
docs: refresh audit completion matrix
2026-08-23 08:32:43 +00:00
m beee8deb34 docs: refresh audit completion matrix
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m6s
2026-08-23 11:30:07 +03:00
m faab913227 Merge pull request 'docs: record gpu invocation smoke gap' (#182) from codex/wp-205-gpu-smoke-evidence into main
CI / test (push) Successful in 1m5s
docs: record gpu invocation smoke gap
2026-08-23 08:24:36 +00:00
m 346a8c8467 docs: record gpu invocation smoke gap
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m6s
2026-08-23 11:21:51 +03:00
m 288d18a4fb Merge pull request 'docs: record protected gpu model discovery' (#181) from codex/wp-200-gpu-discovery-evidence into main
CI / test (push) Successful in 1m13s
2026-08-23 08:02:48 +00:00
m 97ed2e90ee docs: record protected gpu model discovery
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m5s
2026-08-23 11:00:00 +03:00
m b4d166480e Merge pull request 'feat: keep agent work durable without external bridge' (#180) from codex/wp-301-local-fallback into main
CI / test (push) Successful in 1m10s
2026-08-23 07:55:59 +00:00
m 46b35161e9 feat: keep agent work durable without external bridge
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m8s
2026-08-23 10:53:10 +03:00
m bee238b61e Merge pull request 'docs: record authenticated ui quick smoke' (#179) from codex/wp-806-authenticated-ui-visual-evidence into main
CI / test (push) Successful in 1m13s
2026-08-23 07:49:55 +00:00
m ca9c4bede5 Merge remote-tracking branch 'origin/main' into codex/wp-806-authenticated-ui-visual-evidence
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 1m5s
2026-08-23 10:46:39 +03:00
m 48d73c3a4b docs: record authenticated ui quick smoke
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m5s
2026-08-23 10:43:03 +03:00
m 76c558ee1a Merge pull request 'docs: record authenticated ui visual evidence' (#178) from codex/wp-806-authenticated-ui-visual-evidence into main
CI / test (push) Successful in 1m9s
2026-08-23 07:21:41 +00:00
m e530058659 docs: record authenticated ui visual evidence
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m2s
2026-08-23 10:18:21 +03:00
m 79feab8e41 Merge pull request 'docs: record medium bounded live smoke evidence' (#177) from codex/wp-200-medium-live-smoke-evidence into main
CI / test (push) Successful in 1m10s
2026-08-23 02:03:10 +00:00
m 03943f152d docs: record medium bounded live smoke evidence
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m9s
2026-08-23 04:59:14 +03:00
m c8aa731eeb Merge pull request 'fix: disable hidden thinking for bounded local advice' (#176) from codex/wp-200-medium-thinking-control into main
CI / test (push) Successful in 1m12s
2026-08-23 01:04:25 +00:00
m 45435814ee fix: disable hidden thinking for bounded local advice
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m6s
2026-08-23 04:01:09 +03:00
m c7170ee751 Merge pull request 'docs: record durable development gate evidence' (#175) from codex/wp-701-development-gate-live-evidence into main
CI / test (push) Successful in 1m6s
2026-08-23 00:55:29 +00:00
m e9f338484a docs: record durable development gate evidence
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m4s
2026-08-23 03:52:11 +03:00
m bb131147f8 Merge pull request 'fix: grant development gate owner access' (#174) from codex/wp-701-durable-development-gate into main
CI / test (push) Successful in 1m9s
2026-08-23 00:48:59 +00:00
m 22b84ead9c fix: grant development gate owner access
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m8s
2026-08-23 03:45:36 +03:00
m 42c7df964b Merge pull request 'feat: persist governed development gates' (#173) from codex/wp-701-durable-development-gate into main
CI / test (push) Successful in 1m4s
2026-08-23 00:37:34 +00:00
m c542642bf0 feat: persist governed development gates
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m4s
2026-08-23 03:34:43 +03:00
m fde14cf0b8 Merge pull request 'docs: record durable agent canary evidence' (#172) from codex/wp-301-agent-canary-live-evidence into main
CI / test (push) Successful in 1m6s
2026-08-23 00:22:21 +00:00
m b52590c458 docs: record durable agent canary evidence
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m6s
2026-08-23 03:18:57 +03:00
m 6854458e21 Merge pull request 'fix: make agent canary replayable' (#171) from codex/wp-301-agent-canary-replayable into main
CI / test (push) Successful in 1m6s
2026-08-23 00:11:50 +00:00
m 5658bed520 fix: make agent canary replayable
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m4s
2026-08-23 03:10:07 +03:00
m 7252296a74 Merge pull request 'fix: protect agent canary database credentials' (#170) from codex/wp-301-agent-canary-protected-config into main
CI / test (push) Successful in 1m2s
2026-08-23 00:05:20 +00:00
m 348856a9c2 chore: align canary task base with main
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m9s
2026-08-23 03:03:43 +03:00
m 7779dc37d1 fix: protect agent canary database credentials
CI / test (push) Failing after 45s
CI / test (pull_request) Failing after 48s
2026-08-23 03:01:35 +03:00
m c1db571a39 Merge pull request 'docs: record live degraded fleet readiness' (#169) from codex/wp-301-fleet-live-degraded-evidence into main
CI / test (push) Successful in 1m9s
2026-08-22 23:53:36 +00:00
m 5f65f3d570 docs: record live degraded fleet readiness
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m4s
2026-08-23 02:51:51 +03:00
m bb18cb4f1c Merge pull request 'docs: record live reflection reconciliation' (#168) from codex/wp-500-reflection-live-evidence into main
CI / test (push) Successful in 1m9s
2026-08-22 23:45:57 +00:00
m 3f7b1f6e9c docs: record live reflection reconciliation
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m2s
2026-08-23 02:44:14 +03:00
m 413a432e8c Merge pull request 'docs: avoid drifting production revision projection' (#167) from codex/cleanup-obsolete-docs into main
CI / test (push) Successful in 1m13s
2026-08-22 23:40:16 +00:00
m ca5f96115b docs: avoid drifting production revision projection
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m2s
2026-08-23 02:38:10 +03:00
m 9fe2f37012 Merge pull request 'docs: align audit status with deployed revision' (#166) from codex/cleanup-obsolete-docs into main
CI / test (push) Successful in 1m14s
2026-08-22 23:35:17 +00:00
m b7247c9abd docs: align audit status with deployed revision
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 1m2s
2026-08-23 02:33:33 +03:00
m be8561c7d8 Merge pull request 'chore: remove obsolete audit status history' (#165) from codex/cleanup-obsolete-docs into main
CI / test (push) Successful in 1m31s
2026-08-22 23:30:42 +00:00
m b78083f0d9 chore: remove obsolete audit status history
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m3s
2026-08-23 02:28:48 +03:00
m 5d9124d0df Merge pull request 'docs: synchronize live audit status evidence' (#164) from codex/wp-900-live-status-sync into main
CI / test (push) Successful in 1m16s
2026-08-22 23:23:07 +00:00
m b7a4c0f83e chore: activate live audit status task
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m7s
2026-08-23 02:21:24 +03:00
m 50d4bdf174 docs: synchronize live audit status evidence
CI / test (push) Failing after 44s
CI / test (pull_request) Failing after 47s
2026-08-23 02:19:12 +03:00
m 5b92cd6a58 Merge pull request 'fix: serialize graph synthesis request fields' (#163) from codex/wp-403-graph-persistence into main
CI / test (push) Successful in 1m17s
2026-08-22 23:10:08 +00:00
m 18d0911810 fix: serialize graph synthesis request fields
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 1m3s
2026-08-23 02:08:24 +03:00
m dcf8cbc855 Merge pull request 'feat: persist verified graph synthesis runtime' (#162) from codex/wp-403-graph-persistence into main
CI / test (push) Successful in 1m16s
2026-08-22 22:56:25 +00:00
m 3d30681279 feat: persist verified graph synthesis runtime
CI / test (push) Successful in 3m1s
CI / test (pull_request) Successful in 2m1s
2026-08-23 01:52:39 +03:00
m 41bc4b567b Merge pull request 'feat: gate graph synthesis on independent verification' (#161) from codex/wp-403-conflict-synthesis into main
CI / test (push) Successful in 1m3s
2026-08-22 22:45:18 +00:00
m 38f8f816d5 feat: gate graph synthesis on independent verification
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m33s
2026-08-23 01:43:35 +03:00
m 4d9d1b8023 Merge pull request 'feat: add isolated governed scaffold candidate' (#160) from codex/wp-602-scaffold-factory into main
CI / test (push) Successful in 1m28s
2026-08-22 22:37:46 +00:00
m c60a575fda feat: add isolated governed scaffold candidate
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 57s
2026-08-23 01:36:03 +03:00
m a09a7e811f Merge pull request 'feat: generate governed extension proposals from repeated evidence' (#159) from codex/wp-500-reflection-worker into main
CI / test (push) Successful in 1m10s
2026-08-22 22:29:49 +00:00
m ecbb34627a feat: generate governed extension proposals from repeated evidence
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m46s
2026-08-23 01:26:41 +03:00
m cdbccb6238 Merge pull request 'fix: align agent worker degraded status log' (#158) from codex/agent-bridge-protected-config into main
CI / test (push) Successful in 1m6s
2026-08-22 21:57:11 +00:00
m 6e28e02f98 fix: align agent worker degraded status log
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m1s
2026-08-23 00:54:44 +03:00
m 5812974a80 Merge pull request 'feat: load agent bridge from protected storage' (#157) from codex/agent-bridge-protected-config into main
CI / test (push) Successful in 54s
2026-08-22 21:50:17 +00:00
m 0b6a249178 feat: load agent bridge from protected storage
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 56s
2026-08-23 00:47:26 +03:00
m 7ab4b02afa Merge pull request 'feat: connect protected OpenAI-compatible GPU provider' (#156) from codex/gpu-openai-provider into main
CI / test (push) Successful in 35s
2026-08-22 21:27:58 +00:00
m d3a50c7afd feat: connect protected OpenAI-compatible GPU provider
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 35s
2026-08-23 00:26:17 +03:00
m 16a0e8c492 Merge pull request 'feat: add evidence-based adaptive model rating' (#155) from codex/model-rating-adaptive-routing into main
CI / test (push) Successful in 47s
2026-08-22 20:47:02 +00:00
m 0d6326b45b Merge remote-tracking branch 'origin/main' into codex/medium-invocation-timeout
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 36s
2026-08-22 23:45:33 +03:00
m 6b3a6b2dfb chore: activate adaptive fleet task
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 34s
2026-08-22 23:43:54 +03:00
m 30443027a6 feat: add evidence-based adaptive model rating
CI / test (push) Failing after 37s
CI / test (pull_request) Failing after 32s
2026-08-22 23:42:10 +03:00
m 416d7c3a82 Merge pull request 'docs: record bounded medium retest' (#154) from codex/medium-bounded-retest-evidence into main
CI / test (push) Successful in 46s
2026-08-22 20:01:34 +00:00
m 4709ef4607 docs: record bounded medium retest
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 47s
2026-08-22 22:59:43 +03:00
m 6f0e099632 Merge pull request 'feat: bound benchmark advisory output' (#153) from codex/bounded-medium-qualification into main
CI / test (push) Successful in 51s
2026-08-22 19:52:16 +00:00
m 775fa03ed4 feat: bound benchmark advisory output
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 34s
2026-08-22 22:50:20 +03:00
m 2ed2302526 Merge pull request 'docs: consolidate current runtime status' (#152) from codex/runtime-status-consolidation into main
CI / test (push) Successful in 45s
2026-08-22 19:43:53 +00:00
m 46538ba1e9 docs: consolidate current runtime status
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 35s
2026-08-22 22:42:01 +03:00
m 5960844fcb Merge pull request 'docs: record current heavy qualification' (#151) from codex/heavy-qualification-refresh into main
CI / test (push) Successful in 49s
2026-08-22 19:35:48 +00:00
m b60fc38333 docs: record current heavy qualification
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 37s
2026-08-22 22:33:51 +03:00
m dc1ee7ff3c Merge pull request 'docs: record heavy qualification gap' (#150) from codex/heavy-qualification-evidence into main
CI / test (push) Successful in 48s
2026-08-22 19:29:17 +00:00
m a51bf57d7a docs: record heavy qualification gap
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 36s
2026-08-22 22:27:24 +03:00
m d60fb8697a Merge pull request 'docs: record medium qualification retest' (#149) from codex/medium-qualification-retest-v2 into main
CI / test (push) Successful in 42s
2026-08-22 19:22:54 +00:00
m bf4b81e320 docs: record medium qualification retest
CI / test (push) Successful in 39s
CI / test (pull_request) Successful in 36s
2026-08-22 22:20:32 +03:00
m 389dd0c23d Merge pull request 'feat: configure medium provider invocation timeout' (#148) from codex/medium-invocation-timeout into main
CI / test (push) Successful in 48s
2026-08-22 19:13:26 +00:00
m 064a650763 chore: align medium timeout task base
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 38s
2026-08-22 22:11:43 +03:00
m 6ecb431ccd feat: configure medium provider invocation timeout
CI / test (push) Failing after 28s
CI / test (pull_request) Failing after 26s
2026-08-22 22:08:32 +03:00
m b29f8775e1 Merge pull request 'docs: record medium qualification timeout' (#147) from codex/medium-qualification-evidence into main
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 35s
2026-08-22 18:59:30 +00:00
m 85ee05e291 docs: record medium qualification timeout
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 36s
2026-08-22 21:57:29 +03:00
m 35aa64546b Merge pull request 'docs: record live medium failover evidence' (#146) from codex/refresh-medium-failover-evidence into main
CI / test (push) Successful in 43s
2026-08-22 18:43:41 +00:00
m 6e0d1a2279 docs: record live medium failover evidence
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 34s
2026-08-22 21:42:01 +03:00
m 64642f4bae Merge pull request 'fix: prefer available fleet tier before lifeboat' (#145) from codex/fleet-failover-priority into main
CI / test (push) Successful in 44s
2026-08-22 18:36:44 +00:00
m 2f9e9bdc9f fix: prefer available fleet tier before lifeboat
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 33s
2026-08-22 21:35:16 +03:00
m 2c7a85779b Merge pull request 'feat: connect existing medium model runtime' (#144) from codex/connect-medium-runtime into main
CI / test (push) Successful in 42s
2026-08-22 18:30:30 +00:00
m b4c3027e6d feat: connect existing medium model runtime
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 33s
2026-08-22 21:29:02 +03:00
m 503fe84074 Merge pull request 'docs: refresh fleet audit evidence' (#143) from codex/refresh-fleet-evidence into main
CI / test (push) Successful in 42s
2026-08-22 18:21:37 +00:00
m 3b551b2918 docs: refresh fleet audit evidence
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 33s
2026-08-22 21:20:10 +03:00
m 51427a246e Merge pull request 'fix: keep unconfigured medium out of readiness' (#142) from codex/optional-medium-readiness into main
CI / test (push) Successful in 47s
2026-08-22 18:15:36 +00:00
m 9b5ce2a850 fix: keep unconfigured medium out of readiness
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 34s
2026-08-22 21:14:12 +03:00
m 8f3920519b Merge pull request 'feat: add configurable medium model fleet slot' (#141) from codex/model-fleet-readiness into main
CI / test (push) Successful in 43s
2026-08-22 18:11:23 +00:00
m 2277f8a699 feat: add configurable medium model fleet slot
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 31s
2026-08-22 21:09:58 +03:00
m 205d8f6206 Merge pull request 'fix: align cpu readiness client timeout' (#140) from codex/per-provider-readiness-timeouts into main
CI / test (push) Successful in 43s
2026-08-22 18:03:16 +00:00
m ae06d33ccc fix: align cpu readiness client timeout
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 33s
2026-08-22 21:00:46 +03:00
m c7b57daaf8 Merge pull request 'fix: use provider-specific readiness timeouts' (#139) from codex/per-provider-readiness-timeouts into main
CI / test (push) Successful in 38s
2026-08-22 17:57:27 +00:00
m 8aa0123047 chore: format readiness health server
CI / test (push) Successful in 31s
CI / test (pull_request) Successful in 35s
2026-08-22 20:55:48 +03:00
m 4a3d348e39 fix: use provider-specific readiness timeouts
CI / test (push) Failing after 25s
CI / test (pull_request) Failing after 23s
2026-08-22 20:52:40 +03:00
m 4086b75224 Merge pull request 'fix: authenticate cognitive readiness probe' (#138) from codex/authorized-cpu-health-probe into main
CI / test (push) Successful in 43s
2026-08-22 17:47:25 +00:00
m 4f89c46545 fix: authenticate cognitive readiness probe
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 34s
2026-08-22 20:46:03 +03:00
m 5d5afc8888 Merge pull request 'fix: authenticate fleet readiness probe' (#137) from codex/authorized-fleet-readiness into main
CI / test (push) Successful in 48s
2026-08-22 17:41:55 +00:00
m 79ce5a0b82 fix: authenticate fleet readiness probe
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 32s
2026-08-22 20:40:33 +03:00
m 65a60d5f06 Merge pull request 'test: cover operations status surfaces' (#136) from codex/operations-ui-contract-refresh into main
CI / test (push) Successful in 49s
2026-08-22 17:36:54 +00:00
m 4739a22f7b test: cover operations status surfaces
CI / test (push) Successful in 32s
CI / test (pull_request) Successful in 33s
2026-08-22 20:35:33 +03:00
m c25a6346d0 Merge pull request 'docs: refresh completion evidence revision' (#135) from codex/status-after-completion-matrix into main
CI / test (push) Successful in 47s
2026-08-22 17:31:57 +00:00
m 5f71fa8dc4 docs: refresh completion evidence revision
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 32s
2026-08-22 20:30:37 +03:00
m bda71caece Merge pull request 'docs: publish audit completion matrix' (#134) from codex/completion-audit-matrix into main
CI / test (push) Successful in 44s
2026-08-22 17:27:36 +00:00
m 0656f88fd0 docs: publish audit completion matrix
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 32s
2026-08-22 20:26:14 +03:00
m 3cf44ada8a Merge pull request 'docs: refresh bridge wiring status' (#133) from codex/status-after-bridge-wiring into main
CI / test (push) Successful in 49s
2026-08-22 17:22:32 +00:00
m ca00377bec docs: refresh bridge wiring status
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 32s
2026-08-22 20:21:13 +03:00
m ada2da0c6c Merge pull request 'chore: wire optional agent bridge URL' (#132) from codex/agent-bridge-config-wiring into main
CI / test (push) Successful in 46s
2026-08-22 17:17:58 +00:00
m 82df14bde9 chore: wire optional agent bridge URL
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 33s
2026-08-22 20:16:36 +03:00
m e1186757bf Merge pull request 'docs: synchronize current production evidence' (#131) from codex/status-evidence-consolidation into main
CI / test (push) Successful in 45s
2026-08-22 16:00:39 +00:00
m 1f002bd1a3 docs: synchronize current production evidence
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 31s
2026-08-22 18:59:04 +03:00
m 3a52c18bef Merge pull request 'docs: record postgres restore evidence' (#130) from codex/postgres-restore-evidence into main
CI / test (push) Successful in 45s
2026-08-22 15:56:02 +00:00
m fe0351edae docs: record postgres restore evidence
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 34s
2026-08-22 18:54:41 +03:00
m 077f16e50c Merge pull request 'docs: record fleet and execution status evidence' (#129) from codex/fleet-execution-status-evidence into main
CI / test (push) Successful in 40s
2026-08-22 15:50:38 +00:00
m 3a9d3f0503 docs: record fleet and execution status evidence
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 34s
2026-08-22 18:49:08 +03:00
m 03c3484ee1 Merge pull request 'docs: publish current audit status' (#128) from codex/current-audit-status into main
CI / test (push) Successful in 41s
2026-08-22 14:56:38 +00:00
m e47d363145 docs: publish current audit status
CI / test (push) Successful in 38s
CI / test (pull_request) Successful in 33s
2026-08-22 17:55:07 +03:00
m 5703ab3ed5 Merge pull request 'docs(evidence): record live FAST gateway smoke' (#127) from codex/fast-live-evidence into main
CI / test (push) Successful in 41s
2026-08-22 14:45:02 +00:00
m d03e9d3307 docs(evidence): record live FAST gateway smoke
CI / test (push) Successful in 31s
CI / test (pull_request) Successful in 31s
2026-08-22 17:43:37 +03:00
m 283f5c044c Merge pull request 'chore(deploy): wire optional cognitive provider secret volume' (#126) from codex/fleet-secret-wiring into main
CI / test (push) Successful in 41s
2026-08-22 14:28:33 +00:00
m ea5327799a chore(deploy): wire optional cognitive provider secret volume
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 25s
2026-08-22 17:27:29 +03:00
m dbbc54171d Merge pull request 'feat(cognitive): resolve local provider key from secret ref' (#125) from codex/cognitive-cpu-auth into main
CI / test (push) Successful in 33s
2026-08-22 14:22:55 +00:00
m 2b98501a68 Merge origin/main before cognitive provider secret release
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 26s
2026-08-22 17:21:02 +03:00
m d082005b37 feat(cognitive): resolve local provider key from secret ref
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 26s
2026-08-22 17:19:04 +03:00
m 624c41d4f6 Merge pull request 'feat: expose agent worker capability status' (#124) from codex/wp-804-worker-status into main
CI / test (push) Successful in 37s
2026-08-22 14:09:26 +00:00
m 6d1d68e406 feat: expose agent worker capability status
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 26s
2026-08-22 17:08:09 +03:00
m 6871e36657 Merge pull request 'feat: expose durable agent state in creator cabinet' (#123) from codex/wp-801-agent-ui into main
CI / test (push) Successful in 41s
2026-08-22 14:01:40 +00:00
m b1ccc3200b feat: expose durable agent state in creator cabinet
CI / test (push) Successful in 28s
CI / test (pull_request) Successful in 25s
2026-08-22 17:00:33 +03:00
m ee678db42c Merge pull request 'docs: record live Lifeboat qualification evidence' (#122) from codex/wp-201-live-qualification into main
CI / test (push) Successful in 34s
2026-08-22 13:53:33 +00:00
m b16c12068e docs: record live Lifeboat qualification evidence
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 25s
2026-08-22 16:52:26 +03:00
m eb920bb9f2 Merge pull request 'fix: disable agent dry-run without executor' (#121) from codex/wp-301-executor-safety into main
CI / test (push) Successful in 43s
2026-08-22 13:45:40 +00:00
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
235 changed files with 8824 additions and 436 deletions
+32 -7
View File
@@ -22,17 +22,35 @@ import (
type OpenAICompatibleLocalAdapter struct {
base *url.URL
client *http.Client
apiKey string
}
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
}
// NewOpenAICompatibleLocalAdapterWithAPIKey creates an adapter for a gateway
// whose bearer credential was resolved by the trusted runtime boundary.
func NewOpenAICompatibleLocalAdapterWithAPIKey(raw, apiKey string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey, transport, 10*time.Second)
}
// NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout creates a local gateway
// adapter with an explicit invocation deadline. The request context remains
// authoritative for per-call cancellation; this client deadline prevents a
// provider from holding a worker indefinitely.
func NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey string, transport http.RoundTripper, timeout time.Duration) (*OpenAICompatibleLocalAdapter, error) {
base, err := url.Parse(raw)
if err != nil || base.Scheme != "http" || base.Host == "" || strings.TrimSuffix(base.Path, "/") != "/v1" || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
return nil, errors.New("invalid local compatible endpoint")
}
if timeout <= 0 {
return nil, errors.New("local compatible timeout must be positive")
}
if transport == nil {
transport = http.DefaultTransport
}
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}, nil
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, apiKey: strings.TrimSpace(apiKey)}, nil
}
type compatibleModelsResponse struct {
@@ -78,11 +96,15 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
}
type compatibleChatRequest struct {
Model string `json:"model"`
Messages []compatibleMessage `json:"messages"`
Temperature int `json:"temperature"`
Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens"`
Model string `json:"model"`
Messages []compatibleMessage `json:"messages"`
Temperature int `json:"temperature"`
Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens"`
ChatTemplateKwargs compatibleTemplateKwargs `json:"chat_template_kwargs"`
}
type compatibleTemplateKwargs struct {
EnableThinking bool `json:"enable_thinking"`
}
type compatibleMessage struct {
Role string `json:"role"`
@@ -103,7 +125,7 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" || len(request.Context) != 0 {
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
var result compatibleChatResponse
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
if err != nil {
@@ -145,6 +167,9 @@ func (a *OpenAICompatibleLocalAdapter) requestJSON(ctx context.Context, method,
return 0, err
}
request.Header.Set("Accept", "application/json")
if a.apiKey != "" {
request.Header.Set("Authorization", "Bearer "+a.apiKey)
}
if input != nil {
request.Header.Set("Content-Type", "application/json")
}
@@ -0,0 +1,94 @@
package models
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type captureRoundTripper struct {
authorization string
body []byte
}
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
if err != nil {
t.Fatal(err)
}
if adapter.client.Timeout != 47*time.Second {
t.Fatalf("unexpected client timeout: %s", adapter.client.Timeout)
}
}
func TestOpenAICompatibleLocalAdapterRejectsNonPositiveTimeout(t *testing.T) {
if _, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", nil, 0); err == nil {
t.Fatal("expected non-positive timeout to be rejected")
}
}
func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
t.authorization = request.Header.Get("Authorization")
if request.Body != nil {
t.body, _ = io.ReadAll(request.Body)
}
body := `{"data":[]}`
if strings.HasSuffix(request.URL.Path, "/chat/completions") {
body = `{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"ok"}}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`
}
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header), Request: request}, nil
}
func TestOpenAICompatibleLocalAdapterUsesResolvedAPIKey(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKey("http://gateway/v1", "resolved-key", transport)
if err != nil {
t.Fatal(err)
}
_, _ = adapter.Discover(context.Background())
if transport.authorization != "Bearer resolved-key" {
t.Fatalf("unexpected authorization header: %q", transport.authorization)
}
}
func TestOpenAICompatibleLocalAdapterDoesNotSendAuthorizationWithoutKey(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, _ = adapter.Discover(context.Background())
if transport.authorization != "" {
t.Fatalf("unexpected authorization header")
}
}
func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope", Message: "help", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1, ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("medium-model")})
if err != nil {
t.Fatal(err)
}
var payload struct {
ChatTemplateKwargs struct {
EnableThinking bool `json:"enable_thinking"`
} `json:"chat_template_kwargs"`
}
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if payload.ChatTemplateKwargs.EnableThinking {
t.Fatal("expected bounded advisory to disable template thinking")
}
}
+56 -2
View File
@@ -430,6 +430,32 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
}
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
return r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
}
func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
}
func (r *CapabilityRepository) LoadCognitiveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
var capabilityID capability.CapabilityID
var owner identity.PrincipalID
if err := r.pool.QueryRow(ctx, `SELECT capability_id,owner_principal_id FROM capability.cognitive_bindings WHERE binding_id=$1 AND revision=$2`, bindingID, revision).Scan(&capabilityID, &owner); err != nil {
return capability.ResolvedCognitiveBinding{}, err
}
candidates, err := r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
if err != nil {
return capability.ResolvedCognitiveBinding{}, err
}
for _, candidate := range candidates {
if candidate.Binding.ID == bindingID && candidate.Binding.Revision == revision {
return candidate, nil
}
}
return capability.ResolvedCognitiveBinding{}, pgx.ErrNoRows
}
func (r *CapabilityRepository) loadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
rows, err := r.pool.Query(ctx, `SELECT
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
@@ -454,7 +480,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
if err != nil {
return nil, err
}
if err = r.loadCognitiveEvidence(ctx, &value); err != nil {
if err = r.loadCognitiveEvidence(ctx, &value, profileID); err != nil {
return nil, err
}
result = append(result, value)
@@ -462,7 +488,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
return result, rows.Err()
}
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding) error {
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding, profileID capability.TaskEvaluationProfileID) error {
var evaluation capability.EvaluatedCapability
err := r.pool.QueryRow(ctx, `SELECT evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_evaluated_capabilities WHERE binding_id=$1 AND binding_revision=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Binding.ID, value.Binding.Revision).Scan(&evaluation.ID, &evaluation.BindingID, &evaluation.BindingRevision, &evaluation.CapabilityID, &evaluation.Result, &evaluation.QualityScoreMillis, &evaluation.CriticalFailureMillis, &evaluation.BenchmarkVersion, &evaluation.ArtifactID, &evaluation.ObservedAtUnixMillis)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
@@ -479,6 +505,24 @@ func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value
if err == nil {
value.Performance = &performance
}
var qualification capability.ModelQualificationObservation
err = r.pool.QueryRow(ctx, `SELECT observation_id,provider_instance_id,provider_model_id,status,finish_reason,latency_millis,output_observed,quality_assessed,category,observed_at_unix_millis FROM capability.cognitive_model_qualification_observations WHERE provider_instance_id=$1 AND provider_model_id=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Provider.ID, value.Model.ProviderModelID).Scan(&qualification.ID, &qualification.ProviderInstanceID, &qualification.ProviderModelID, &qualification.Status, &qualification.FinishReason, &qualification.LatencyMillis, &qualification.OutputObserved, &qualification.QualityAssessed, &qualification.Category, &qualification.ObservedAtUnixMillis)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err
}
if err == nil {
value.Qualification = &qualification
}
if profileID != "" {
var taskEvaluation capability.TaskEvaluation
err = r.pool.QueryRow(ctx, `SELECT evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_task_evaluations WHERE task_profile_id=$1 AND binding_id=$2 AND binding_revision=$3 ORDER BY observed_at_unix_millis DESC LIMIT 1`, profileID, value.Binding.ID, value.Binding.Revision).Scan(&taskEvaluation.ID, &taskEvaluation.ProfileID, &taskEvaluation.BindingID, &taskEvaluation.BindingRevision, &taskEvaluation.CapabilityID, &taskEvaluation.Result, &taskEvaluation.QualityScoreMillis, &taskEvaluation.ReliabilityScoreMillis, &taskEvaluation.LatencyScoreMillis, &taskEvaluation.OverallScoreMillis, &taskEvaluation.CriticalFailureMillis, &taskEvaluation.BenchmarkVersion, &taskEvaluation.ArtifactID, &taskEvaluation.ObservedAtUnixMillis)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err
}
if err == nil {
value.TaskEvaluation = &taskEvaluation
}
}
return nil
}
@@ -592,3 +636,13 @@ func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, valu
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
return err
}
func (r *CapabilityRepository) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_task_evaluations (evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.ProfileID, value.BindingID, value.BindingRevision, value.CapabilityID, value.Result, value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
return err
}
func (r *CapabilityRepository) SaveModelQualificationObservation(ctx context.Context, value capability.ModelQualificationObservation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_model_qualification_observations (observation_id,provider_instance_id,provider_model_id,status,finish_reason,latency_millis,output_observed,quality_assessed,category,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, value.ID, value.ProviderInstanceID, value.ProviderModelID, value.Status, value.FinishReason, value.LatencyMillis, value.OutputObserved, value.QualityAssessed, value.Category, value.ObservedAtUnixMillis)
return err
}
+215 -15
View File
@@ -2,10 +2,14 @@ package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"slices"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
@@ -14,9 +18,11 @@ import (
type WorkRepository struct{ pool *pgxpool.Pool }
var (
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
ErrAgentCallbackRejected = errors.New("agent callback rejected")
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
ErrAgentCallbackRejected = errors.New("agent callback rejected")
ErrDevelopmentGateConflict = errors.New("development gate revision conflict")
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
)
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
@@ -49,33 +55,135 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
return target, nil
}
func (r *WorkRepository) LoadDevelopmentGate(ctx context.Context, id string) (work.DevelopmentGate, error) {
var gate work.DevelopmentGate
var evidence, producer, verifier []string
err := r.pool.QueryRow(ctx, `SELECT gate_id,revision,updated_at_unix_millis,stage,evidence_refs,proposal_ref,task_packet_ref,worktree_ref,producer_run_refs,verifier_run_refs,verification_evidence,canary_evidence,promotion_ref,rollback_ref FROM work.development_gates WHERE gate_id=$1`, id).Scan(
&gate.ID, &gate.Revision, &gate.UpdatedAtUnixMillis, &gate.Stage, &evidence, &gate.ProposalRef, &gate.TaskPacketRef, &gate.WorktreeRef, &producer, &verifier, &gate.VerificationEvidence, &gate.CanaryEvidence, &gate.PromotionRef, &gate.RollbackRef)
if err != nil {
return work.DevelopmentGate{}, fmt.Errorf("load development gate: %w", err)
}
gate.EvidenceRefs = append([]string(nil), evidence...)
for _, run := range producer {
gate.ProducerRunRefs = append(gate.ProducerRunRefs, work.AgentRunID(run))
}
for _, run := range verifier {
gate.VerifierRunRefs = append(gate.VerifierRunRefs, work.AgentRunID(run))
}
return gate, nil
}
func (r *WorkRepository) SaveDevelopmentGate(ctx context.Context, gate work.DevelopmentGate, expectedRevision int64) error {
if gate.ID == "" || expectedRevision < 0 || gate.Revision < 1 || gate.UpdatedAtUnixMillis < 1 {
return ErrDevelopmentGateConflict
}
producer := make([]string, 0, len(gate.ProducerRunRefs))
for _, run := range gate.ProducerRunRefs {
producer = append(producer, string(run))
}
verifier := make([]string, 0, len(gate.VerifierRunRefs))
for _, run := range gate.VerifierRunRefs {
verifier = append(verifier, string(run))
}
if expectedRevision == 0 {
command, err := r.pool.Exec(ctx, `INSERT INTO work.development_gates(gate_id,revision,updated_at_unix_millis,stage,evidence_refs,proposal_ref,task_packet_ref,worktree_ref,producer_run_refs,verifier_run_refs,verification_evidence,canary_evidence,promotion_ref,rollback_ref) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(gate_id) DO NOTHING`, gate.ID, gate.Revision, gate.UpdatedAtUnixMillis, gate.Stage, gate.EvidenceRefs, gate.ProposalRef, gate.TaskPacketRef, gate.WorktreeRef, producer, verifier, gate.VerificationEvidence, gate.CanaryEvidence, gate.PromotionRef, gate.RollbackRef)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrDevelopmentGateConflict
}
return nil
}
command, err := r.pool.Exec(ctx, `UPDATE work.development_gates SET revision=$1,updated_at_unix_millis=$2,stage=$3,evidence_refs=$4,proposal_ref=$5,task_packet_ref=$6,worktree_ref=$7,producer_run_refs=$8,verifier_run_refs=$9,verification_evidence=$10,canary_evidence=$11,promotion_ref=$12,rollback_ref=$13 WHERE gate_id=$14 AND revision=$15`, gate.Revision, gate.UpdatedAtUnixMillis, gate.Stage, gate.EvidenceRefs, gate.ProposalRef, gate.TaskPacketRef, gate.WorktreeRef, producer, verifier, gate.VerificationEvidence, gate.CanaryEvidence, gate.PromotionRef, gate.RollbackRef, gate.ID, expectedRevision)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrDevelopmentGateConflict
}
return nil
}
func (r *WorkRepository) SaveExtensionProposal(ctx context.Context, value work.ExtensionProposal) error {
result, err := r.pool.Exec(ctx, `INSERT INTO work.extension_proposals(proposal_id,requested_by_principal_id,stage,evidence_refs,deduplicated_signal_keys,desired_outcome,smallest_change_layer,affected_owners,affected_contracts,risk_tier,evaluation_plan,alternatives,rollback_plan,assumptions,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(proposal_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.EvidenceRefs, value.DeduplicatedSignalKeys, value.DesiredOutcome, value.SmallestChangeLayer, value.AffectedOwners, value.AffectedContracts, value.RiskTier, value.EvaluationPlan, value.Alternatives, value.RollbackPlan, value.Assumptions, value.CreatedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrDevelopmentCandidateConflict
}
return nil
}
func (r *WorkRepository) SaveDevelopmentScaffold(ctx context.Context, value work.DevelopmentScaffold) error {
result, err := r.pool.Exec(ctx, `INSERT INTO work.development_scaffolds(scaffold_id,requested_by_principal_id,stage,proposal_ref,base_repo_revision,worktree_ref,task_intent,modules,allowed_paths,forbidden_paths,required_tests,artifact_lineage_refs,protected_main_access,production_access,secret_access,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(scaffold_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.ProposalRef, value.BaseRepoRevision, value.WorktreeRef, value.TaskIntent, value.Modules, value.AllowedPaths, value.ForbiddenPaths, value.RequiredTests, value.ArtifactLineageRefs, value.ProtectedMainAccess, value.ProductionAccess, value.SecretAccess, value.CreatedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrDevelopmentCandidateConflict
}
return nil
}
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis)
profileJSON, casesJSON, err := marshalTaskPayload(intent.TaskEvaluationProfile, intent.TaskEvaluationCases)
if err != nil {
return err
}
executionJSON, err := json.Marshal(intent.TaskEvaluationExecution)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis, intent.SpecialistPurpose, intent.TaskEvaluationProfileID, intent.TaskEvaluationBindingID, intent.TaskEvaluationCaseIDs, profileJSON, casesJSON, executionJSON)
if err != nil {
return fmt.Errorf("save dispatch intent: %w", err)
}
var existing work.AgentDispatchIntent
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis)
var profileJSONExisting, casesJSONExisting, executionJSONExisting []byte
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis, &existing.SpecialistPurpose, &existing.TaskEvaluationProfileID, &existing.TaskEvaluationBindingID, &existing.TaskEvaluationCaseIDs, &profileJSONExisting, &casesJSONExisting, &executionJSONExisting)
if err != nil {
return fmt.Errorf("read dispatch intent: %w", err)
}
if existing != intent {
if err := unmarshalTaskPayload(profileJSONExisting, casesJSONExisting, &existing.TaskEvaluationProfile, &existing.TaskEvaluationCases); err != nil {
return err
}
if len(executionJSONExisting) > 0 && string(executionJSONExisting) != "null" {
if err := json.Unmarshal(executionJSONExisting, &existing.TaskEvaluationExecution); err != nil {
return err
}
}
if !sameDispatchIntent(existing, intent) {
return ErrDispatchIntentConflict
}
return tx.Commit(ctx)
}
func sameDispatchIntent(left, right work.AgentDispatchIntent) bool {
return left.DispatchID == right.DispatchID && left.IdempotencyKey == right.IdempotencyKey && left.DelegationID == right.DelegationID && left.RunID == right.RunID && left.ImplementationID == right.ImplementationID && left.ImplementationRevision == right.ImplementationRevision && left.SourceRevision == right.SourceRevision && left.WorktreeID == right.WorktreeID && left.VerificationPlanRef == right.VerificationPlanRef && left.CreatedAtUnixMillis == right.CreatedAtUnixMillis && left.SpecialistPurpose == right.SpecialistPurpose && left.TaskEvaluationProfileID == right.TaskEvaluationProfileID && left.TaskEvaluationBindingID == right.TaskEvaluationBindingID && slices.Equal(left.TaskEvaluationCaseIDs, right.TaskEvaluationCaseIDs) && reflect.DeepEqual(left.TaskEvaluationProfile, right.TaskEvaluationProfile) && slices.Equal(left.TaskEvaluationCases, right.TaskEvaluationCases) && reflect.DeepEqual(left.TaskEvaluationExecution, right.TaskEvaluationExecution)
}
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
var intent work.AgentDispatchIntent
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
var profileJSON, casesJSON, executionJSON []byte
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis, &intent.SpecialistPurpose, &intent.TaskEvaluationProfileID, &intent.TaskEvaluationBindingID, &intent.TaskEvaluationCaseIDs, &profileJSON, &casesJSON, &executionJSON)
if err != nil {
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
}
if err := unmarshalTaskPayload(profileJSON, casesJSON, &intent.TaskEvaluationProfile, &intent.TaskEvaluationCases); err != nil {
return work.AgentDispatchIntent{}, err
}
if len(executionJSON) > 0 && string(executionJSON) != "null" {
if err := json.Unmarshal(executionJSON, &intent.TaskEvaluationExecution); err != nil {
return work.AgentDispatchIntent{}, err
}
}
return intent, nil
}
@@ -91,7 +199,7 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
}
defer tx.Rollback(ctx)
var item work.AgentDispatchQueueItem
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 AND attempts < retry_limit ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return work.AgentDispatchQueueItem{}, nil
@@ -134,17 +242,36 @@ func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, worke
return nil
}
func (r *WorkRepository) RetryDispatch(ctx context.Context, dispatchID, workerID string, nowUnixMillis int64, delay time.Duration) error {
if delay < 0 {
return fmt.Errorf("invalid retry delay")
}
availableAt := nowUnixMillis + delay.Milliseconds()
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, availableAt, nowUnixMillis, dispatchID, workerID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrAgentCallbackRejected
}
return nil
}
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref,orchestration_bound,orchestration_goal_id,orchestration_goal_state,orchestration_search_round,orchestration_max_round,orchestration_cancel_requested,orchestration_verification_ready,orchestration_unresolved) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts, graph.ParentGraphID, graph.ParentGraphRevision, graph.TriggerEvidenceRef, graph.SearchInputBound, graph.SearchInput.GoalID, graph.SearchInput.GoalState, graph.SearchInput.SearchRound, graph.SearchInput.MaxRound, graph.SearchInput.CancelRequested, graph.SearchInput.VerificationReady, graph.SearchInput.Unresolved); err != nil {
return err
}
for _, node := range graph.Nodes {
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 {
profileJSON, casesJSON, err := marshalTaskPayload(node.TaskProfile, node.TaskCases)
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids,task_evaluation_profile,task_evaluation_cases) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts, node.Purpose, node.TaskProfileID, node.BindingID, node.CaseIDs, profileJSON, casesJSON); err != nil {
return err
}
}
@@ -158,17 +285,22 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
var graph work.WorkGraph
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref,orchestration_bound,orchestration_goal_id,orchestration_goal_state,orchestration_search_round,orchestration_max_round,orchestration_cancel_requested,orchestration_verification_ready,orchestration_unresolved FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts, &graph.ParentGraphID, &graph.ParentGraphRevision, &graph.TriggerEvidenceRef, &graph.SearchInputBound, &graph.SearchInput.GoalID, &graph.SearchInput.GoalState, &graph.SearchInput.SearchRound, &graph.SearchInput.MaxRound, &graph.SearchInput.CancelRequested, &graph.SearchInput.VerificationReady, &graph.SearchInput.Unresolved)
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
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)
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids,task_evaluation_profile,task_evaluation_cases 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.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
var profileJSON, casesJSON []byte
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts, &node.Purpose, &node.TaskProfileID, &node.BindingID, &node.CaseIDs, &profileJSON, &casesJSON); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
if err := unmarshalTaskPayload(profileJSON, casesJSON, &node.TaskProfile, &node.TaskCases); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
@@ -199,9 +331,48 @@ func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revi
return graph, nil
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
func (r *WorkRepository) SaveGraphSynthesis(ctx context.Context, conflict work.ConflictSet, candidates []work.ResultCandidate, synthesis work.GraphSynthesisCandidate) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
for _, candidate := range candidates {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_result_candidates(result_candidate_id,graph_id,graph_revision,node_id,author_run_id,artifact_refs,evidence_summary,summary) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(result_candidate_id) DO NOTHING`, candidate.ID, candidate.GraphID, candidate.GraphRevision, candidate.NodeID, candidate.AuthorRunID, candidate.ArtifactRefs, candidate.EvidenceSummary, candidate.Summary); err != nil {
return err
}
}
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_conflicts(conflict_id,graph_id,graph_revision,node_id,candidate_ids,resolved) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(conflict_id) DO NOTHING`, conflict.ID, conflict.GraphID, conflict.GraphRevision, conflict.NodeID, conflict.CandidateIDs, conflict.Resolved); err != nil {
return err
}
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_synthesis_candidates(synthesis_id,graph_id,graph_revision,node_id,candidate_ids,selected_candidate_id,dissent_candidate_ids,verifier_run_id,verifier_identity,evidence_refs,summary,state) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT(synthesis_id) DO NOTHING`, synthesis.ID, synthesis.GraphID, synthesis.GraphRevision, synthesis.NodeID, synthesis.CandidateIDs, synthesis.SelectedCandidateID, synthesis.DissentCandidateIDs, synthesis.VerifierRunID, synthesis.VerifierIdentity, synthesis.EvidenceRefs, synthesis.Summary, synthesis.State); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveGraphResultCandidate(ctx context.Context, candidate work.ResultCandidate) error {
if candidate.ID == "" || candidate.GraphID == "" || candidate.GraphRevision < 1 || candidate.NodeID == "" || candidate.AuthorRunID == "" || candidate.EvidenceSummary == "" || candidate.Summary == "" {
return errors.New("invalid graph result candidate")
}
_, err := r.pool.Exec(ctx, `INSERT INTO work.graph_result_candidates(result_candidate_id,graph_id,graph_revision,node_id,author_run_id,artifact_refs,evidence_summary,summary) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(result_candidate_id) DO NOTHING`, candidate.ID, candidate.GraphID, candidate.GraphRevision, candidate.NodeID, candidate.AuthorRunID, candidate.ArtifactRefs, candidate.EvidenceSummary, candidate.Summary)
return err
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return 0, err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `UPDATE work.agent_leases AS l SET state='EXPIRED' FROM work.agent_dispatch_queue AS q JOIN work.agent_dispatch_intents AS i ON i.dispatch_id=q.dispatch_id WHERE q.state='CLAIMED' AND q.updated_at_unix_millis <= $1 AND l.run_id=i.run_id AND l.state='ACTIVE'`, nowUnixMillis-staleAfterMillis); err != nil {
return 0, err
}
command, err := tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE state='CLAIMED' AND updated_at_unix_millis <= $3`, nowUnixMillis, nowUnixMillis, nowUnixMillis-staleAfterMillis)
if err != nil {
return 0, err
}
if err := tx.Commit(ctx); err != nil {
return 0, err
}
return command.RowsAffected(), nil
@@ -494,3 +665,32 @@ func latestRowErr(err error) error {
}
return err
}
func marshalTaskPayload(profile *capability.TaskEvaluationProfile, cases []capability.TaskEvaluationCase) ([]byte, []byte, error) {
var profileJSON []byte
var err error
if profile != nil {
profileJSON, err = json.Marshal(profile)
if err != nil {
return nil, nil, err
}
}
casesJSON, err := json.Marshal(cases)
return profileJSON, casesJSON, err
}
func unmarshalTaskPayload(profileJSON, casesJSON []byte, profile **capability.TaskEvaluationProfile, cases *[]capability.TaskEvaluationCase) error {
if len(profileJSON) > 0 && string(profileJSON) != "null" {
var value capability.TaskEvaluationProfile
if err := json.Unmarshal(profileJSON, &value); err != nil {
return err
}
*profile = &value
}
if len(casesJSON) > 0 && string(casesJSON) != "null" {
if err := json.Unmarshal(casesJSON, cases); err != nil {
return err
}
}
return nil
}
+43 -4
View File
@@ -6,12 +6,16 @@ import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"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"
)
type memoryStore struct{ writes int }
@@ -20,7 +24,8 @@ func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchInte
s.writes++
return nil
}
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *memoryStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error { return nil }
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.writes++
return nil
@@ -30,14 +35,24 @@ func main() {
enabled := flag.Bool("enable-canary", false, "explicitly enable the manual canary")
implementation := flag.String("implementation", "fake-worker", "allowlisted deterministic implementation")
postgresMode := flag.Bool("postgres", false, "use the Work PostgreSQL repository instead of in-memory storage")
runID := flag.String("run-id", "", "optional unique canary run suffix; generated when omitted")
flag.Parse()
now := time.UnixMilli(1_700_000_000_000)
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-1", IdempotencyKey: "manual-idempotency-1", DelegationID: "manual-delegation-1", RunID: "manual-run-1", ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-1", WorktreeID: "manual-worktree-1", VerificationPlanRef: "manual-verification-1", CreatedAtUnixMillis: now.UnixMilli()}
suffix := *runID
if suffix == "" {
suffix = fmt.Sprintf("%d", time.Now().UnixNano())
}
now := time.Now().UTC()
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-" + suffix, IdempotencyKey: "manual-idempotency-" + suffix, DelegationID: work.DelegationID("manual-delegation-" + suffix), RunID: work.AgentRunID("manual-run-" + suffix), ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-" + suffix, WorktreeID: "manual-worktree-" + suffix, VerificationPlanRef: "manual-verification-" + suffix, CreatedAtUnixMillis: now.UnixMilli()}
var store ports.ExecutionStore = &memoryStore{}
var closeStore func()
if *postgresMode {
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
password, err := resolveDatabasePassword()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
repository, err := postgres.OpenWork(context.Background(), 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 {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
@@ -67,3 +82,27 @@ func main() {
os.Exit(1)
}
}
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"
}
cleanRoot := filepath.Clean(root)
cleanPath := filepath.Clean(path)
if cleanPath != cleanRoot && !strings.HasPrefix(cleanPath, cleanRoot+string(os.PathSeparator)) {
return "", fmt.Errorf("database password file escapes protected secret root")
}
resolver, err := platformsecrets.NewFileResolver(cleanRoot)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerWorkWorker, Path: cleanPath})
}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"path/filepath"
"testing"
)
func TestResolveDatabasePasswordRejectsDirectEnvironmentSecret(t *testing.T) {
t.Setenv("WORK_POSTGRES_PASSWORD", "literal-secret")
if _, err := resolveDatabasePassword(); err == nil {
t.Fatal("direct database password environment variable was accepted")
}
}
func TestResolveDatabasePasswordRejectsPathOutsideProtectedRoot(t *testing.T) {
root := t.TempDir()
t.Setenv("SECRET_BACKEND_ROOT", root)
t.Setenv("WORK_POSTGRES_PASSWORD_FILE", filepath.Join(root, "..", "outside"))
t.Setenv("WORK_POSTGRES_CREDENTIAL_REF", "credential://domovoy/work/db-password/v1")
if _, err := resolveDatabasePassword(); err == nil {
t.Fatal("database password path outside protected root was accepted")
}
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"context"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
// localExecutor provides the bounded local execution capability. It never
// claims that an unsupported or external action happened. The work owner
// records the WAITING callback and can retry after a suitable executor is
// configured or a human chooses the next step.
type localExecutor struct {
now func() time.Time
}
func newLocalExecutor() localExecutor {
return localExecutor{now: time.Now}
}
func (e localExecutor) Execute(_ context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
now := time.Now()
if e.now != nil {
now = e.now()
}
return work.AgentExecutionResult{
State: work.AgentExecutionWaiting,
Summary: "Локальный исполнитель не поддерживает эту операцию; durable работа сохранена и ожидает подходящего исполнителя.",
ObservedAtUnixMillis: now.UnixMilli(),
}, nil
}
+24
View File
@@ -0,0 +1,24 @@
package main
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
func TestLocalExecutorNeverClaimsUnsupportedExecution(t *testing.T) {
when := time.UnixMilli(1_700_000_000_000)
executor := localExecutor{now: func() time.Time { return when }}
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1"}})
if err != nil {
t.Fatal(err)
}
if result.State != work.AgentExecutionWaiting || result.ObservedAtUnixMillis != when.UnixMilli() {
t.Fatalf("unexpected fallback result: %+v", result)
}
if result.OutcomeRef != "" || result.Summary == "" {
t.Fatalf("local executor must not claim an outcome: %+v", result)
}
}
+46 -4
View File
@@ -7,6 +7,8 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/agentbridge"
@@ -20,8 +22,9 @@ import (
func main() {
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
statusAddr := flag.String("status-addr", ":8090", "internal status HTTP address")
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()
@@ -38,21 +41,32 @@ func main() {
}
defer repository.Close()
var executor ports.AgentExecutor
if bridgeURL := os.Getenv("AGENT_EXECUTION_BRIDGE_URL"); bridgeURL != "" {
localFallback := false
if bridgeURL, bridgeConfigured := loadProtectedBridgeURL(); bridgeConfigured {
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; using legacy dry-run dispatch")
// The local executor keeps supported task-qualification work alive, but
// it is not an external Agent Execution Bridge and must never be reported
// as one. Unsupported work remains durably WAITING.
localFallback = true
executor = newTaskEvaluationExecutor(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), envOr("TASK_VERIFIER_URL", "http://task-verifier:8091"), &http.Client{Timeout: *leaseTTL})
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; task evaluations use the local durable evaluator, other work remains in fallback")
}
go func() {
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil && !localFallback, localFallback)); serveErr != nil {
fmt.Fprintln(os.Stderr, "agent worker status server:", serveErr)
}
}()
ctx := context.Background()
for {
var result application.WorkerCycleResult
var cycleErr error
if executor == nil {
result, cycleErr = application.ExecuteWorkerCycle(ctx, repository, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
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)
}
@@ -75,6 +89,34 @@ func main() {
}
}
func loadProtectedBridgeURL() (string, bool) {
return loadProtectedBridgeURLFrom("/run/secrets", "/run/secrets/agent_execution_bridge_url")
}
func loadProtectedBridgeURLFrom(root, path string) (string, bool) {
clean := filepath.Clean(path)
cleanRoot := filepath.Clean(root)
if clean != cleanRoot && !strings.HasPrefix(clean, cleanRoot+string(os.PathSeparator)) {
return "", false
}
payload, err := os.ReadFile(clean)
if err != nil {
return "", false
}
value := strings.TrimSpace(string(payload))
if value == "" || strings.ContainsAny(value, "\r\n") {
return "", false
}
return value, true
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
+51
View File
@@ -0,0 +1,51 @@
package main
import (
"encoding/json"
"net/http"
)
type workerStatusResponse struct {
Runtime string `json:"runtime"`
ExecutorConfigured bool `json:"executor_configured"`
Mode string `json:"mode"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
LocalFallback bool `json:"local_fallback"`
LocalExecutor bool `json:"local_executor"`
Capabilities []string `json:"capabilities,omitempty"`
}
func workerStatusHandler(executorConfigured bool, localFallback ...bool) http.Handler {
local := len(localFallback) > 0 && localFallback[0]
status := workerStatusResponse{
Runtime: "agent-worker",
ExecutorConfigured: executorConfigured,
Mode: "recovery-only",
Ready: false,
Reason: "protected agent execution bridge URL is not configured",
LocalFallback: local,
LocalExecutor: local,
}
if local {
status.Mode = "local-executor"
status.Reason = "bounded local task-evaluation executor is active; unsupported work remains durably waiting"
status.Capabilities = []string{"task-evaluation"}
}
if executorConfigured {
status.Mode = "execution"
status.Ready = true
status.Reason = "typed agent execution bridge configured"
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || (r.URL.Path != "/healthz" && r.URL.Path != "/readyz" && r.URL.Path != "/status") {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/readyz" && !status.Ready {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(status)
})
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestLoadProtectedBridgeURL(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "agent_execution_bridge_url")
if err := os.WriteFile(path, []byte("http://bridge.local\n"), 0o600); err != nil {
t.Fatal(err)
}
if value, configured := loadProtectedBridgeURLFrom(root, path); !configured || value != "http://bridge.local" {
t.Fatalf("protected bridge was not loaded: %q %v", value, configured)
}
if value, configured := loadProtectedBridgeURLFrom(root, filepath.Join(root, "missing")); configured || value != "" {
t.Fatalf("missing bridge was accepted: %q %v", value, configured)
}
if value, configured := loadProtectedBridgeURLFrom(root, "/tmp/not-protected"); configured || value != "" {
t.Fatalf("path outside protected root was accepted: %q %v", value, configured)
}
}
func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || value.Ready || value.Mode != "recovery-only" || value.ExecutorConfigured {
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
}
}
func TestWorkerStatusReportsLocalExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-executor" || !value.LocalFallback || !value.LocalExecutor {
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
}
if len(value.Capabilities) != 1 || value.Capabilities[0] != "task-evaluation" {
t.Fatalf("unexpected local capabilities: %+v", value.Capabilities)
}
}
func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if value.ExecutorConfigured || value.Ready || !value.LocalFallback || !value.LocalExecutor || value.Mode != "local-executor" {
t.Fatalf("local fallback was presented as external execution: %+v", value)
}
}
func TestWorkerReadyzRequiresExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("readyz status=%d", response.Code)
}
response = httptest.NewRecorder()
workerStatusHandler(true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusOK {
t.Fatalf("configured readyz status=%d", response.Code)
}
}
@@ -0,0 +1,109 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
type taskEvaluationExecutor struct {
cognitiveURL string
callbackURL string
verifierURL string
client *http.Client
fallback localExecutor
}
func newTaskEvaluationExecutor(cognitiveURL, callbackURL, verifierURL string, client *http.Client) taskEvaluationExecutor {
if client == nil {
client = http.DefaultClient
}
return taskEvaluationExecutor{cognitiveURL: strings.TrimRight(cognitiveURL, "/"), callbackURL: strings.TrimRight(callbackURL, "/"), verifierURL: strings.TrimRight(verifierURL, "/"), client: client, fallback: newLocalExecutor()}
}
func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
intent := request.Intent
if intent.TaskEvaluationExecution == nil || intent.TaskEvaluationProfile == nil || len(intent.TaskEvaluationCases) == 0 {
return e.fallback.Execute(ctx, request)
}
execution := intent.TaskEvaluationExecution
if execution.GraphID == "" || execution.NodeID == "" || execution.BindingRevision < 1 || execution.ActorID == "" || execution.PrincipalID == "" || execution.ScopeID == "" {
return e.fallback.Execute(ctx, request)
}
if e.cognitiveURL == "" || e.callbackURL == "" || e.verifierURL == "" {
return e.fallback.Execute(ctx, request)
}
payload := capability.TaskEvaluationExecutionRequest{Profile: *intent.TaskEvaluationProfile, CapabilityID: intent.TaskEvaluationProfile.CapabilityID, BindingID: intent.TaskEvaluationBindingID, BindingRevision: execution.BindingRevision, ActorID: execution.ActorID, PrincipalID: execution.PrincipalID, ScopeID: execution.ScopeID, Cases: intent.TaskEvaluationCases, MaxOutputBytes: 4096}
response, err := postJSON[capability.TaskEvaluationExecutionResponse](ctx, e.client, e.cognitiveURL+"/v1/task-evaluations/execute", payload)
if err != nil {
return work.AgentExecutionResult{}, err
}
if len(response.Responses) == 0 || response.ObservedAtUnixMillis < 1 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation returned no observations")
}
if len(intent.TaskEvaluationCases) == 0 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation cases are missing")
}
caseByID := make(map[string]capability.TaskEvaluationCase, len(intent.TaskEvaluationCases))
for _, item := range intent.TaskEvaluationCases {
caseByID[item.CaseID] = item
}
observations := make([]capability.TaskCaseObservation, 0, len(response.Responses))
for _, item := range response.Responses {
caseValue, ok := caseByID[item.CaseID]
if !ok || caseValue.Verification.Mode == "" {
return e.fallback.Execute(ctx, request)
}
verified, verifyErr := postJSON[capability.TaskCaseVerificationResponse](ctx, e.client, e.verifierURL+"/v1/task-evaluations/verify", capability.TaskCaseVerificationRequest{CaseID: item.CaseID, ResponseText: item.ResponseText, LatencyMicros: item.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef, Verification: caseValue.Verification})
if verifyErr != nil {
return e.fallback.Execute(ctx, request)
}
observations = append(observations, capability.TaskCaseObservation{CaseID: verified.CaseID, Result: verified.Result, QualityScoreMillis: verified.QualityScoreMillis, LatencyMicros: verified.LatencyMicros, CriticalFailure: verified.CriticalFailure, EvidenceArtifactRef: verified.EvidenceArtifactRef, VerificationEvidenceRef: verified.VerificationEvidenceRef, VerifierIdentity: verified.VerifierIdentity})
}
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
if err != nil {
return work.AgentExecutionResult{}, err
}
callback := work.TaskEvaluationEvidenceCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, NodeID: execution.NodeID, Evaluation: evaluation, Succeeded: true}
if _, err := postJSON[json.RawMessage](ctx, e.client, e.callbackURL+"/v1/work-graphs/task-evaluations/callback", callback); err != nil {
return work.AgentExecutionResult{}, err
}
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "task evaluation executed, aggregated and accepted by Work Graph callback", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
func postJSON[T any](ctx context.Context, client *http.Client, url string, payload any) (T, error) {
var result T
body, err := json.Marshal(payload)
if err != nil {
return result, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return result, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return result, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return result, fmt.Errorf("POST %s: status %s", url, resp.Status)
}
if resp.StatusCode == http.StatusNoContent {
return result, nil
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,43 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
)
func TestTaskEvaluationExecutorInvokesAggregatesAndCallbacks(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-1"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
verification := capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1/task-evaluations/execute" {
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Responses: []capability.TaskCaseResponse{{CaseID: "case-1", ResponseText: "answer", LatencyMicros: 2, EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
return
}
if r.URL.Path == "/v1/task-evaluations/verify" {
_ = json.NewEncoder(w).Encode(capability.TaskCaseVerificationResponse{CaseID: "case-1", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 2, EvidenceArtifactRef: "artifact-1", VerificationEvidenceRef: "verify-1", VerifierIdentity: "verifier-1"})
return
}
if r.URL.Path == "/v1/work-graphs/task-evaluations/callback" {
var callback work.TaskEvaluationEvidenceCallback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil || callback.GraphID != "graph-1" || callback.NodeID != "node-1" || callback.Evaluation.Result != capability.EvaluationSupported {
http.Error(w, "invalid callback", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer server.Close()
executor := newTaskEvaluationExecutor(server.URL, server.URL, server.URL, server.Client())
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1", Verification: verification}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
t.Fatalf("result=%+v err=%v", result, err)
}
}
+10 -9
View File
@@ -98,8 +98,9 @@ func main() {
manifestPath := flag.String("manifest", "", "benchmark manifest JSON")
outputPath := flag.String("output", "", "output artifact JSON")
timeout := flag.Duration("case-timeout", 30*time.Second, "deadline for each case")
maxOutputBytes := flag.Int64("max-output-bytes", 4096, "maximum advisory output bytes per case")
flag.Parse()
if *manifestPath == "" || *outputPath == "" || *timeout <= 0 {
if *manifestPath == "" || *outputPath == "" || *timeout <= 0 || *maxOutputBytes < 1 {
fatal(errors.New("manifest, output and positive case-timeout are required"))
}
input, err := os.ReadFile(*manifestPath)
@@ -110,7 +111,7 @@ func main() {
if err := json.Unmarshal(input, &plan); err != nil {
fatal(err)
}
artifact, err := run(context.Background(), plan, *timeout, time.Now().UTC())
artifact, err := run(context.Background(), plan, *timeout, *maxOutputBytes, time.Now().UTC())
if err != nil {
fatal(err)
}
@@ -125,7 +126,7 @@ func main() {
fmt.Printf("artifact=%s models=%d output=%s\n", artifact.ArtifactID, len(artifact.Results), *outputPath)
}
func run(ctx context.Context, plan manifest, timeout time.Duration, observed time.Time) (benchmarkArtifact, error) {
func run(ctx context.Context, plan manifest, timeout time.Duration, maxOutputBytes int64, observed time.Time) (benchmarkArtifact, error) {
if plan.ArtifactID == "" || len(plan.Providers) == 0 {
return benchmarkArtifact{}, errors.New("invalid benchmark manifest")
}
@@ -134,7 +135,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
artifact.Corpus = append(artifact.Corpus, corpusRecord{ID: item.id, Prompt: item.prompt, PromptSHA256: digest(item.prompt), Critical: item.critical})
}
for _, provider := range plan.Providers {
adapter, err := newProvider(provider)
adapter, err := newProvider(provider, timeout)
if err != nil {
return benchmarkArtifact{}, err
}
@@ -142,7 +143,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
return provider.Candidates[i].ProviderModelID < provider.Candidates[j].ProviderModelID
})
for _, candidate := range provider.Candidates {
result, err := runModel(ctx, adapter, provider.ProviderInstanceID, candidate, timeout)
result, err := runModel(ctx, adapter, provider.ProviderInstanceID, candidate, timeout, maxOutputBytes)
if err != nil {
return benchmarkArtifact{}, err
}
@@ -158,10 +159,10 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
return artifact, nil
}
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
func newProvider(candidate providerCandidate, timeout time.Duration) (models.LocalProvider, error) {
switch candidate.ProviderKind {
case "openai-compatible-chat-v1":
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
return models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(candidate.Endpoint, "", nil, timeout)
case "ollama-v1":
return models.NewOllamaAdapter(candidate.Endpoint, nil)
default:
@@ -169,7 +170,7 @@ func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
}
}
func runModel(ctx context.Context, adapter models.LocalProvider, providerID string, candidate modelCandidate, timeout time.Duration) (modelResult, error) {
func runModel(ctx context.Context, adapter models.LocalProvider, providerID string, candidate modelCandidate, timeout time.Duration, maxOutputBytes int64) (modelResult, error) {
if providerID == "" || candidate.ProviderModelID == "" || candidate.ModelDescriptorID == "" || candidate.ModelRevision < 1 || candidate.BindingID == "" || candidate.BindingRevision < 1 {
return modelResult{}, errors.New("invalid benchmark candidate")
}
@@ -178,7 +179,7 @@ func runModel(ctx context.Context, adapter models.LocalProvider, providerID stri
for index, item := range corpus {
caseCtx, cancel := context.WithTimeout(ctx, timeout)
started := time.Now()
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("bench-%s-%d", digest(candidate.BindingID)[:12], index)), ActorID: "benchmark-actor", PrincipalID: "domovoy-creator-principal", ScopeID: "benchmark-public-synthetic", Message: item.prompt, Locale: "ru-RU", MaxOutputBytes: 4096, ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID), ProviderBindingRevision: candidate.BindingRevision, ProviderInstanceID: capability.ProviderInstanceID(providerID), ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID), ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID)})
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("bench-%s-%d", digest(candidate.BindingID)[:12], index)), ActorID: "benchmark-actor", PrincipalID: "domovoy-creator-principal", ScopeID: "benchmark-public-synthetic", Message: item.prompt, Locale: "ru-RU", MaxOutputBytes: int(maxOutputBytes), ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID), ProviderBindingRevision: candidate.BindingRevision, ProviderInstanceID: capability.ProviderInstanceID(providerID), ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID), ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID)})
cancel()
caseResult := caseResult{CaseID: item.id, LatencyMillis: time.Since(started).Milliseconds()}
if err != nil {
+44
View File
@@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"net/http"
"sort"
"time"
"git.cin.su/m/Assistant/contracts/capability"
@@ -17,6 +18,10 @@ type discoveryService struct {
}
func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v1/provider-discovery/status" {
s.statusHandler(w, r)
return
}
registration, ok := s.providers[r.PathValue("provider")]
if !ok || registration.Discovery == nil {
http.Error(w, "provider discovery not found", http.StatusNotFound)
@@ -35,3 +40,42 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
type discoveryStatus struct {
Provider string `json:"provider"`
ProviderKind capability.ProviderKind `json:"provider_kind"`
Health capability.HealthState `json:"health"`
FailureClass capability.CognitiveFailureType `json:"failure_class,omitempty"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Models []discoveredModelStatus `json:"models"`
}
type discoveredModelStatus struct {
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
Fingerprint capability.ModelFingerprint `json:"fingerprint,omitempty"`
Capabilities []capability.ModelCapability `json:"capabilities,omitempty"`
}
func (s discoveryService) statusHandler(w http.ResponseWriter, r *http.Request) {
providers := make([]string, 0, len(s.providers))
for name := range s.providers {
providers = append(providers, name)
}
sort.Strings(providers)
result := make([]discoveryStatus, 0, len(providers))
for _, name := range providers {
registration := s.providers[name]
status := discoveryStatus{Provider: name, ProviderKind: registration.Kind, Health: capability.HealthUnknown, ObservedAtUnixMillis: s.now().UnixMilli(), Models: []discoveredModelStatus{}}
if registration.Discovery != nil {
models, probe := registration.Discovery.Discover(r.Context())
status.Health = probe.Health
status.FailureClass = capability.CognitiveFailureType(probe.Failure)
for _, model := range models {
status.Models = append(status.Models, discoveredModelStatus{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, Capabilities: model.Capabilities})
}
}
result = append(result, status)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
)
type discoveryTestProvider struct{}
func (discoveryTestProvider) Probe(context.Context) models.ProviderProbe {
return models.ProviderProbe{Health: capability.HealthAvailable}
}
func (discoveryTestProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
return []models.DiscoveredModel{{ProviderModelID: "local-model", Fingerprint: "fingerprint-1", Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}}}, models.ProviderProbe{Health: capability.HealthAvailable}
}
func (discoveryTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
return cognitive.AdvisoryResponse{}, nil
}
func TestDiscoveryStatusIsReadOnlyAndDoesNotExposeCredentialMetadata(t *testing.T) {
service := discoveryService{
providers: map[string]providerRegistration{"cpu": {ProviderInstanceID: "provider-1", Kind: "openai-compatible-chat-v1", CredentialRef: "credential://must-not-be-returned", Discovery: discoveryTestProvider{}}},
owner: identity.PrincipalID("owner-1"),
now: func() time.Time { return time.UnixMilli(1234) },
}
response := httptest.NewRecorder()
service.handler(response, httptest.NewRequest(http.MethodGet, "/v1/provider-discovery/status", nil))
if response.Code != http.StatusOK {
t.Fatalf("status=%d", response.Code)
}
body := response.Body.String()
var result []discoveryStatus
if err := json.NewDecoder(strings.NewReader(body)).Decode(&result); err != nil {
t.Fatal(err)
}
if len(result) != 1 || result[0].Provider != "cpu" || result[0].Health != capability.HealthAvailable || len(result[0].Models) != 1 || result[0].ObservedAtUnixMillis != 1234 {
t.Fatalf("unexpected discovery status: %+v", result)
}
if strings.Contains(body, "must-not-be-returned") || strings.Contains(body, "credential://") {
t.Fatalf("credential metadata leaked in discovery status: %s", body)
}
}
+10 -1
View File
@@ -17,6 +17,8 @@ type fleetEndpoint struct {
ModelID string
Tier capability.FleetTier
URL string
Headers map[string]string
Client *http.Client
}
type fleetStatusResponse struct {
@@ -37,7 +39,14 @@ func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier,
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
if err == nil {
response, requestErr := client.Do(request)
for name, value := range endpoint.Headers {
request.Header.Set(name, value)
}
probeClient := endpoint.Client
if probeClient == nil {
probeClient = client
}
response, requestErr := probeClient.Do(request)
if requestErr == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
+21 -2
View File
@@ -9,7 +9,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
)
func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T) {
func TestFleetReadinessSelectsBestAvailablePrimaryTierBeforeLifeboat(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{
@@ -21,7 +21,26 @@ func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T)
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"`) {
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"FAST"`) || !strings.Contains(recorder.Body.String(), `"tier":"MEDIUM"`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer fleet-key" {
t.Fatalf("authorization=%q", r.Header.Get("Authorization"))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierFast, []fleetEndpoint{{
ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL,
Headers: map[string]string{"Authorization": "Bearer fleet-key"},
}})
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"ready":true`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+34 -1
View File
@@ -26,6 +26,10 @@ type bindingResolver interface {
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
}
type candidateBindingResolver interface {
ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error)
}
type admissionClient interface {
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
Release(context.Context, capability.AdmissionLease) error
@@ -294,12 +298,21 @@ func fallbackEligible(reason cognitive.FinishReason) bool {
}
func (g cognitiveGateway) provider(selected capability.ResolvedCognitiveBinding) advisoryProvider {
var byStableID advisoryProvider
for _, registration := range g.providers[selected.Provider.Kind] {
if registration.EndpointRef == selected.Provider.EndpointRef && registration.CredentialRef == selected.Provider.CredentialRef {
return registration.Provider
}
// Capability revisions can legitimately retain an older credential
// reference while the protected runtime has rotated or reloaded the
// current credential. ProviderInstanceID is the stable owner identity;
// use it after exact metadata matching so stale non-secret metadata does
// not force an unnecessary builtin fallback.
if byStableID == nil && registration.ProviderInstanceID == selected.Provider.ID && registration.Kind == selected.Provider.Kind {
byStableID = registration.Provider
}
}
return nil
return byStableID
}
func writeGatewayResponse(w http.ResponseWriter, response cognitive.GatewayAdvisoryResponse) {
@@ -341,6 +354,26 @@ func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.Cogn
return result, err
}
func (r *httpBindingResolver) ResolveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
var result capability.ResolvedCognitiveBinding
target := *r.base
target.Path = "/v1/cognitive-bindings/" + url.PathEscape(string(bindingID)) + "/revisions/" + fmt.Sprint(revision) + "/snapshot"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
if err != nil {
return result, err
}
response, err := r.client.Do(request)
if err != nil {
return result, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return result, fmt.Errorf("candidate status %d", response.StatusCode)
}
err = json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&result)
return result, err
}
func (r *httpBindingResolver) Observe(ctx context.Context, value capability.CognitiveUsageObservation) error {
return r.post(ctx, "/v1/cognitive-usage-observations", value, nil)
}
@@ -88,6 +88,19 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
}
}
func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) {
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{
"provider-kind-1": {{ProviderInstanceID: "provider-1", Kind: "provider-kind-1", EndpointRef: "current-endpoint", CredentialRef: "current-credential", Provider: provider}},
}}
selected := gatewaySelection()
selected.Provider.EndpointRef = "old-endpoint"
selected.Provider.CredentialRef = ""
if got := gateway.provider(selected); got != provider {
t.Fatalf("stable provider identity was not used for stale metadata")
}
}
func TestCanaryFlagIsForwardedToCapabilityResolver(t *testing.T) {
registry := &registryStub{err: errors.New("stop after resolve")}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{}, now: time.Now, allowCanary: true}
+177 -21
View File
@@ -10,11 +10,14 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
@@ -43,13 +46,45 @@ func main() {
discoveries := map[string]providerRegistration{}
var builtinLocal *providerRegistration
externalRecovery := []providerRegistration{}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), nil)
cpuAPIKey, err := loadProviderAPIKey("LOCAL_CPU_PROVIDER_API_KEY", "LOCAL_CPU_PROVIDER_API_KEY_FILE", "LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
if err != nil {
log.Fatal(err)
}
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapterWithAPIKey(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), cpuAPIKey, nil)
if err != nil {
log.Fatal(err)
}
cpuAuthMode := capability.AuthNone
cpuCredentialRef := capability.CredentialRef("")
if cpuAPIKey != "" {
cpuAuthMode = capability.AuthKey
cpuCredentialRef = capability.CredentialRef(envOr("LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1"))
}
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", CredentialRef: cpuCredentialRef, AuthMode: cpuAuthMode, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
providers[cpu.Kind] = append(providers[cpu.Kind], cpu)
discoveries["cpu"] = cpu
mediumURL := strings.TrimSpace(os.Getenv("LOCAL_MEDIUM_PROVIDER_URL"))
mediumAPIKey := ""
if mediumURL != "" {
mediumAPIKey, err = loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
if err != nil {
log.Fatal(err)
}
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
if adapterErr != nil {
log.Fatal(adapterErr)
}
mediumAuthMode := capability.AuthNone
mediumCredentialRef := capability.CredentialRef("")
if mediumAPIKey != "" {
mediumAuthMode = capability.AuthKey
mediumCredentialRef = capability.CredentialRef(envOr("LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1"))
}
medium := providerRegistration{ProviderInstanceID: "local-medium-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-medium-primary-v1", CredentialRef: mediumCredentialRef, AuthMode: mediumAuthMode, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: mediumAdapter, Discovery: mediumAdapter}
providers[medium.Kind] = append(providers[medium.Kind], medium)
discoveries["medium"] = medium
}
if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil {
log.Fatal(lifeboatErr)
} else if enabled {
@@ -58,14 +93,20 @@ func main() {
discoveries["lifeboat"] = lifeboat
builtinLocal = &lifeboat
}
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
if err != nil {
log.Fatal(err)
gpuURL, gpuKey, gpuConfigured, gpuErr := loadProtectedGPUConfiguration()
if gpuErr != nil {
log.Printf("optional GPU provider disabled: %v", gpuErr)
} else if gpuConfigured {
gpuAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKey(gpuURL, gpuKey, nil)
if adapterErr != nil {
log.Printf("optional GPU provider disabled: %v", adapterErr)
} else {
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "openai-compatible-chat-v1", EndpointRef: "local-gpu-opportunistic-v1", CredentialRef: "credential://domovoy/gpu/provider-api-key/v1", AuthMode: capability.AuthKey, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
discoveries["gpu"] = gpu
externalRecovery = append(externalRecovery, gpu)
}
}
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
discoveries["gpu"] = gpu
externalRecovery = append(externalRecovery, gpu)
if openAIProvider != nil {
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})
@@ -73,27 +114,63 @@ func main() {
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}
cpuReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessClient := &http.Client{Timeout: 10 * 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.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
health.HTTPProbe(readinessClient, lifeboatURL),
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"},
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierFast)))
cpuFleetHeaders := map[string]string{}
if cpuAPIKey != "" {
cpuFleetHeaders["Authorization"] = "Bearer " + cpuAPIKey
}
cpuEndpoint := strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"
cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(cpuReadinessClient, cpuEndpoint, cpuFleetHeaders)
cpuHealthProbe.Timeout = 10 * time.Second
mediumEndpoint := ""
mediumHeaders := map[string]string{}
mediumHealthProbe := health.Probe{}
if mediumURL != "" {
mediumEndpoint = strings.TrimRight(mediumURL, "/") + "/models"
if mediumAPIKey != "" {
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
}
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
mediumHealthProbe.Timeout = 10 * time.Second
}
readinessProbes := []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
cpuHealthProbe,
}
if mediumURL != "" {
readinessProbes = append(readinessProbes, mediumHealthProbe)
}
readinessProbes = append(readinessProbes, health.HTTPProbe(readinessClient, lifeboatURL))
if gpuConfigured {
gpuHeaders := map[string]string{"Authorization": "Bearer " + gpuKey}
gpuProbe := health.OptionalHTTPProbeWithHeaders(readinessClient, strings.TrimRight(gpuURL, "/")+"/models", gpuHeaders)
gpuProbe.Timeout = 10 * time.Second
readinessProbes = append(readinessProbes, gpuProbe)
}
fleetEndpoints := []fleetEndpoint{
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}},
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient},
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
}))
}
if gpuConfigured {
fleetEndpoints = append(fleetEndpoints, fleetEndpoint{ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(gpuURL, "/") + "/models", Headers: map[string]string{"Authorization": "Bearer " + gpuKey}})
}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: readinessProbes})
mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, fleetEndpoints))
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
refreshProviderDiscoveries(resolver, discovery)
log.Printf("cognitive-worker listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
@@ -101,6 +178,19 @@ func main() {
}
}
func durationEnv(name string, fallback time.Duration) time.Duration {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err != nil || parsed <= 0 {
log.Printf("invalid %s=%q; using %s", name, value, fallback)
return fallback
}
return parsed
}
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
for name := range service.providers {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
@@ -145,6 +235,72 @@ func refreshProviderDiscoveries(resolver bindingResolver, service discoveryServi
}
}
func loadProviderAPIKey(directEnvName, fileEnvName, refEnvName, defaultRef string) (string, error) {
if _, set := os.LookupEnv(directEnvName); set {
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s", directEnvName, fileEnvName)
}
fileName := strings.TrimSpace(os.Getenv(fileEnvName))
if fileName == "" {
return "", nil
}
ref := strings.TrimSpace(os.Getenv(refEnvName))
if ref == "" {
ref = defaultRef
}
if ref == "" {
return "", fmt.Errorf("%s is required when %s is configured", refEnvName, fileEnvName)
}
resolver, err := platformsecrets.NewFileResolver(envOr("SECRET_BACKEND_ROOT", "/run/secrets"))
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeProviderAPIKey, Consumer: secretcontracts.RuntimeConsumerCognitive, Path: fileName})
}
func loadLocalCPUProviderAPIKey() (string, error) {
return loadProviderAPIKey("LOCAL_CPU_PROVIDER_API_KEY", "LOCAL_CPU_PROVIDER_API_KEY_FILE", "LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
}
const (
gpuProviderURLFile = "/run/secrets/llm_gpu_gateway_url"
gpuProviderKeyFile = "/run/secrets/llm_gpu_gateway_api_key"
)
func loadProtectedGPUConfiguration() (string, string, bool, error) {
urlValue, err := readProtectedRuntimeValue(gpuProviderURLFile)
if errors.Is(err, os.ErrNotExist) {
return "", "", false, nil
}
if err != nil {
return "", "", false, err
}
key, err := readProtectedRuntimeValue(gpuProviderKeyFile)
if errors.Is(err, os.ErrNotExist) {
return "", "", false, fmt.Errorf("GPU provider key is not configured")
}
if err != nil {
return "", "", false, err
}
return urlValue, key, true, nil
}
func readProtectedRuntimeValue(path string) (string, error) {
const root = "/run/secrets"
clean := filepath.Clean(path)
if clean != root && !strings.HasPrefix(clean, root+string(os.PathSeparator)) {
return "", fmt.Errorf("protected runtime path outside secret root")
}
payload, err := os.ReadFile(clean)
if err != nil {
return "", err
}
value := strings.TrimSpace(string(payload))
if value == "" || strings.ContainsAny(value, "\r\n") {
return "", fmt.Errorf("protected runtime value is empty or multiline")
}
return value, nil
}
func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
request, ok := decodeAdvisoryRequest(w, r)
if !ok {
+61 -1
View File
@@ -1,6 +1,10 @@
package main
import "testing"
import (
"os"
"path/filepath"
"testing"
)
func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
if _, enabled, err := newLifeboatProvider(""); err != nil || enabled {
@@ -14,3 +18,59 @@ func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
t.Fatalf("unexpected lifeboat registration: %+v", provider)
}
}
func TestLoadLocalCPUProviderAPIKeyUsesProtectedFileReference(t *testing.T) {
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY", "")
_ = os.Unsetenv("LOCAL_CPU_PROVIDER_API_KEY")
root := t.TempDir()
path := filepath.Join(root, "local_cpu_provider_api_key")
if err := os.WriteFile(path, []byte("resolved-key\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY_FILE", path)
t.Setenv("LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
t.Setenv("SECRET_BACKEND_ROOT", root)
key, err := loadLocalCPUProviderAPIKey()
if err != nil || key != "resolved-key" {
t.Fatalf("unexpected key resolution result: err=%v", err)
}
}
func TestLoadLocalCPUProviderAPIKeyRejectsDirectEnvironmentSecret(t *testing.T) {
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY", "must-not-be-used")
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY_FILE", "")
if _, err := loadLocalCPUProviderAPIKey(); err == nil {
t.Fatal("expected direct environment secret to be rejected")
}
}
func TestLoadProviderAPIKeySupportsMediumCredentialRef(t *testing.T) {
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY", "")
_ = os.Unsetenv("LOCAL_MEDIUM_PROVIDER_API_KEY")
root := t.TempDir()
path := filepath.Join(root, "medium_provider_api_key")
if err := os.WriteFile(path, []byte("medium-key\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", path)
t.Setenv("LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
t.Setenv("SECRET_BACKEND_ROOT", root)
key, err := loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
if err != nil || key != "medium-key" {
t.Fatalf("unexpected medium key resolution result: err=%v", err)
}
}
func TestLoadProviderAPIKeyRejectsMediumDirectEnvironmentSecret(t *testing.T) {
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY", "must-not-be-used")
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "")
if _, err := loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1"); err == nil {
t.Fatal("expected direct medium environment secret to be rejected")
}
}
func TestReadProtectedRuntimeValueRejectsPathOutsideSecretRoot(t *testing.T) {
if _, err := readProtectedRuntimeValue("/tmp/not-a-domovoy-secret"); err == nil {
t.Fatal("expected protected path rejection")
}
}
+167
View File
@@ -0,0 +1,167 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"sync"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type qualificationObserver interface {
ObserveQualification(context.Context, capability.ModelQualificationObservation) error
}
type httpQualificationObserver struct{ resolver *httpBindingResolver }
func (o httpQualificationObserver) ObserveQualification(ctx context.Context, value capability.ModelQualificationObservation) error {
if o.resolver == nil {
return fmt.Errorf("qualification observer resolver is nil")
}
return o.resolver.post(ctx, "/v1/cognitive-registry/model-qualification-observations", value, nil)
}
type modelQualificationRequest struct {
Prompt string `json:"prompt"`
MaxOutputBytes int `json:"max_output_bytes"`
ModelIDs []capability.ProviderModelID `json:"model_ids,omitempty"`
}
type modelQualificationResult struct {
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
Status string `json:"status"`
FinishReason cognitive.FinishReason `json:"finish_reason,omitempty"`
LatencyMillis int64 `json:"latency_millis"`
OutputObserved bool `json:"output_observed"`
QualityAssessed bool `json:"quality_assessed"`
Category capability.ModelRatingCategory `json:"category"`
Error string `json:"error,omitempty"`
}
type modelQualificationResponse struct {
Provider capability.ProviderInstanceID `json:"provider"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Results []modelQualificationResult `json:"results"`
}
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("provider")
registration, ok := providers[name]
if !ok || registration.Discovery == nil || registration.Provider == nil {
http.Error(w, "provider qualification not found", http.StatusNotFound)
return
}
var request modelQualificationRequest
if !decodeJSON(w, r, 16<<10, &request) || request.Prompt == "" {
return
}
if request.MaxOutputBytes < 1 {
request.MaxOutputBytes = 512
}
if request.MaxOutputBytes > 4096 {
http.Error(w, "max_output_bytes exceeds qualification bound", http.StatusBadRequest)
return
}
modelsFound, probe := registration.Discovery.Discover(r.Context())
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
return
}
if len(request.ModelIDs) > 0 {
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
})
}
if len(modelsFound) == 0 {
http.Error(w, "requested model not discovered", http.StatusNotFound)
return
}
if len(modelsFound) > 16 {
modelsFound = modelsFound[:16]
}
results := qualifyModels(r.Context(), registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
if len(observers) > 0 && observers[0] != nil {
if err := persistQualificationResults(r.Context(), observers[0], registration, results); err != nil {
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
return
}
}
w.Header().Set("Content-Type", "application/json")
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
})
}
func qualifyModels(ctx context.Context, registration providerRegistration, owner, prompt string, maxOutputBytes int, discovered []models.DiscoveredModel) []modelQualificationResult {
results := make([]modelQualificationResult, len(discovered))
var wg sync.WaitGroup
for index, model := range discovered {
wg.Add(1)
go func(index int, model models.DiscoveredModel) {
defer wg.Done()
started := time.Now()
request := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("qualification:%s:%d", model.ProviderModelID, started.UnixNano())), ActorID: identity.ActorID("qualification-runtime"), PrincipalID: identity.PrincipalID(owner), ScopeID: authority.ScopeID("qualification-scope"), Message: prompt, Locale: "en-US", MaxOutputBytes: maxOutputBytes, ProviderBindingID: "qualification-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: registration.ProviderInstanceID, ModelDescriptorID: capability.ModelDescriptorID("qualification-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
response, err := registration.Provider.Advise(ctx, request)
result := modelQualificationResult{ProviderModelID: model.ProviderModelID, LatencyMillis: time.Since(started).Milliseconds(), QualityAssessed: false, Category: capability.ModelRatingUnrated}
if err != nil {
result.Status = "UNAVAILABLE"
result.FinishReason = cognitive.FinishProviderUnavailable
result.Error = err.Error()
results[index] = result
return
}
result.FinishReason = response.FinishReason
result.OutputObserved = response.AdvisoryText != ""
if response.FinishReason == cognitive.FinishCompleted && result.OutputObserved {
result.Status = "REACHABLE"
} else {
result.Status = "RESPONDED_WITHOUT_QUALIFICATION"
}
results[index] = result
}(index, model)
}
wg.Wait()
return results
}
func persistQualificationResults(ctx context.Context, observer qualificationObserver, registration providerRegistration, results []modelQualificationResult) error {
for _, result := range results {
observationID, err := ids.New()
if err != nil {
return err
}
status := capability.ModelQualificationRespondedWithoutRating
switch result.Status {
case "REACHABLE":
status = capability.ModelQualificationReachable
case "UNAVAILABLE":
status = capability.ModelQualificationUnavailable
}
observation := capability.ModelQualificationObservation{
ID: capability.ModelQualificationObservationID(observationID), ProviderInstanceID: registration.ProviderInstanceID, ProviderModelID: result.ProviderModelID,
Status: status, FinishReason: string(result.FinishReason), LatencyMillis: result.LatencyMillis,
OutputObserved: result.OutputObserved, QualityAssessed: false, Category: capability.ModelRatingUnrated,
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
if err := observation.Validate(); err != nil {
return err
}
if err := observer.ObserveQualification(ctx, observation); err != nil {
return err
}
}
return nil
}
func jsonEncode(w http.ResponseWriter, value any) error {
return json.NewEncoder(w).Encode(value)
}
@@ -0,0 +1,85 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type qualificationProvider struct{}
func (qualificationProvider) Probe(context.Context) models.ProviderProbe {
return models.ProviderProbe{Health: capability.HealthAvailable}
}
func (qualificationProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
return []models.DiscoveredModel{{ProviderModelID: "model-a"}, {ProviderModelID: "model-b"}}, models.ProviderProbe{Health: capability.HealthAvailable}
}
func (qualificationProvider) Advise(_ context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if err := models.ValidateAdvisoryRequest(request); err != nil {
return cognitive.AdvisoryResponse{}, err
}
return cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: "reachable", FinishReason: cognitive.FinishCompleted}, nil
}
func TestModelQualificationUsesDiscoveredModelsAndLeavesQualityUnrated(t *testing.T) {
provider := qualificationProvider{}
mux := http.NewServeMux()
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"gpu": {ProviderInstanceID: "gpu", Provider: provider, Discovery: provider}}, "owner"))
server := httptest.NewServer(mux)
defer server.Close()
request := httptest.NewRequest(http.MethodPost, server.URL+"/v1/provider-discovery/gpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":128}`))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("qualification status=%d body=%s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"status":"REACHABLE"`) || !strings.Contains(response.Body.String(), `"category":"UNRATED"`) || strings.Contains(response.Body.String(), `"quality_assessed":true`) {
t.Fatalf("qualification overclaimed quality: %s", response.Body.String())
}
}
func TestModelQualificationRejectsUnboundedOutput(t *testing.T) {
provider := qualificationProvider{}
mux := http.NewServeMux()
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"cpu": {ProviderInstanceID: "cpu", Provider: provider, Discovery: provider}}, "owner"))
request := httptest.NewRequest(http.MethodPost, "/v1/provider-discovery/cpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":8192}`))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusBadRequest {
t.Fatalf("unbounded qualification status=%d", response.Code)
}
}
type qualificationObserverStub struct {
observations []capability.ModelQualificationObservation
}
func (s *qualificationObserverStub) ObserveQualification(_ context.Context, value capability.ModelQualificationObservation) error {
s.observations = append(s.observations, value)
return nil
}
func TestModelQualificationPersistsReachabilityWithoutQuality(t *testing.T) {
provider := qualificationProvider{}
observer := &qualificationObserverStub{}
mux := http.NewServeMux()
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"cpu": {ProviderInstanceID: "cpu", Provider: provider, Discovery: provider}}, "owner", observer))
request := httptest.NewRequest(http.MethodPost, "/v1/provider-discovery/cpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":128,"model_ids":["model-a"]}`))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK || len(observer.observations) != 1 {
t.Fatalf("qualification persistence status=%d observations=%d body=%s", response.Code, len(observer.observations), response.Body.String())
}
if err := observer.observations[0].Validate(); err != nil || observer.observations[0].Status != capability.ModelQualificationReachable {
t.Fatalf("invalid qualification observation: %+v err=%v", observer.observations[0], err)
}
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.Request) {
var request capability.TaskEvaluationExecutionRequest
if !decodeJSON(w, r, 256<<10, &request) {
return
}
if err := request.Profile.Validate(); err != nil || request.CapabilityID == "" || request.CapabilityID != request.Profile.CapabilityID || request.BindingID == "" || request.BindingRevision < 1 || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || request.MaxOutputBytes < 1 || len(request.Cases) != len(request.Profile.RequiredCaseIDs) {
http.Error(w, "invalid task evaluation execution request", http.StatusBadRequest)
return
}
resolver, ok := g.resolver.(candidateBindingResolver)
if !ok {
http.Error(w, "exact candidate resolver unavailable", http.StatusServiceUnavailable)
return
}
selected, err := resolver.ResolveCandidate(r.Context(), request.BindingID, request.BindingRevision)
if err != nil || selected.Binding.ID != request.BindingID || selected.Binding.Revision != request.BindingRevision || selected.Binding.CapabilityID != request.CapabilityID || selected.Binding.OwnerPrincipalID != request.PrincipalID {
http.Error(w, "cognitive binding snapshot unavailable", http.StatusConflict)
return
}
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
http.Error(w, "external egress acknowledgement required", http.StatusBadRequest)
return
}
provider := g.provider(selected)
if provider == nil {
http.Error(w, "cognitive provider unavailable", http.StatusServiceUnavailable)
return
}
caseSet := make(map[string]struct{}, len(request.Profile.RequiredCaseIDs))
for _, id := range request.Profile.RequiredCaseIDs {
caseSet[id] = struct{}{}
}
observations := make([]capability.TaskCaseObservation, len(request.Cases))
responses := make([]capability.TaskCaseResponse, len(request.Cases))
var wg sync.WaitGroup
seenCases := make(map[string]struct{}, len(request.Cases))
for i, item := range request.Cases {
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
http.Error(w, "invalid task evaluation case", http.StatusBadRequest)
return
}
if _, exists := caseSet[item.CaseID]; !exists {
http.Error(w, "task evaluation case is not in profile", http.StatusBadRequest)
return
}
if _, exists := seenCases[item.CaseID]; exists {
http.Error(w, "duplicate task evaluation case", http.StatusBadRequest)
return
}
seenCases[item.CaseID] = struct{}{}
wg.Add(1)
go func(index int, item capability.TaskEvaluationCase) {
defer wg.Done()
started := time.Now()
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
response, invokeErr := g.invokeAdmitted(r.Context(), string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
observation := capability.TaskCaseObservation{CaseID: item.CaseID, LatencyMicros: time.Since(started).Microseconds(), EvidenceArtifactRef: item.EvidenceArtifactRef}
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: observation.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef}
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
// Invocation success proves reachability only. Without an
// independent verifier and answer evidence, semantic support and
// quality must remain UNKNOWN rather than becoming false evidence.
observation.Result = capability.EvaluationUnknown
observation.QualityScoreMillis = 0
} else {
observation.Result = capability.EvaluationUnknown
observation.CriticalFailure = invokeErr != nil
}
observations[index] = observation
}(i, item)
}
if len(seenCases) != len(caseSet) {
http.Error(w, "task evaluation case set is incomplete", http.StatusBadRequest)
return
}
wg.Wait()
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, Responses: responses, ObservedAtUnixMillis: time.Now().UnixMilli()})
}
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
@@ -0,0 +1,58 @@
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
)
type candidateRegistryStub struct {
registryStub
selected capability.ResolvedCognitiveBinding
}
func (s *candidateRegistryStub) ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error) {
return s.selected, nil
}
func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
selected := gatewaySelection()
registry := &candidateRegistryStub{selected: selected}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "observed response"}}
gateway := cognitiveGateway{
resolver: registry,
providers: map[capability.ProviderKind][]providerRegistration{
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}},
},
now: func() time.Time { return time.UnixMilli(1000) },
}
profile := capability.TaskEvaluationProfile{ID: "profile-general", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: selected.Binding.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact:a"}, {CaseID: "case-b", Prompt: "two", EvidenceArtifactRef: "artifact:b"}}}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response capability.TaskEvaluationExecutionResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if len(response.Observations) != 2 || response.Observations[0].Result != capability.EvaluationUnknown || response.Observations[1].Result != capability.EvaluationUnknown {
t.Fatalf("unexpected observations: %+v", response.Observations)
}
for _, observation := range response.Observations {
if observation.QualityScoreMillis != 0 || observation.CriticalFailure {
t.Fatalf("reachability-only evidence must not carry quality or failure claims: %+v", observation)
}
}
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"errors"
"time"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
)
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
return work.DevelopmentGate{}, errDevelopmentGateCreate
}
value.UpdatedAtUnixMillis = now.UnixMilli()
if err := workdomain.ValidateDevelopmentGate(value); err != nil {
return work.DevelopmentGate{}, err
}
return value, nil
}
func advanceDevelopmentGate(value work.DevelopmentGate, next work.DevelopmentStage, now time.Time) (work.DevelopmentGate, error) {
advanced, err := workdomain.AdvanceDevelopmentGate(value, next)
if err != nil {
return work.DevelopmentGate{}, err
}
advanced.Revision = value.Revision + 1
advanced.UpdatedAtUnixMillis = now.UnixMilli()
return advanced, nil
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) {
now := time.UnixMilli(10)
gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now)
if err != nil {
t.Fatal(err)
}
gate.ProposalRef = "proposal-1"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now)
if err != nil || gate.Revision != 2 {
t.Fatalf("proposal transition failed: %#v %v", gate, err)
}
gate.TaskPacketRef = "task-1"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now)
if err != nil {
t.Fatal(err)
}
gate.WorktreeRef = "isolated-worktree"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now)
if err != nil {
t.Fatal(err)
}
gate.ProducerRunRefs = []work.AgentRunID{"producer"}
gate.VerifierRunRefs = []work.AgentRunID{"verifier"}
gate.VerificationEvidence = "verification-1"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
if err != nil {
t.Fatal(err)
}
if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil {
t.Fatal("promotion bypassed canary")
}
}
+182 -1
View File
@@ -116,6 +116,10 @@ func main() {
if err != nil {
log.Fatal(err)
}
researchClient, err := newResearchRuntimeClient(envOr("RESEARCH_RUNTIME_URL", "http://research-runtime:8087"), &http.Client{Timeout: 12 * time.Second})
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
@@ -178,6 +182,135 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(session)
})
mux.HandleFunc("POST /v1/creator/research/web", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request epistemics.WebResearchRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid web research request", http.StatusBadRequest)
return
}
result, err := researchClient.EvaluateWeb(r.Context(), request)
if err != nil {
http.Error(w, "web research unavailable", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
Evidence []work.ExtensionEvidence `json:"evidence"`
Proposal work.ExtensionProposalRequest `json:"proposal"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid development proposal", http.StatusBadRequest)
return
}
proposal, err := workdomain.BuildExtensionProposal(request.Evidence, request.Proposal)
if err != nil {
http.Error(w, "development proposal rejected", http.StatusUnprocessableEntity)
return
}
proposal.RequestedByPrincipalID = string(session.PrincipalID)
if err := workRepository.SaveExtensionProposal(r.Context(), proposal); err != nil {
http.Error(w, "development proposal conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(proposal)
})
mux.HandleFunc("POST /v1/creator/development/scaffolds", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request work.DevelopmentScaffoldRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid development scaffold", http.StatusBadRequest)
return
}
scaffold, err := workdomain.BuildDevelopmentScaffold(request)
if err != nil {
http.Error(w, "development scaffold rejected", http.StatusUnprocessableEntity)
return
}
scaffold.RequestedByPrincipalID = string(session.PrincipalID)
if err := workRepository.SaveDevelopmentScaffold(r.Context(), scaffold); err != nil {
http.Error(w, "development scaffold conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(scaffold)
})
mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var gate work.DevelopmentGate
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&gate); err != nil {
http.Error(w, "invalid development gate", http.StatusBadRequest)
return
}
gate, err := prepareDevelopmentGate(gate, time.Now())
if err != nil {
http.Error(w, "development gate rejected", http.StatusUnprocessableEntity)
return
}
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil {
http.Error(w, "development gate conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
ExpectedRevision int64 `json:"expected_revision"`
NextStage work.DevelopmentStage `json:"next_stage"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" {
http.Error(w, "invalid development gate transition", http.StatusBadRequest)
return
}
gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id"))
if err != nil || gate.Revision != request.ExpectedRevision {
http.Error(w, "development gate revision conflict", http.StatusConflict)
return
}
gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now())
if err != nil {
http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity)
return
}
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil {
http.Error(w, "development gate conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
@@ -834,6 +967,20 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(binding)
})
mux.HandleFunc("GET /v1/cognitive-bindings/{id}/revisions/{revision}/snapshot", func(w http.ResponseWriter, r *http.Request) {
revision, err := strconv.ParseInt(r.PathValue("revision"), 10, 64)
if err != nil || revision < 1 {
http.Error(w, "invalid cognitive binding revision", http.StatusBadRequest)
return
}
snapshot, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), capability.CapabilityBindingID(r.PathValue("id")), revision)
if err != nil {
http.Error(w, "cognitive binding snapshot not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(snapshot)
})
mux.HandleFunc("POST /v1/cognitive-bindings/resolve", func(w http.ResponseWriter, r *http.Request) {
var requirement capability.CognitiveRequirement
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
@@ -842,7 +989,13 @@ func main() {
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
return
}
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
var candidates []capability.ResolvedCognitiveBinding
var err error
if requirement.TaskProfileID != "" {
candidates, err = capabilityRepository.LoadCognitiveCandidatesForProfile(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID, requirement.TaskProfileID)
} else {
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
}
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
@@ -891,6 +1044,34 @@ func main() {
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/cognitive-registry/task-evaluations", func(w http.ResponseWriter, r *http.Request) {
var value capability.TaskEvaluation
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || capabilitydomain.ValidateTaskEvaluation(value) != nil {
http.Error(w, "invalid task evaluation", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveTaskEvaluation(r.Context(), value); err != nil {
http.Error(w, "save task evaluation", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/cognitive-registry/model-qualification-observations", func(w http.ResponseWriter, r *http.Request) {
var value capability.ModelQualificationObservation
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Validate() != nil {
http.Error(w, "invalid model qualification observation", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveModelQualificationObservation(r.Context(), value); err != nil {
http.Error(w, "save model qualification observation", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
type researchRuntimeClient struct {
baseURL string
client *http.Client
}
func newResearchRuntimeClient(rawURL string, client *http.Client) (researchRuntimeClient, error) {
base := strings.TrimRight(strings.TrimSpace(rawURL), "/")
if base == "" {
return researchRuntimeClient{}, fmt.Errorf("research runtime URL is empty")
}
if client == nil {
client = http.DefaultClient
}
return researchRuntimeClient{baseURL: base, client: client}, nil
}
func (c researchRuntimeClient) EvaluateWeb(ctx context.Context, request epistemics.WebResearchRequest) (epistemics.ResearchResult, error) {
payload, err := json.Marshal(request)
if err != nil {
return epistemics.ResearchResult{}, err
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/research/web/evaluate", bytes.NewReader(payload))
if err != nil {
return epistemics.ResearchResult{}, err
}
httpRequest.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(httpRequest)
if err != nil {
return epistemics.ResearchResult{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return epistemics.ResearchResult{}, fmt.Errorf("research runtime status %d", response.StatusCode)
}
var result epistemics.ResearchResult
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return epistemics.ResearchResult{}, err
}
return result, nil
}
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestResearchRuntimeClientForwardsBoundedWebRequest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/research/web/evaluate" || r.Header.Get("Content-Type") != "application/json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var request epistemics.WebResearchRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.URL != "https://assistant.cin.su/healthz" {
t.Fatalf("unexpected request body: %#v %v", request, err)
}
_ = json.NewEncoder(w).Encode(epistemics.ResearchResult{PlanID: request.Plan.ID, Findings: []epistemics.ResearchFinding{{PropositionKey: request.PropositionKey, State: epistemics.FindingSupported}}})
}))
defer server.Close()
client, err := newResearchRuntimeClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
result, err := client.EvaluateWeb(context.Background(), epistemics.WebResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan-1"}, PropositionKey: "health", URL: "https://assistant.cin.su/healthz"})
if err != nil || result.PlanID != "plan-1" || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingSupported {
t.Fatalf("unexpected result: %#v %v", result, err)
}
}
+1
View File
@@ -15,6 +15,7 @@ func main() {
envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"),
&http.Client{Timeout: 10 * time.Second},
envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"),
envOr("AGENT_WORKER_URL", "http://agent-worker:8090"),
)
if err != nil {
log.Fatal(err)
+112 -9
View File
@@ -33,13 +33,14 @@ const (
var staticFiles embed.FS
type gateway struct {
core *url.URL
continuity *url.URL
commit *url.URL
cognitive *url.URL
client *http.Client
static http.Handler
build buildMetadata
core *url.URL
continuity *url.URL
commit *url.URL
cognitive *url.URL
agentWorker *url.URL
client *http.Client
static http.Handler
build buildMetadata
}
type buildMetadata struct {
@@ -76,6 +77,14 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
if err != nil {
return nil, err
}
agentWorkerURL := "http://agent-worker:8090"
if len(cognitiveURLs) > 1 {
agentWorkerURL = cognitiveURLs[1]
}
agentWorker, err := parseRuntimeURL(agentWorkerURL, "Agent worker")
if err != nil {
return nil, err
}
assets, err := fs.Sub(staticFiles, "static")
if err != nil {
return nil, fmt.Errorf("load embedded console: %w", err)
@@ -96,7 +105,7 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
if len(revision) > 12 {
revision = revision[:12]
}
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, agentWorker: agentWorker, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil
}
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
@@ -126,6 +135,14 @@ 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
}
if r.Method == http.MethodGet && r.URL.Path == "/api/agent-worker/status" {
g.handleAgentWorkerStatus(w, r)
return
}
g.forwardAPI(w, r)
return
}
@@ -133,7 +150,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
if r.URL.Path == "/" || r.URL.Path == "/login" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
g.serveEmbeddedHTML(w, "chat.html")
return
}
@@ -248,6 +265,82 @@ 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) handleAgentWorkerStatus(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.agentWorker
target.Path = "/status"
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target.String(), nil)
if err != nil {
http.Error(w, "agent worker status unavailable", http.StatusBadGateway)
return
}
response, err := g.client.Do(request)
if err != nil {
http.Error(w, "agent worker status 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) {
@@ -621,6 +714,16 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
}
case method == http.MethodGet && path == "/api/learning/recent":
return targetCore, "/v1/creator/learning/recent", true
case method == http.MethodGet && strings.HasPrefix(path, "/api/delegations/") && strings.HasSuffix(path, "/latest-checkpoint"):
id := strings.TrimSuffix(strings.TrimPrefix(path, "/api/delegations/"), "/latest-checkpoint")
if validID(id) {
return targetCore, "/v1/creator/delegations/" + id + "/latest-checkpoint", true
}
case method == http.MethodGet && strings.HasPrefix(path, "/api/delegations/"):
id := strings.TrimPrefix(path, "/api/delegations/")
if validID(id) {
return targetCore, "/v1/creator/delegations/" + id, true
}
case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"):
id := strings.TrimPrefix(path, "/api/prospective-items/")
if validID(id) {
+98
View File
@@ -23,6 +23,92 @@ func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
}
}
func TestCreatorRouteAllowsOnlyBoundedDelegationReads(t *testing.T) {
tests := []struct {
path string
want string
}{
{"/api/delegations/delegation-1", "/v1/creator/delegations/delegation-1"},
{"/api/delegations/delegation-1/latest-checkpoint", "/v1/creator/delegations/delegation-1/latest-checkpoint"},
}
for _, test := range tests {
_, got, ok := creatorRoute(http.MethodGet, test.path)
if !ok || got != test.want {
t.Fatalf("creatorRoute(%q) = %q, %t; want %q, true", test.path, got, ok, test.want)
}
}
if _, _, ok := creatorRoute(http.MethodGet, "/api/delegations/../secrets"); ok {
t.Fatal("path traversal-like delegation route must be denied")
}
}
func TestAgentWorkerStatusProxyRequiresCreatorSession(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()
worker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/status" {
t.Fatalf("unexpected worker path %s", r.URL.Path)
}
_, _ = io.WriteString(w, `{"runtime":"agent-worker","executor_configured":false,"mode":"recovery-only","ready":false,"reason":"bridge missing"}`)
}))
defer worker.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), "http://cognitive.invalid", worker.URL)
if err != nil {
t.Fatal(err)
}
denied := httptest.NewRecorder()
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/agent-worker/status", nil))
if denied.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated status=%d", denied.Code)
}
request := httptest.NewRequest(http.MethodGet, "/api/agent-worker/status", 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(), `"mode":"recovery-only"`) || 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())
}
}
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) {
@@ -268,6 +354,18 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
}
}
func TestLoginRouteServesChatShell(t *testing.T) {
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
if err != nil {
t.Fatal(err)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/login", nil))
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "id=\"username\"") || !strings.Contains(response.Body.String(), "id=\"password\"") {
t.Fatalf("login response = %d", response.Code)
}
}
func TestAdvisoryModeRejectsInvalidConfiguration(t *testing.T) {
_, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, "browser-selected")
if err == nil || !strings.Contains(err.Error(), "invalid advisory binding mode") {
+40
View File
@@ -253,6 +253,40 @@ 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); }
.agent-panel { margin-bottom: 18px; border: 1px solid rgba(120,198,163,.3); background: rgba(13,20,17,.92); box-shadow: inset 3px 0 0 rgba(120,198,163,.38); }
.agent-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); }
.agent-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.agent-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.agent-empty { padding: 20px 18px; color: var(--muted); font: 10px/1.5 var(--mono); }
.agent-details { padding: 16px 18px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
.agent-details div { min-width: 0; }
.agent-details span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.agent-details strong { display: block; margin-top: 7px; overflow: hidden; color: var(--ink); font: 600 10px/1.4 var(--mono); text-overflow: ellipsis; white-space: nowrap; }
.agent-details p { grid-column: 1 / -1; margin: 0; padding-top: 12px; border-top: 1px solid var(--line-soft); color: var(--muted); font: 10px/1.5 var(--mono); }
.agent-worker-status { margin: 0 18px 16px; padding-top: 13px; display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; border-top: 1px solid var(--line-soft); }
.agent-worker-status span { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.agent-worker-status strong { color: var(--warn); font: 600 10px/1.3 var(--mono); }
.agent-worker-status strong[data-state="ready"] { color: var(--good); }
.agent-worker-status p { grid-column: 1 / -1; margin: 0; color: var(--muted); font: 10px/1.45 var(--mono); }
.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 +438,12 @@ 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); }
.agent-details { grid-template-columns: 1fr; gap: 14px; }
.agent-details p { grid-column: auto; }
.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; }
+96 -1
View File
@@ -6,7 +6,7 @@ const contextForm = byId("context-form");
const advisoryForm = byId("advisory-form");
const loginError = byId("login-error");
const toast = byId("toast");
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id", "delegation-id"];
const settingsStorageKey = "domovoy.creator.settings";
const contextPresetsStorageKey = "domovoy.creator.context.presets";
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
@@ -172,6 +172,9 @@ async function showConsole() {
else {
loadActionHistory();
loadLearning();
loadFleetReadiness();
loadAgentDelegation();
loadAgentWorkerStatus();
}
runDiagnostics();
}
@@ -353,6 +356,8 @@ 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("refresh-agent").addEventListener("click", loadAgentDelegation);
byId("logout").addEventListener("click", () => endSession());
byId("run-diagnostics").addEventListener("click", runDiagnostics);
@@ -442,6 +447,9 @@ async function loadDashboard() {
work.push(loadProspectiveRegister(context["prospective-id"]));
work.push(loadActionHistory());
work.push(loadLearning());
work.push(loadFleetReadiness());
work.push(loadAgentDelegation());
work.push(loadAgentWorkerStatus());
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
else setSignal("capability", "не выбрана", "idle");
@@ -500,6 +508,93 @@ 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 loadAgentDelegation() {
const delegationID = byId("delegation-id").value.trim();
const empty = byId("agent-empty");
const details = byId("agent-details");
if (!delegationID) {
empty.hidden = false;
details.hidden = true;
setText("agent-summary", "DELEGATION НЕ ВЫБРАНА");
return;
}
empty.hidden = false;
empty.textContent = "Читаю durable delegation…";
details.hidden = true;
try {
const delegation = await request(`/api/delegations/${encodeURIComponent(delegationID)}`);
const checkpoint = await request(`/api/delegations/${encodeURIComponent(delegationID)}/latest-checkpoint`).catch(() => ({}));
setText("agent-summary", delegation.State || "СОСТОЯНИЕ НЕИЗВЕСТНО");
setText("agent-delegation-state", delegation.State);
setText("agent-run-state", `${delegation.LatestRunID || "run не выбран"} · ${delegation.SelectedImplementationID || "implementation не указана"}`);
setText("agent-blocker", checkpoint.BlockerClass || "нет checkpoint");
setText("agent-next-step", checkpoint.NextStep || checkpoint.RemainingSummary || delegation.DesiredOutcome || "Следующий шаг не записан.");
empty.hidden = true;
details.hidden = false;
} catch (error) {
setText("agent-summary", "ОШИБКА");
empty.textContent = safeClientError(error);
empty.hidden = false;
}
}
async function loadAgentWorkerStatus() {
try {
const status = await request("/api/agent-worker/status");
setText("agent-worker-state", status.mode || "UNKNOWN");
setText("agent-worker-message", status.reason || "Причина не указана.");
byId("agent-worker-state").dataset.state = status.ready === true ? "ready" : "degraded";
} catch (error) {
setText("agent-worker-state", "НЕДОСТУПЕН");
setText("agent-worker-message", safeClientError(error));
byId("agent-worker-state").dataset.state = "degraded";
}
}
async function loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
+26
View File
@@ -94,6 +94,8 @@
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
<label for="delegation-id">Delegation / агентская работа</label>
<input id="delegation-id" autocomplete="off" placeholder="delegation-id из чата">
</div>
</details>
<div class="context-actions"><button class="secondary-button" type="submit">Загрузить контекст</button><button id="reset-context" class="text-button" type="button">Сбросить</button></div>
@@ -181,6 +183,30 @@
</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="agent-panel" aria-labelledby="agent-title" data-page="operations">
<header>
<div><p>Work / durable execution</p><h2 id="agent-title">Агентская работа</h2></div>
<div class="activity-tools"><span id="agent-summary">DELEGATION НЕ ВЫБРАНА</span><button id="refresh-agent" class="icon-button" type="button" aria-label="Обновить агентскую работу" title="Обновить агентскую работу">↻</button></div>
</header>
<div id="agent-empty" class="agent-empty">Укажите delegation ID в расширенных идентификаторах, чтобы прочитать durable state.</div>
<div id="agent-details" class="agent-details" hidden>
<div><span>Состояние delegation</span><strong id="agent-delegation-state">—</strong></div>
<div><span>Run / implementation</span><strong id="agent-run-state">—</strong></div>
<div><span>Последний blocker</span><strong id="agent-blocker">—</strong></div>
<p id="agent-next-step">—</p>
</div>
<div class="agent-worker-status"><span>Executor capability</span><strong id="agent-worker-state">ПРОВЕРКА НЕ ВЫПОЛНЯЛАСЬ</strong><p id="agent-worker-message">Статус worker виден только в авторизованной диагностике.</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>
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
const verifierIdentity = "deterministic-task-verifier-v1"
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"runtime":"task-verifier","status":"ok"}`))
})
mux.HandleFunc("POST /v1/task-evaluations/verify", verifyHandler)
addr := ":8091"
if value := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); value != "" {
addr = value
}
if err := http.ListenAndServe(addr, mux); err != nil {
panic(err)
}
}
func verifyHandler(w http.ResponseWriter, r *http.Request) {
var request capability.TaskCaseVerificationRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.CaseID == "" || request.ResponseText == "" || request.EvidenceArtifactRef == "" || request.LatencyMicros < 0 || (request.Verification.Mode != capability.TaskCaseVerificationExactText && request.Verification.Mode != capability.TaskCaseVerificationJSONFieldEquals) || (request.Verification.Mode == capability.TaskCaseVerificationJSONFieldEquals && request.Verification.FieldPath == "") || request.Verification.ExpectedText == "" || request.Verification.MinimumQualityMillis < 0 || request.Verification.MinimumQualityMillis > 1000 {
http.Error(w, "invalid task verification request", http.StatusBadRequest)
return
}
matched := matches(request)
quality := int64(0)
result := capability.EvaluationUnsupported
if matched {
quality = request.Verification.MinimumQualityMillis
if quality == 0 {
quality = 1000
}
result = capability.EvaluationSupported
}
evidence := verificationEvidence(request, matched)
writeJSON(w, capability.TaskCaseVerificationResponse{CaseID: request.CaseID, Result: result, QualityScoreMillis: quality, LatencyMicros: request.LatencyMicros, EvidenceArtifactRef: request.EvidenceArtifactRef, VerificationEvidenceRef: evidence, VerifierIdentity: verifierIdentity})
}
func verificationEvidence(request capability.TaskCaseVerificationRequest, matched bool) string {
payload := fmt.Sprintf("%s|%s|%s|%s|%s|%t", verifierIdentity, request.CaseID, request.EvidenceArtifactRef, request.Verification.Mode, request.Verification.FieldPath, matched)
digest := sha256.Sum256([]byte(payload))
return "verification:" + hex.EncodeToString(digest[:])
}
func matches(request capability.TaskCaseVerificationRequest) bool {
if request.Verification.Mode == capability.TaskCaseVerificationExactText {
return strings.TrimSpace(request.ResponseText) == strings.TrimSpace(request.Verification.ExpectedText)
}
var document map[string]json.RawMessage
if json.Unmarshal([]byte(request.ResponseText), &document) != nil {
return false
}
value, ok := document[request.Verification.FieldPath]
return ok && strings.TrimSpace(string(value)) == strings.TrimSpace(request.Verification.ExpectedText)
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestVerifyExactTextProducesIndependentEvidence(t *testing.T) {
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: " answer ", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
recorder := httptest.NewRecorder()
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
var response capability.TaskCaseVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 900 || response.VerifierIdentity == "" || response.VerificationEvidenceRef == "" {
t.Fatalf("unexpected verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
func TestVerifyMismatchIsUnsupportedWithEvidence(t *testing.T) {
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: "wrong", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
recorder := httptest.NewRecorder()
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
var response capability.TaskCaseVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationUnsupported || response.QualityScoreMillis != 0 || response.VerificationEvidenceRef == "" {
t.Fatalf("unexpected mismatch response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
func TestVerifyJSONFieldEquals(t *testing.T) {
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "json-1", ResponseText: `{"answer":42}`, EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationJSONFieldEquals, FieldPath: "answer", ExpectedText: "42", MinimumQualityMillis: 850}})
recorder := httptest.NewRecorder()
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
var response capability.TaskCaseVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 850 {
t.Fatalf("unexpected JSON verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
+92 -1
View File
@@ -7,9 +7,11 @@ import (
"log"
"net/http"
"os"
"time"
"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/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
@@ -26,6 +28,10 @@ func main() {
log.Fatal(err)
}
defer repository.Close()
evidenceSink, err := newCoreTaskEvaluationSink(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 12 * time.Second})
if err != nil {
log.Fatal(err)
}
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) }}}})
@@ -42,19 +48,97 @@ func main() {
}
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
})
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/schedule", func(w http.ResponseWriter, r *http.Request) {
var request application.TaskEvaluationScheduleRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
http.Error(w, "invalid task evaluation graph request", http.StatusBadRequest)
return
}
graph, ready, err := application.AdmitAndScheduleTaskEvaluation(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/orchestration/next", func(w http.ResponseWriter, r *http.Request) {
var request work.SolutionSearchInput
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)).Decode(&request); err != nil {
http.Error(w, "invalid orchestration input", http.StatusBadRequest)
return
}
decision, err := application.DecideSolutionSearch(request)
if err != nil {
http.Error(w, "invalid orchestration input", http.StatusUnprocessableEntity)
return
}
writeJSON(w, decision)
})
mux.HandleFunc("POST /v1/work-graphs/orchestration/wave", func(w http.ResponseWriter, r *http.Request) {
var request work.SolutionSearchWaveRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
http.Error(w, "invalid orchestration wave request", http.StatusBadRequest)
return
}
result, err := application.OrchestrateSolutionSearch(r.Context(), repository, request)
if err != nil {
http.Error(w, "invalid orchestration wave request", http.StatusUnprocessableEntity)
return
}
writeJSON(w, result)
})
mux.HandleFunc("POST /v1/work-graphs/result-callback", func(w http.ResponseWriter, r *http.Request) {
var request work.GraphResultCallback
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
http.Error(w, "invalid graph result callback", http.StatusBadRequest)
return
}
graph, err := application.RecordGraphResultCandidate(r.Context(), repository, repository, request)
if err != nil {
http.Error(w, "invalid graph result callback", http.StatusUnprocessableEntity)
return
}
writeJSON(w, graph)
})
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)
result, err := application.CompleteGraphNodeWithReplan(r.Context(), repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, result)
})
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/callback", func(w http.ResponseWriter, r *http.Request) {
var request work.TaskEvaluationEvidenceCallback
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
http.Error(w, "invalid task evaluation callback", http.StatusBadRequest)
return
}
graph, err := application.CompleteTaskEvaluationNode(r.Context(), repository, evidenceSink, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, graph)
})
mux.HandleFunc("POST /v1/work-graphs/synthesis", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphSynthesisRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
http.Error(w, "invalid graph synthesis request", http.StatusBadRequest)
return
}
conflict, synthesis, graph, err := application.SynthesizeAndCompleteGraph(r.Context(), repository, repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, map[string]any{"conflict": conflict, "synthesis": synthesis, "graph": graph})
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8088"
@@ -63,6 +147,13 @@ func main() {
log.Fatal(http.ListenAndServe(addr, mux))
}
func envOr(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
@@ -0,0 +1,49 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
type coreTaskEvaluationSink struct {
baseURL string
client *http.Client
}
func newCoreTaskEvaluationSink(baseURL string, client *http.Client) (*coreTaskEvaluationSink, error) {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if baseURL == "" {
return nil, fmt.Errorf("core runtime URL is required")
}
if client == nil {
client = http.DefaultClient
}
return &coreTaskEvaluationSink{baseURL: baseURL, client: client}, nil
}
func (s *coreTaskEvaluationSink) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
payload, err := json.Marshal(value)
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, s.baseURL+"/v1/cognitive-registry/task-evaluations", bytes.NewReader(payload))
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
response, err := s.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("save task evaluation: status %s", response.Status)
}
return nil
}
+28 -5
View File
@@ -26,6 +26,7 @@ type PerformanceEvidenceID ids.ID
type BillingMode string
type AuthMode string
type SelectionObjective string
type ModelRatingCategory string
const (
QualificationDiscovered QualificationState = "DISCOVERED"
@@ -73,6 +74,11 @@ const (
SelectionQuality SelectionObjective = "QUALITY"
SelectionSpeed SelectionObjective = "SPEED"
SelectionEconomy SelectionObjective = "ECONOMY"
ModelRatingUnrated ModelRatingCategory = "UNRATED"
ModelRatingFast ModelRatingCategory = "FAST"
ModelRatingBalanced ModelRatingCategory = "BALANCED"
ModelRatingQuality ModelRatingCategory = "QUALITY"
)
// ProviderInstance contains only opaque references to endpoint and credentials.
@@ -154,16 +160,33 @@ type CognitiveRequirement struct {
ExcludedBindingIDs []CapabilityBindingID
Objective SelectionObjective
MaxCostMicros *int64
TaskProfileID TaskEvaluationProfileID
}
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
// by Capability. It contains references and declared metadata, never secrets.
type ResolvedCognitiveBinding struct {
Binding CognitiveBinding
Provider ProviderInstance
Model ModelDescriptor
Evaluation *EvaluatedCapability
Performance *PerformanceEvidence
Binding CognitiveBinding
Provider ProviderInstance
Model ModelDescriptor
Evaluation *EvaluatedCapability
Performance *PerformanceEvidence
TaskEvaluation *TaskEvaluation
Qualification *ModelQualificationObservation
Rating *ModelRating
}
// ModelRating is a derived, provider-neutral explanation of why an eligible
// model is routed. It never grants qualification or activation authority.
type ModelRating struct {
TaskProfileID TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
TaskScoreMillis int64 `json:"task_score_millis,omitempty"`
Category ModelRatingCategory `json:"category"`
OverallScoreMillis int64 `json:"overall_score_millis"`
QualityScoreMillis int64 `json:"quality_score_millis"`
ReliabilityScoreMillis int64 `json:"reliability_score_millis"`
LatencyScoreMillis int64 `json:"latency_score_millis"`
EvidenceScoreMillis int64 `json:"evidence_score_millis"`
}
type CognitiveResultClass string
@@ -0,0 +1,47 @@
package capability
import (
"errors"
"git.cin.su/m/Assistant/platform/ids"
)
type ModelQualificationObservationID ids.ID
type ModelQualificationStatus string
const (
ModelQualificationReachable ModelQualificationStatus = "REACHABLE"
ModelQualificationRespondedWithoutRating ModelQualificationStatus = "RESPONDED_WITHOUT_QUALIFICATION"
ModelQualificationUnavailable ModelQualificationStatus = "UNAVAILABLE"
)
// ModelQualificationObservation is operational evidence only. It records
// reachability and latency, never quality, activation, routing priority or
// provider credentials.
type ModelQualificationObservation struct {
ID ModelQualificationObservationID
ProviderInstanceID ProviderInstanceID
ProviderModelID ProviderModelID
Status ModelQualificationStatus
FinishReason string
LatencyMillis int64
OutputObserved bool
QualityAssessed bool
Category ModelRatingCategory
ObservedAtUnixMillis int64
}
func (o ModelQualificationObservation) Validate() error {
if o.ID == "" || o.ProviderInstanceID == "" || o.ProviderModelID == "" || o.FinishReason == "" || o.LatencyMillis < 0 || o.ObservedAtUnixMillis < 1 {
return errors.New("model qualification observation identity and timing are incomplete")
}
switch o.Status {
case ModelQualificationReachable, ModelQualificationRespondedWithoutRating, ModelQualificationUnavailable:
default:
return errors.New("model qualification observation status is unsupported")
}
if o.Category != ModelRatingUnrated || o.QualityAssessed {
return errors.New("model qualification observation cannot assert quality")
}
return nil
}
+122
View File
@@ -0,0 +1,122 @@
package capability
import (
"errors"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/platform/ids"
)
type TaskEvaluationProfileID ids.ID
type TaskEvaluationID ids.ID
type TaskEvaluationProfileStage string
const (
TaskEvaluationGeneral TaskEvaluationProfileStage = "GENERAL"
TaskEvaluationAdditional TaskEvaluationProfileStage = "ADDITIONAL"
)
// TaskEvaluationProfile is the extensible, provider-neutral description of a
// task-specific qualification run. Case payloads live in Artifact; the
// profile stores only stable case references and scoring policy.
type TaskEvaluationProfile struct {
ID TaskEvaluationProfileID
Revision int64
CapabilityID CapabilityID
Stage TaskEvaluationProfileStage
ParentProfileID TaskEvaluationProfileID
TriggerEvidenceRef string
BenchmarkVersion string
RequiredCaseIDs []string
QualityWeightMillis int64
ReliabilityWeightMillis int64
LatencyWeightMillis int64
}
func (p TaskEvaluationProfile) Validate() error {
if p.ID == "" || p.Revision < 1 || p.CapabilityID == "" || (p.Stage != TaskEvaluationGeneral && p.Stage != TaskEvaluationAdditional) || p.BenchmarkVersion == "" || len(p.RequiredCaseIDs) == 0 {
return errors.New("task evaluation profile identity and cases are incomplete")
}
if p.Stage == TaskEvaluationGeneral && (p.ParentProfileID != "" || p.TriggerEvidenceRef != "") {
return errors.New("general task evaluation profile cannot have an expansion parent")
}
if p.Stage == TaskEvaluationAdditional && (p.ParentProfileID == "" || p.TriggerEvidenceRef == "") {
return errors.New("additional task evaluation profile requires parent and trigger evidence")
}
if p.QualityWeightMillis < 0 || p.ReliabilityWeightMillis < 0 || p.LatencyWeightMillis < 0 || p.QualityWeightMillis+p.ReliabilityWeightMillis+p.LatencyWeightMillis != 1000 {
return errors.New("task evaluation profile weights must be non-negative and sum to 1000")
}
seen := make(map[string]struct{}, len(p.RequiredCaseIDs))
for _, caseID := range p.RequiredCaseIDs {
if caseID == "" {
return errors.New("task evaluation profile contains an empty case")
}
if _, ok := seen[caseID]; ok {
return errors.New("task evaluation profile contains duplicate cases")
}
seen[caseID] = struct{}{}
}
return nil
}
// TaskEvaluationExpansionRequest is emitted after a task remains unresolved
// with the current best eligible candidate. It creates only a bounded new
// profile; it never silently rewrites the general benchmark.
type TaskEvaluationExpansionRequest struct {
ParentProfileID TaskEvaluationProfileID
CapabilityID CapabilityID
TriggerEvidenceRef string
NewCaseIDs []string
BenchmarkVersion string
}
type TaskCaseVerificationMode string
const (
TaskCaseVerificationExactText TaskCaseVerificationMode = "EXACT_TEXT"
TaskCaseVerificationJSONFieldEquals TaskCaseVerificationMode = "JSON_FIELD_EQUALS"
)
// TaskCaseVerification is a bounded, task-owned rubric. The expected text is
// test data, not model output, and is never sent to the cognitive provider.
type TaskCaseVerification struct {
Mode TaskCaseVerificationMode
FieldPath string
ExpectedText string
MinimumQualityMillis int64
}
// TaskCaseObservation is produced by the bounded evaluator and verifier. A
// score without a case result and evidence reference is not accepted as
// qualification evidence.
type TaskCaseObservation struct {
CaseID string
Result EvaluationResult
QualityScoreMillis int64
LatencyMicros int64
CriticalFailure bool
EvidenceArtifactRef string
// VerificationEvidenceRef and VerifierIdentity are required whenever an
// observation claims semantic support or a non-zero quality score.
VerificationEvidenceRef string
VerifierIdentity string
}
// TaskEvaluation is durable evidence for one model/binding revision under one
// task profile. It is not activation authority.
type TaskEvaluation struct {
ID TaskEvaluationID
ProfileID TaskEvaluationProfileID
BindingID CapabilityBindingID
BindingRevision int64
CapabilityID CapabilityID
Result EvaluationResult
QualityScoreMillis int64
ReliabilityScoreMillis int64
LatencyScoreMillis int64
OverallScoreMillis int64
CriticalFailureMillis int64
BenchmarkVersion string
ArtifactID artifact.ArtifactID
ObservedAtUnixMillis int64
}
@@ -0,0 +1,63 @@
package capability
import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
)
// TaskEvaluationCase carries one bounded prompt selected by the evaluator.
// The prompt is an input artifact, not a model-produced fact.
type TaskEvaluationCase struct {
CaseID string
Prompt string
EvidenceArtifactRef string
Verification TaskCaseVerification
}
type TaskCaseResponse struct {
CaseID string
ResponseText string
LatencyMicros int64
EvidenceArtifactRef string
}
type TaskCaseVerificationRequest struct {
CaseID string
ResponseText string
LatencyMicros int64
EvidenceArtifactRef string
Verification TaskCaseVerification
}
type TaskCaseVerificationResponse struct {
CaseID string
Result EvaluationResult
QualityScoreMillis int64
LatencyMicros int64
CriticalFailure bool
EvidenceArtifactRef string
VerificationEvidenceRef string
VerifierIdentity string
}
type TaskEvaluationExecutionRequest struct {
Profile TaskEvaluationProfile
CapabilityID CapabilityID
BindingID CapabilityBindingID
BindingRevision int64
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Cases []TaskEvaluationCase
MaxOutputBytes int64
EgressAcknowledged bool
}
type TaskEvaluationExecutionResponse struct {
ProfileID TaskEvaluationProfileID
BindingID CapabilityBindingID
BindingRevision int64
Observations []TaskCaseObservation
Responses []TaskCaseResponse
ObservedAtUnixMillis int64
}
+2
View File
@@ -9,6 +9,7 @@ type SecretPurpose string
const (
SecretPurposeDatabasePassword SecretPurpose = "DATABASE_PASSWORD"
SecretPurposeProviderAPIKey SecretPurpose = "PROVIDER_API_KEY"
)
// RuntimeConsumer identifies a trusted runtime allowed to resolve a credential.
@@ -18,6 +19,7 @@ const (
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
RuntimeConsumerCognitive RuntimeConsumer = "cognitive-worker"
)
// SecretFileRef is bootstrap delivery metadata for the V1 protected local
+114 -33
View File
@@ -1,11 +1,17 @@
package work
import "git.cin.su/m/Assistant/contracts/capability"
type GraphNodeID string
type GraphState string
type GraphNodeState string
type GraphNodePurpose string
const (
GraphNodePurposeTaskEvaluation GraphNodePurpose = "TASK_EVALUATION_SPECIALIST"
GraphNodePurposeGeneric GraphNodePurpose = "GENERIC"
GraphDraft GraphState = "DRAFT"
GraphAdmitted GraphState = "ADMITTED"
GraphRunning GraphState = "RUNNING"
@@ -15,14 +21,15 @@ const (
GraphFailed GraphState = "FAILED"
GraphUnknown GraphState = "UNKNOWN"
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
GraphNodeReady GraphNodeState = "READY"
GraphNodeLeased GraphNodeState = "LEASED"
GraphNodeRunning GraphNodeState = "RUNNING"
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
GraphNodeFailed GraphNodeState = "FAILED"
GraphNodeCancel GraphNodeState = "CANCELLED"
GraphNodeUnknown GraphNodeState = "UNKNOWN"
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
GraphNodeReady GraphNodeState = "READY"
GraphNodeLeased GraphNodeState = "LEASED"
GraphNodeRunning GraphNodeState = "RUNNING"
GraphNodeVerifying GraphNodeState = "VERIFYING"
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
GraphNodeFailed GraphNodeState = "FAILED"
GraphNodeCancel GraphNodeState = "CANCELLED"
GraphNodeUnknown GraphNodeState = "UNKNOWN"
)
type GraphBudget struct {
@@ -31,11 +38,17 @@ type GraphBudget struct {
}
type GraphNode struct {
ID GraphNodeID `json:"id"`
DispatchID string `json:"dispatch_id"`
IdempotencyKey string `json:"idempotency_key"`
State GraphNodeState `json:"state"`
Attempts int `json:"attempts"`
ID GraphNodeID `json:"id"`
DispatchID string `json:"dispatch_id"`
IdempotencyKey string `json:"idempotency_key"`
State GraphNodeState `json:"state"`
Attempts int `json:"attempts"`
Purpose GraphNodePurpose `json:"purpose,omitempty"`
TaskProfileID capability.TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
BindingID capability.CapabilityBindingID `json:"binding_id,omitempty"`
CaseIDs []string `json:"case_ids,omitempty"`
TaskProfile *capability.TaskEvaluationProfile `json:"task_profile,omitempty"`
TaskCases []capability.TaskEvaluationCase `json:"task_cases,omitempty"`
}
type GraphEdge struct {
@@ -44,30 +57,98 @@ type GraphEdge struct {
}
type WorkGraph struct {
ID string `json:"id"`
Revision int64 `json:"revision"`
State GraphState `json:"state"`
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
Budget GraphBudget `json:"budget"`
ID string `json:"id"`
Revision int64 `json:"revision"`
State GraphState `json:"state"`
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
Budget GraphBudget `json:"budget"`
ParentGraphID string `json:"parent_graph_id,omitempty"`
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
SearchInputBound bool `json:"search_input_bound,omitempty"`
SearchInput SolutionSearchInput `json:"search_input,omitempty"`
}
// TaskEvaluationGraphRequest describes a durable fan-out. The graph contains
// one specialist node per binding; each node receives the profile's bounded
// cases and reports evidence back to Capability for fan-in aggregation.
type TaskEvaluationGraphRequest struct {
GraphID string `json:"graph_id"`
Profile capability.TaskEvaluationProfile `json:"profile"`
BindingIDs []capability.CapabilityBindingID `json:"binding_ids"`
DispatchIDs []string `json:"dispatch_ids"`
IdempotencyPrefix string `json:"idempotency_prefix"`
Budget GraphBudget `json:"budget"`
ParentGraphID string `json:"parent_graph_id,omitempty"`
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
ParentGraphState GraphState `json:"parent_graph_state,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
Cases []capability.TaskEvaluationCase `json:"cases,omitempty"`
BindingRevision int64 `json:"binding_revision,omitempty"`
ActorID string `json:"actor_id,omitempty"`
PrincipalID string `json:"principal_id,omitempty"`
ScopeID string `json:"scope_id,omitempty"`
}
// TaskEvaluationEvidenceCallback is emitted by a specialist after it has
// completed the bounded case set. Capability remains the evidence owner;
// Work only advances its matching graph node after the evidence is accepted.
type TaskEvaluationEvidenceCallback struct {
GraphID string `json:"graph_id"`
Revision int64 `json:"revision"`
NodeID GraphNodeID `json:"node_id"`
Evaluation capability.TaskEvaluation `json:"evaluation"`
Succeeded bool `json:"succeeded"`
}
type ResultCandidate struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
AuthorRunID AgentRunID
ArtifactRefs []EvidenceRef
EvidenceSummary string
Summary string
ID string `json:"id"`
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
AuthorRunID AgentRunID `json:"author_run_id"`
ArtifactRefs []EvidenceRef `json:"artifact_refs"`
EvidenceSummary string `json:"evidence_summary"`
Summary string `json:"summary"`
}
// GraphResultCallback carries one agent's attributable candidate to Work.
// Recording a candidate never makes the node successful; the node enters
// VERIFYING until an independent synthesis/verifier accepts it.
type GraphResultCallback struct {
GraphID string `json:"graph_id"`
Revision int64 `json:"revision"`
Candidate ResultCandidate `json:"candidate"`
}
type ConflictSet struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
CandidateIDs []string
Resolved bool
ID string `json:"id"`
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
CandidateIDs []string `json:"candidate_ids"`
Resolved bool `json:"resolved"`
}
type GraphSynthesisState string
const GraphSynthesisVerified GraphSynthesisState = "VERIFIED"
// GraphSynthesisCandidate retains every competing result while recording the
// independently verified selection. It is a candidate, not an external action
// or authority grant.
type GraphSynthesisCandidate struct {
ID string `json:"id"`
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
CandidateIDs []string `json:"candidate_ids"`
SelectedCandidateID string `json:"selected_candidate_id"`
DissentCandidateIDs []string `json:"dissent_candidate_ids"`
VerifierRunID AgentRunID `json:"verifier_run_id"`
VerifierIdentity string `json:"verifier_identity"`
EvidenceRefs []EvidenceRef `json:"evidence_refs"`
Summary string `json:"summary"`
State GraphSynthesisState `json:"state"`
}
+34 -11
View File
@@ -1,6 +1,12 @@
package work
import "time"
import (
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
)
type AgentLeaseID string
type AgentFencingToken uint64
@@ -50,14 +56,31 @@ type IndependentVerification struct {
}
type AgentDispatchIntent struct {
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"`
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"`
SpecialistPurpose string `json:"specialist_purpose,omitempty"`
TaskEvaluationProfileID capability.TaskEvaluationProfileID `json:"task_evaluation_profile_id,omitempty"`
TaskEvaluationBindingID capability.CapabilityBindingID `json:"task_evaluation_binding_id,omitempty"`
TaskEvaluationCaseIDs []string `json:"task_evaluation_case_ids,omitempty"`
TaskEvaluationProfile *capability.TaskEvaluationProfile `json:"task_evaluation_profile,omitempty"`
TaskEvaluationCases []capability.TaskEvaluationCase `json:"task_evaluation_cases,omitempty"`
TaskEvaluationExecution *TaskEvaluationDispatchContext `json:"task_evaluation_execution,omitempty"`
}
type TaskEvaluationDispatchContext struct {
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
BindingRevision int64 `json:"binding_revision"`
ActorID identity.ActorID `json:"actor_id"`
PrincipalID identity.PrincipalID `json:"principal_id"`
ScopeID authority.ScopeID `json:"scope_id"`
}
+66
View File
@@ -0,0 +1,66 @@
package work
type OrchestrationState string
type SearchOptionKind string
const (
OrchestrationSearching OrchestrationState = "SEARCHING"
OrchestrationExecuting OrchestrationState = "EXECUTING"
OrchestrationVerifying OrchestrationState = "VERIFYING"
OrchestrationWaiting OrchestrationState = "WAITING"
OrchestrationCompleted OrchestrationState = "COMPLETED"
OrchestrationCancelled OrchestrationState = "CANCELLED"
SearchOptionAgents SearchOptionKind = "AGENT_TEAM"
SearchOptionCapability SearchOptionKind = "CAPABILITY_DISCOVERY"
SearchOptionResearch SearchOptionKind = "TRUTH_RESEARCH"
SearchOptionReflection SearchOptionKind = "REFLECTION_MEMORY"
SearchOptionHuman SearchOptionKind = "HUMAN_ATTENTION"
)
// SolutionSearchInput is the durable orchestration snapshot. The scheduler
// stores the graph/goal separately; this value only decides the next bounded
// search wave and never grants authority by itself.
type SolutionSearchInput struct {
GoalID GoalID
GoalState GoalState
SearchRound int
MaxRound int
CancelRequested bool
ActiveOptionCount int
VerificationReady bool
Unresolved bool
}
type SolutionSearchDecision struct {
GoalID GoalID
State OrchestrationState
ContinueSearching bool
NextSearchRound int
Options []SearchOptionKind
Reason string
}
type SolutionSearchWaveRequest struct {
Input SolutionSearchInput `json:"input"`
GraphID string `json:"graph_id"`
IdempotencyPrefix string `json:"idempotency_prefix"`
Dispatches []SolutionSearchDispatch `json:"dispatches"`
Budget GraphBudget `json:"budget"`
ParentGraphID string `json:"parent_graph_id,omitempty"`
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
NowUnixMillis int64 `json:"now_unix_millis"`
}
type SolutionSearchDispatch struct {
Option SearchOptionKind `json:"option"`
Intent AgentDispatchIntent `json:"intent"`
}
type SolutionSearchWaveResult struct {
Decision SolutionSearchDecision `json:"decision"`
Graph WorkGraph `json:"graph"`
ScheduledNodeIDs []GraphNodeID `json:"scheduled_node_ids,omitempty"`
GraphMaterialized bool `json:"graph_materialized"`
}
+104
View File
@@ -1,5 +1,7 @@
package work
import "time"
type DevelopmentStage string
const (
@@ -15,6 +17,8 @@ const (
type DevelopmentGate struct {
ID string
Revision int64
UpdatedAtUnixMillis int64
Stage DevelopmentStage
EvidenceRefs []string
ProposalRef string
@@ -27,3 +31,103 @@ type DevelopmentGate struct {
PromotionRef string
RollbackRef string
}
// ExtensionProposalStage is deliberately proposal-only in this contract. A
// proposal does not authorize source changes, provider calls, deployment or
// promotion; those stages require the existing governed development gate.
type ExtensionProposalStage string
const ExtensionProposalProposed ExtensionProposalStage = "PROPOSED"
// ExtensionEvidence is an immutable reference to an observation that points
// at a repeated or corroborated improvement signal. The payload is owned by
// its source module; Work stores only the typed reference and stable signal
// key.
type ExtensionEvidence struct {
Ref string
SignalKey string
}
// ExtensionProposalRequest contains the operator/governance information
// needed to decide whether an evidence pattern is worth reviewing. Empty
// fields are rejected by the proposal generator rather than inferred.
type ExtensionProposalRequest struct {
ID string
DesiredOutcome string
SmallestChangeLayer string
AffectedOwners []string
AffectedContracts []string
RiskTier string
EvaluationPlan []string
Alternatives []string
RollbackPlan string
Assumptions []string
CreatedAt time.Time
}
// ExtensionProposal is a bounded, evidence-bearing candidate for governed
// development. It intentionally contains no executable command, credential,
// authority grant, implementation body or promotion decision.
type ExtensionProposal struct {
ID string
RequestedByPrincipalID string
Stage ExtensionProposalStage
EvidenceRefs []string
DeduplicatedSignalKeys []string
DesiredOutcome string
SmallestChangeLayer string
AffectedOwners []string
AffectedContracts []string
RiskTier string
EvaluationPlan []string
Alternatives []string
RollbackPlan string
Assumptions []string
CreatedAtUnixMillis int64
}
type DevelopmentScaffoldStage string
const DevelopmentScaffoldSandboxed DevelopmentScaffoldStage = "SANDBOXED"
// DevelopmentScaffoldRequest describes the boundary a later control plane
// would use to create an isolated worktree. This value object itself performs
// no checkout or filesystem operation.
type DevelopmentScaffoldRequest struct {
ID string
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
ProtectedPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAt time.Time
}
// DevelopmentScaffold is a provenance-linked task-packet candidate. It has
// no executable instruction, credential, checkout handle or authority grant.
type DevelopmentScaffold struct {
ID string
RequestedByPrincipalID string
Stage DevelopmentScaffoldStage
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAtUnixMillis int64
}
@@ -0,0 +1,58 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.graph_result_candidates (
result_candidate_id TEXT PRIMARY KEY,
graph_id TEXT NOT NULL,
graph_revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
author_run_id TEXT NOT NULL,
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
evidence_summary TEXT NOT NULL,
summary TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (result_candidate_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0),
CHECK (node_id <> ''), CHECK (author_run_id <> ''), CHECK (evidence_summary <> ''), CHECK (summary <> ''),
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
);
CREATE TABLE IF NOT EXISTS work.graph_conflicts (
conflict_id TEXT PRIMARY KEY,
graph_id TEXT NOT NULL,
graph_revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
candidate_ids TEXT[] NOT NULL,
resolved BOOLEAN NOT NULL DEFAULT FALSE,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (conflict_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0), CHECK (node_id <> ''),
CHECK (cardinality(candidate_ids) >= 2),
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
);
CREATE TABLE IF NOT EXISTS work.graph_synthesis_candidates (
synthesis_id TEXT PRIMARY KEY,
graph_id TEXT NOT NULL,
graph_revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
candidate_ids TEXT[] NOT NULL,
selected_candidate_id TEXT NOT NULL,
dissent_candidate_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
verifier_run_id TEXT NOT NULL,
verifier_identity TEXT NOT NULL,
evidence_refs TEXT[] NOT NULL,
summary TEXT NOT NULL,
state TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (synthesis_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0), CHECK (node_id <> ''),
CHECK (cardinality(candidate_ids) >= 2), CHECK (selected_candidate_id <> ''), CHECK (verifier_run_id <> ''),
CHECK (verifier_identity <> ''), CHECK (cardinality(evidence_refs) >= 1), CHECK (summary <> ''), CHECK (state = 'VERIFIED'),
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
);
CREATE INDEX IF NOT EXISTS graph_result_candidates_node_idx ON work.graph_result_candidates(graph_id, graph_revision, node_id);
CREATE INDEX IF NOT EXISTS graph_conflicts_node_idx ON work.graph_conflicts(graph_id, graph_revision, node_id);
CREATE INDEX IF NOT EXISTS graph_synthesis_node_idx ON work.graph_synthesis_candidates(graph_id, graph_revision, node_id);
RESET ROLE;
COMMIT;
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS work.development_gates (
gate_id TEXT PRIMARY KEY,
revision BIGINT NOT NULL CHECK (revision > 0),
updated_at_unix_millis BIGINT NOT NULL CHECK (updated_at_unix_millis > 0),
stage TEXT NOT NULL,
evidence_refs TEXT[] NOT NULL,
proposal_ref TEXT NOT NULL,
task_packet_ref TEXT NOT NULL,
worktree_ref TEXT NOT NULL,
producer_run_refs TEXT[] NOT NULL,
verifier_run_refs TEXT[] NOT NULL,
verification_evidence TEXT NOT NULL,
canary_evidence TEXT NOT NULL,
promotion_ref TEXT NOT NULL,
rollback_ref TEXT NOT NULL
);
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE work.development_gates OWNER TO work_role;
GRANT SELECT, INSERT, UPDATE ON work.development_gates TO work_role;
COMMIT;
@@ -0,0 +1,35 @@
BEGIN;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.cognitive_task_evaluations (
evaluation_id TEXT PRIMARY KEY,
task_profile_id TEXT NOT NULL,
binding_id TEXT NOT NULL,
binding_revision BIGINT NOT NULL,
capability_id TEXT NOT NULL,
result TEXT NOT NULL,
quality_score_millis BIGINT NOT NULL,
reliability_score_millis BIGINT NOT NULL,
latency_score_millis BIGINT NOT NULL,
overall_score_millis BIGINT NOT NULL,
critical_failure_millis BIGINT NOT NULL,
benchmark_version TEXT NOT NULL,
artifact_id TEXT NOT NULL,
observed_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (binding_revision > 0),
CHECK (result IN ('UNKNOWN','SUPPORTED','UNSUPPORTED')),
CHECK (quality_score_millis BETWEEN 0 AND 1000),
CHECK (reliability_score_millis BETWEEN 0 AND 1000),
CHECK (latency_score_millis BETWEEN 0 AND 1000),
CHECK (overall_score_millis BETWEEN 0 AND 1000),
CHECK (critical_failure_millis BETWEEN 0 AND 1000),
CHECK (observed_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS cognitive_task_evaluations_lookup
ON capability.cognitive_task_evaluations(task_profile_id, binding_id, binding_revision, observed_at_unix_millis DESC);
GRANT SELECT, INSERT ON capability.cognitive_task_evaluations TO capability_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,23 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.agent_dispatch_intents
ADD COLUMN IF NOT EXISTS specialist_purpose TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS task_evaluation_profile_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS task_evaluation_binding_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS task_evaluation_case_ids TEXT[] NOT NULL DEFAULT '{}';
ALTER TABLE work.graphs
ADD COLUMN IF NOT EXISTS parent_graph_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS parent_graph_revision BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS trigger_evidence_ref TEXT NOT NULL DEFAULT '';
ALTER TABLE work.graph_nodes
ADD COLUMN IF NOT EXISTS purpose TEXT NOT NULL DEFAULT 'GENERIC',
ADD COLUMN IF NOT EXISTS task_profile_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS binding_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS case_ids TEXT[] NOT NULL DEFAULT '{}';
RESET ROLE;
COMMIT;
@@ -0,0 +1,16 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.agent_dispatch_intents
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
ALTER TABLE work.graph_nodes
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
RESET ROLE;
COMMIT;
@@ -0,0 +1,15 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.agent_dispatch_queue
ADD COLUMN IF NOT EXISTS retry_limit INTEGER NOT NULL DEFAULT 3;
ALTER TABLE work.agent_dispatch_queue
DROP CONSTRAINT IF EXISTS agent_dispatch_queue_retry_limit_check;
ALTER TABLE work.agent_dispatch_queue
ADD CONSTRAINT agent_dispatch_queue_retry_limit_check CHECK (retry_limit > 0);
RESET ROLE;
COMMIT;
@@ -0,0 +1,45 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.extension_proposals (
proposal_id TEXT PRIMARY KEY,
requested_by_principal_id TEXT NOT NULL,
stage TEXT NOT NULL,
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
deduplicated_signal_keys TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
desired_outcome TEXT NOT NULL,
smallest_change_layer TEXT NOT NULL,
affected_owners TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
affected_contracts TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
risk_tier TEXT NOT NULL,
evaluation_plan TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
alternatives TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
rollback_plan TEXT NOT NULL,
assumptions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
created_at_unix_millis BIGINT NOT NULL,
CHECK (proposal_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'PROPOSED'), CHECK (desired_outcome <> ''), CHECK (smallest_change_layer <> ''), CHECK (risk_tier <> ''), CHECK (rollback_plan <> ''), CHECK (created_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.development_scaffolds (
scaffold_id TEXT PRIMARY KEY,
requested_by_principal_id TEXT NOT NULL,
stage TEXT NOT NULL,
proposal_ref TEXT NOT NULL,
base_repo_revision TEXT NOT NULL,
worktree_ref TEXT NOT NULL,
task_intent TEXT NOT NULL,
modules TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
allowed_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
forbidden_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
required_tests TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
artifact_lineage_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
protected_main_access TEXT NOT NULL,
production_access TEXT NOT NULL,
secret_access TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
CHECK (scaffold_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'SANDBOXED'), CHECK (proposal_ref <> ''), CHECK (base_repo_revision <> ''), CHECK (worktree_ref <> ''), CHECK (task_intent <> ''), CHECK (protected_main_access = 'DENIED'), CHECK (production_access = 'DENIED'), CHECK (secret_access = 'DENIED'), CHECK (created_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,21 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_bound BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_goal_id TEXT NOT NULL DEFAULT '';
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_goal_state TEXT NOT NULL DEFAULT '';
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_search_round INTEGER NOT NULL DEFAULT 0;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_max_round INTEGER NOT NULL DEFAULT 0;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_cancel_requested BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_verification_ready BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_unresolved BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs DROP CONSTRAINT IF EXISTS work_graphs_orchestration_snapshot_check;
ALTER TABLE work.graphs ADD CONSTRAINT work_graphs_orchestration_snapshot_check CHECK (
(NOT orchestration_bound AND orchestration_goal_id = '' AND orchestration_search_round = 0 AND orchestration_max_round = 0)
OR (orchestration_bound AND orchestration_goal_id <> '' AND orchestration_search_round >= 1 AND orchestration_max_round >= 1)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,24 @@
BEGIN;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.cognitive_model_qualification_observations (
observation_id TEXT PRIMARY KEY,
provider_instance_id TEXT NOT NULL,
provider_model_id TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('REACHABLE','RESPONDED_WITHOUT_QUALIFICATION','UNAVAILABLE')),
finish_reason TEXT NOT NULL,
latency_millis BIGINT NOT NULL CHECK (latency_millis >= 0),
output_observed BOOLEAN NOT NULL,
quality_assessed BOOLEAN NOT NULL DEFAULT FALSE CHECK (quality_assessed = FALSE),
category TEXT NOT NULL CHECK (category = 'UNRATED'),
observed_at_unix_millis BIGINT NOT NULL CHECK (observed_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS cognitive_model_qualification_lookup
ON capability.cognitive_model_qualification_observations(provider_instance_id, provider_model_id, observed_at_unix_millis DESC);
GRANT SELECT, INSERT ON capability.cognitive_model_qualification_observations TO capability_role;
RESET ROLE;
COMMIT;
+35 -4
View File
@@ -99,13 +99,17 @@ services:
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
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
agent-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
command: ["-worker-id", "agent-worker-production"]
command: ["-worker-id", "agent-worker-production", "-status-addr", ":8090"]
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
@@ -114,10 +118,24 @@ services:
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 } }
TASK_VERIFIER_URL: http://task-verifier:8091
depends_on:
postgres: { condition: service_healthy }
task-verifier: { condition: service_healthy }
volumes:
- assistant_core_secrets:/run/secrets:ro
task-verifier:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: task-verifier, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: :8091
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8091/healthz >/dev/null"]
interval: 10s
timeout: 3s
retries: 10
work-graph-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: work-graph-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -211,11 +229,18 @@ services:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime:8080
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-api:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-}
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
depends_on: [core-runtime, cognitive-lifeboat]
volumes:
- assistant_core_secrets:/run/secrets:ro
networks: [default, llm-local]
cognitive-lifeboat:
image: ghcr.io/ggml-org/llama.cpp:server
@@ -257,6 +282,7 @@ services:
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
AGENT_WORKER_URL: http://agent-worker:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
@@ -281,3 +307,8 @@ volumes:
external: true
assistant_research_sources:
name: assistant_research_sources
networks:
llm-local:
external: true
name: llm-local_default
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000146-work-graph-runtime.json",
"task_id": "DEV-000146",
"task_file": "dev/tasks/DEV-000243-qualification-selection-evidence.json",
"task_id": "DEV-000243",
"task_revision": 1
}
@@ -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,23 @@
{
"task_id": "DEV-000149",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "eb920bb9f24e9669320ffefaed1177e2e20ba732",
"risk_tier": "E",
"intent": "Capture fresh bounded synthetic qualification evidence for the currently available production Lifeboat model and typed unavailability for unavailable fleet tiers.",
"requirement_ids": ["WP-200", "WP-201", "WP-202", "WP-203"],
"allowed_change_scope": {"modules": ["capability"], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000149-live-model-qualification.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "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": ["creator-advisory-synthetic-v1", "fleet readiness response"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["evidence only; no activation, canary or promotion"],
"side_effects_and_privileges": ["bounded synthetic inference through existing internal Lifeboat endpoint"],
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
"failure_retry_restart": ["provider timeout/auth/unavailability is recorded as typed failure; no automatic retry"],
"assumptions": ["synthetic prompts contain no private data", "SSH tunnel exposes only the existing internal Lifeboat service for the bounded run"],
"acceptance_scenarios": ["Lifeboat receives all four bounded corpus cases", "artifact binds exact provider/model/binding revisions", "unavailable FAST/MEDIUM/HEAVY remain explicit and inactive", "evidence cannot activate a binding"],
"required_tests": ["benchmark artifact validation", "go test ./...", "go vet ./...", "preflight", "check-diff", "git diff --check", "check-architecture", "independent"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000150",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ee678db42cfc62c930b6fd15e7cf93823a61f58b",
"risk_tier": "E",
"intent": "Expose read-only durable delegation and latest checkpoint state in the authenticated creator operations cabinet.",
"requirement_ids": ["WP-801", "WP-804", "INV-TOPO-001"],
"allowed_change_scope": {"modules": ["interaction", "work"], "paths": ["cmd/core-runtime/main.go", "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-000150-agent-operations-ui.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "database/**", "modules/**", "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": ["creator delegation read API", "creator latest checkpoint read API"], "may_change": []},
"semantic_owner": "work",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["authenticated read-only proxy and rendering only"],
"persistence_and_migrations": [],
"failure_retry_restart": ["missing delegation/checkpoint remains visible as unavailable; no mutation or automatic retry"],
"assumptions": ["creator session authentication remains enforced by Core", "delegation ID is supplied by the user from an existing chat response"],
"acceptance_scenarios": ["unauthenticated delegation reads remain denied", "valid delegation ID renders state/run/implementation", "checkpoint blocker and next step render when present", "invalid path-like ID is denied", "mobile layout has no forced horizontal overflow"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "git diff --check", "check-architecture", "independent"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000151",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "6871e36657a10e8ad7fb1e059efdc2dde394ebbd",
"risk_tier": "E",
"intent": "Expose truthful agent-worker executor capability status to the authenticated operations cabinet while preserving recovery-only behavior when no bridge is configured.",
"requirement_ids": ["WP-301", "WP-804", "INV-TOPO-001"],
"allowed_change_scope": {"modules": ["work", "interaction", "agent-worker"], "paths": ["cmd/agent-worker/**", "cmd/creator-web/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000151-agent-worker-status-ui.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "database/**", "contracts/**", "modules/**", "adapters/**"]},
"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": ["creator session", "agent-worker status HTTP response"], "may_change": []},
"semantic_owner": "work",
"canonical_state": [],
"state_transitions": ["executor configured -> execution status; executor absent -> recovery-only status"],
"side_effects_and_privileges": ["read-only internal status endpoint and authenticated UI proxy"],
"persistence_and_migrations": [],
"failure_retry_restart": ["status remains truthful across worker restart; no dispatch claim is introduced"],
"assumptions": ["agent-worker and creator-web share the production compose network", "AGENT_EXECUTION_BRIDGE_URL remains the only execution activation input"],
"acceptance_scenarios": ["worker status reports recovery-only when bridge absent", "readyz is non-ready without executor", "unauthenticated public status proxy returns 401", "authenticated cabinet renders degraded capability reason", "compose wiring reaches internal worker status"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "git diff --check", "check-architecture", "independent", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,35 @@
{
"task_id": "DEV-000152",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "624c41d4f6dae8c8d341aec09485efbba78bc76f",
"risk_tier": "E",
"intent": "Enable cognitive-worker to authenticate the local OpenAI-compatible CPU gateway through a typed file-backed credential reference without exposing provider key material in environment, source, or logs.",
"requirement_ids": ["AUDIT-WP-003", "AUDIT-WP-201", "CONSTITUTION-1.3.28"],
"allowed_change_scope": {
"modules": ["capability", "cognitive"],
"paths": [
"contracts/secrets/types.go",
"platform/secrets/**",
"adapters/models/openai_compatible_local.go",
"adapters/models/openai_compatible_local_test.go",
"cmd/cognitive-worker/main.go",
"cmd/cognitive-worker/main_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000152-cognitive-provider-secret-reference.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "deploy/**", "database/**"]},
"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": ["typed CredentialRef", "protected file-backed secret resolver"], "may_change": ["secret purpose and trusted runtime consumer enum values"]},
"semantic_owner": "capability",
"canonical_state": ["provider credential material remains external to repository and database"],
"state_transitions": ["cognitive-worker sends a bearer credential only when a trusted file-backed reference resolves successfully"],
"side_effects_and_privileges": ["none; adapter receives resolved material from runtime bootstrap and never persists it"],
"persistence_and_migrations": ["no database migration"],
"failure_retry_restart": ["direct secret environment configuration fails closed", "missing reference or unreadable file fails startup", "no configured key preserves existing unauthenticated development behavior"],
"assumptions": ["production operators provision the gateway key into the protected secret volume separately; this task does not copy or reveal it"],
"acceptance_scenarios": ["resolved key becomes Authorization Bearer header", "no key sends no Authorization header", "direct environment secret is rejected", "secret value is absent from logs and repository"],
"required_tests": ["unit", "go test", "go vet", "architecture preflight", "check-diff", "git diff --check", "independent", "exact diff scope", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000153",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "dbbc54171d9c8a9edaa93e5ff5870d3ec434d799",
"risk_tier": "E",
"intent": "Wire the production cognitive-worker to the existing protected secret volume for optional local CPU gateway authentication without placing credential material in compose environment values.",
"requirement_ids": ["AUDIT-WP-003", "AUDIT-WP-201", "CONSTITUTION-1.3.28"],
"allowed_change_scope": {"modules": ["cognitive"], "paths": ["deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000153-fleet-secret-wiring.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "database/**", "contracts/**", "modules/**", "adapters/**"]},
"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": ["assistant_core_secrets read-only volume", "optional LOCAL_CPU_PROVIDER_API_KEY_FILE reference"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": ["provider credential material remains outside repository and compose environment"],
"state_transitions": ["when operators provision the referenced file, cognitive-worker resolves it and authenticates the local CPU gateway; when absent, existing recovery behavior remains available"],
"side_effects_and_privileges": ["read-only mount only; no secret value is added to compose"],
"persistence_and_migrations": ["no database migration"],
"failure_retry_restart": ["empty optional file reference preserves current unauthenticated startup", "configured missing credential file fails closed at worker startup"],
"assumptions": ["the gateway credential is provisioned by production operators into assistant_core_secrets; this task does not copy or reveal it"],
"acceptance_scenarios": ["compose renders with empty optional key path", "cognitive-worker receives read-only /run/secrets mount", "no literal provider credential occurs in repository or compose", "CI and production deployment succeed"],
"required_tests": ["go test", "go vet", "docker compose config", "architecture preflight", "check-diff", "git diff --check", "independent", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000154",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "283f5c044c3bf6739c8ed859412cd736754e3177",
"risk_tier": "E",
"intent": "Record bounded production evidence that the authenticated local CPU provider is available and invokable through the cognitive boundary.",
"requirement_ids": ["AUDIT-WP-003", "AUDIT-WP-201", "WP-601"],
"allowed_change_scope": {"modules": ["capability", "cognitive"], "paths": ["docs/evidence/wp-601-fast-gateway-live-smoke-2026-08-22-v1.json", "docs/evidence/WP-601-FAST-GATEWAY-LIVE-SMOKE-2026-08-22.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000154-fast-live-smoke-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "database/**", "deploy/**", "contracts/**", "modules/**", "adapters/**"]},
"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": ["production cognitive discovery and bounded advisory response evidence"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": ["none; append-only evidence artifact only"],
"state_transitions": ["none; evidence does not activate or promote a binding"],
"side_effects_and_privileges": ["four bounded local-only synthetic advisory requests"],
"persistence_and_migrations": ["no database migration"],
"failure_retry_restart": ["not applicable to evidence artifact"],
"assumptions": ["provider key remains in protected production volume and is not copied to repository or logs"],
"acceptance_scenarios": ["evidence records available API_KEY provider", "four requests complete with expected sentinels", "limitations remain explicit", "no secret material is present"],
"required_tests": ["JSON validation", "go test", "go vet", "architecture preflight", "check-diff", "git diff --check", "independent", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000155",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "5703ab3ed5e22a5424a2db7f19886c0305e54280",
"risk_tier": "E",
"intent": "Synchronize the project status projection with current live audit evidence, explicitly distinguishing completed runtime foundations from external blockers.",
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS", "WP-003", "WP-301", "WP-800"],
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000155-current-audit-status.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "database/**", "deploy/**", "contracts/**", "modules/**", "adapters/**"]},
"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": ["live audit observations and committed evidence artifacts"], "may_change": []},
"semantic_owner": "none; documentation projection",
"canonical_state": ["none"],
"state_transitions": ["none"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["no database migration"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["status projection remains subordinate to runtime evidence and does not declare blocked items complete"],
"acceptance_scenarios": ["current revision and FAST evidence are referenced", "Medium/Heavy and bridge gaps are explicit", "UI acceptance remains pending without session", "no secret value appears"],
"required_tests": ["JSON validation", "go test", "go vet", "architecture preflight", "check-diff", "git diff --check", "independent", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000156",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "03c3484ee1488ebc3b2349ab6ba5c8005c895110",
"risk_tier": "E",
"intent": "Capture fresh production evidence for model fleet readiness and Agent Execution capability without changing runtime behavior.",
"requirement_ids": ["WP-200", "WP-301", "AUDIT-TRUTHFUL-STATUS"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000156-fleet-execution-status-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["fleet readiness response", "agent-worker status response"], "may_change": []},
"semantic_owner": "none; evidence projection",
"canonical_state": [],
"state_transitions": ["evidence only; no activation, promotion or configuration change"],
"side_effects_and_privileges": ["bounded read-only production status probes"],
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
"failure_retry_restart": ["probe failures are recorded as observed facts; no retry-dependent claim"],
"assumptions": ["all probes are internal read-only GET requests", "no secret values are collected"],
"acceptance_scenarios": ["FAST readiness auth mismatch is distinguished from successful bounded gateway smoke", "MEDIUM remains NOT_CONFIGURED", "HEAVY timeout is recorded", "LIFEBOAT is the selected fallback", "Agent Execution is recovery-only", "no secret value appears"],
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "git diff --check", "independent", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000157",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "077f16e50ca22e14dbf00214005f0fe98a1c8308",
"risk_tier": "E",
"intent": "Capture fresh production PostgreSQL backup checksum and isolated restore verification evidence without changing the production database.",
"requirement_ids": ["WP-002", "AUDIT-RECOVERY-TRUTH"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000157-postgres-restore-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["backup artifact format", "restore verification output"], "may_change": []},
"semantic_owner": "none; evidence projection",
"canonical_state": [],
"state_transitions": ["evidence only; no production database mutation"],
"side_effects_and_privileges": ["isolated temporary restore container created and removed by the existing verifier"],
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
"failure_retry_restart": ["checksum or restore failure is recorded as failure; no blind retry claim"],
"assumptions": ["the standard verifier uses tmpfs and cleans up its temporary container", "backup contents and credentials are not copied into repository evidence"],
"acceptance_scenarios": ["latest backup directory is selected", "checksums pass", "isolated restore reports expected schema/table counts", "production database remains untouched"],
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "git diff --check", "independent", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000158",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "3a52c18bef7e29a0369ad1edf05adeed1be5935e",
"risk_tier": "E",
"intent": "Synchronize the project status projection with the latest production revision and committed fleet/recovery evidence.",
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS", "WP-002", "WP-200", "WP-301"],
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000158-status-evidence-consolidation.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["production revision", "committed fleet evidence", "committed restore evidence"], "may_change": []},
"semantic_owner": "none; documentation projection",
"canonical_state": [],
"state_transitions": ["evidence projection only"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["no database migration"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["status remains subordinate to live evidence and does not declare unavailable capabilities complete"],
"acceptance_scenarios": ["latest production revision is shown", "fleet and restore evidence are linked", "remaining gaps remain explicit", "no secret value appears"],
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "git diff --check", "independent", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000159",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "e1186757bfcf9a349ce0ced603f4a4ebdad636fe",
"risk_tier": "E",
"intent": "Expose the already-supported optional Agent Execution bridge URL to the production agent-worker without enabling it when unset.",
"requirement_ids": ["WP-301", "Agent Execution runtime and recovery"],
"allowed_change_scope": {"modules": ["deployment", "work-agent-execution"], "paths": ["deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000159-agent-bridge-config-wiring.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "cmd/**", "modules/**", "adapters/**", ".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": ["AGENT_EXECUTION_BRIDGE_URL deployment variable", "agent-worker optional bridge behavior"], "may_change": []},
"semantic_owner": "deployment projection; work-agent-execution remains owner",
"canonical_state": ["none"],
"state_transitions": ["unset URL -> recovery-only; configured URL -> existing typed HTTP executor path"],
"side_effects_and_privileges": ["deployment privileged-surface review is declared; no effective new privilege; no credential forwarding; no outbound call when variable is empty"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["existing worker fail-closed and stale recovery behavior remains unchanged"],
"assumptions": ["an independently operated bridge is still required before enabling the variable in production", "empty default preserves current recovery-only behavior"],
"acceptance_scenarios": ["production compose renders with empty bridge URL", "agent-worker remains recovery-only when unset", "configured URL is available to the process without source secret", "no bridge runtime is fabricated"],
"required_tests": ["JSON validation", "docker compose config", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000160",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "ada2da0c6c93d630f7699bd05abd32031c11544c",
"risk_tier": "E",
"intent": "Synchronize project status with the latest bridge wiring deployment and preserve the explicit external capability gap.",
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS", "WP-301"],
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000160-status-after-bridge-wiring.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["production revision", "optional bridge configuration state"], "may_change": []},
"semantic_owner": "none; documentation projection",
"canonical_state": [],
"state_transitions": ["evidence projection only"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["unset bridge URL remains truthful recovery-only state"],
"acceptance_scenarios": ["current production revision is shown", "bridge wiring and missing runtime are distinguished", "no secret value appears"],
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000161",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "3cf44ada8afd75fb008cf8d5965caf9c3f5b0462",
"risk_tier": "E",
"intent": "Publish a requirement-by-requirement completion matrix for the audit objective, distinguishing repository proof, production proof and external gaps.",
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-301", "WP-601", "WP-800"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000161-completion-audit-matrix.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["audit roadmap", "committed task packets", "production evidence artifacts"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["evidence projection only"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["unknown or externally blocked requirements are not marked complete", "matrix claims are limited to cited evidence scope"],
"acceptance_scenarios": ["all seven objective areas have a status", "production and repository proof are distinguished", "remaining external gaps are explicit", "no secret value appears"],
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000162",
"task_revision": 1,
"architecture_revision": "ADR-0010",
"base_repo_revision": "bda71caecece67e5ef225ac250a7a80c4bc84d94",
"risk_tier": "E",
"intent": "Refresh current production revision in project status and completion evidence after the audit matrix deployment.",
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS"],
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000162-status-after-completion-matrix.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["current production revision", "completion matrix evidence"], "may_change": []},
"semantic_owner": "none; documentation projection",
"canonical_state": [],
"state_transitions": ["evidence projection only"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["matrix remains NOT_COMPLETE until external gaps are actually verified"],
"acceptance_scenarios": ["status and matrix point to bda71ca", "no completion claim is widened", "no secret value appears"],
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000163",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "c25a6346d0615f66ddb411450fcc7a36c62d2b48",
"risk_tier": "E",
"intent": "Extend authenticated cabinet contract tests to cover truthful model-fleet and Agent Execution status surfaces already implemented in Creator Web.",
"requirement_ids": ["WP-801", "WP-802", "WP-805", "Authenticated cabinet acceptance"],
"allowed_change_scope": {"modules": ["creator-web"], "paths": ["tests/creator_web/cabinet_acceptance_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000163-operations-ui-contract-refresh.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "contracts/**", ".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 authenticated Creator Web status routes and static cabinet"], "may_change": ["acceptance tests only"]},
"semantic_owner": "creator-web",
"canonical_state": ["none; tests inspect existing UI contract"],
"state_transitions": ["authenticated session gates truthful status requests"],
"side_effects_and_privileges": ["no external mutation; no credentials embedded"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["missing status surface fails CI"],
"assumptions": ["live authenticated browser visual acceptance remains a separate production verification"],
"acceptance_scenarios": ["fleet and executor status controls exist", "server exposes authenticated routes", "console renders actual selected tier and worker mode", "responsive/reduced-motion tests remain active"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_AUTHENTICATED_BROWSER_SESSION", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000164",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "65a60d5f068e3d8f70733c4ee96bfbb9d9943d5e",
"risk_tier": "E",
"intent": "Make live model-fleet readiness use the existing file-backed CPU provider API key so FAST status reflects the authenticated provider path.",
"requirement_ids": ["WP-601", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["cognitive-worker", "capability"], "paths": ["cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status.go", "cmd/cognitive-worker/fleet_status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000164-authorized-fleet-readiness.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "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 provider API key resolver", "fleet readiness response"], "may_change": ["fleet readiness probe request headers"]},
"semantic_owner": "capability/cognitive-worker",
"canonical_state": [],
"state_transitions": ["authenticated provider probe -> truthful FAST readiness"],
"side_effects_and_privileges": ["uses existing in-memory resolved key only for the internal provider probe; no key logging or persistence"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["401 remains a typed unavailable result when key is absent or rejected; GPU/Medium behavior unchanged"],
"assumptions": ["OpenAI-compatible local provider uses Bearer authentication as the existing model adapter does"],
"acceptance_scenarios": ["fleet readiness sends Bearer header when CPU key is configured", "FAST is ready when authenticated /models succeeds", "no Authorization header is sent without a key", "Lifeboat fallback remains unchanged"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000165",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "5d5afc88883c7f79e3b051c790642e41386666ec",
"risk_tier": "E",
"intent": "Use the existing file-backed CPU provider API key in cognitive-worker readiness probes so /readyz and fleet readiness report consistent provider state.",
"requirement_ids": ["WP-601", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["cognitive-worker", "platform-health"], "paths": ["runtime/health/server.go", "runtime/health/server_test.go", "cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000165-authorized-cpu-health-probe.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "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 health Probe contract", "existing provider API key resolver"], "may_change": ["health probe header support"]},
"semantic_owner": "cognitive-worker/platform health",
"canonical_state": [],
"state_transitions": ["authenticated CPU probe -> consistent readiness evidence"],
"side_effects_and_privileges": ["resolved key is used only in outbound probe header; never logged or persisted"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["provider timeout or rejected key remains degraded; no fallback behavior changed"],
"assumptions": ["the local provider uses Bearer authentication"],
"acceptance_scenarios": ["health probe sends configured header", "cognitive /readyz uses authenticated CPU probe", "GPU remains optional", "no key is sent when absent"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000166",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "4086b7522460a78afc341d996ca7874505018871",
"risk_tier": "E",
"intent": "Give the authenticated CPU readiness probe the provider adapter timeout while retaining short opportunistic GPU probing.",
"requirement_ids": ["WP-601", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["cognitive-worker", "platform-health"], "paths": ["runtime/health/server.go", "runtime/health/server_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000166-per-provider-readiness-timeouts.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "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 health Probe contract", "existing fleet readiness probe"], "may_change": ["per-probe timeout metadata"]},
"semantic_owner": "cognitive-worker/platform health",
"canonical_state": [],
"state_transitions": ["provider-specific probe deadline -> truthful readiness result"],
"side_effects_and_privileges": ["no new privilege; existing provider auth only"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["CPU gets bounded 10-second probe; GPU retains 2-second opportunistic timeout"],
"assumptions": ["CPU provider catalog may take longer than opportunistic probes while remaining within adapter bounds"],
"acceptance_scenarios": ["per-probe health timeout is honored", "CPU fleet client uses 10 seconds", "GPU remains bounded", "Lifeboat fallback remains unchanged"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000167",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "c7b57daaf8233b0f0e79c77c02e92040378921d5",
"risk_tier": "E",
"intent": "Use the bounded CPU provider HTTP client for the authenticated readiness probe so /readyz reports the same truthful result as fleet readiness.",
"requirement_ids": ["WP-601", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000167-cpu-readiness-client-timeout.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "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 readiness Probe contract", "existing authenticated CPU fleet endpoint"], "may_change": []},
"semantic_owner": "cognitive-worker",
"canonical_state": [],
"state_transitions": ["CPU provider response -> consistent /readyz and fleet readiness state"],
"side_effects_and_privileges": ["no new privilege; existing provider auth only"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["CPU readiness remains bounded at 10 seconds; opportunistic GPU remains 2 seconds"],
"assumptions": ["The CPU provider is authenticated with the existing file-backed API key"],
"acceptance_scenarios": ["/readyz can report CPU AVAILABLE when fleet readiness reports CPU AVAILABLE", "GPU remains bounded", "Lifeboat fallback remains unchanged"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000168",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "205d8f620603ccf3e0138f6beb93156450664314",
"risk_tier": "E",
"intent": "Make the MEDIUM model-fleet slot a real optional OpenAI-compatible provider path with protected file credentials, discovery, readiness and gateway registration.",
"requirement_ids": ["WP-200", "WP-301", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["cognitive-worker", "deployment-policy", "tests"], "paths": ["cmd/cognitive-worker/main.go", "cmd/cognitive-worker/main_test.go", "deploy/docker-compose.prod.yml", "tests/cognitive_advisory/cognitive_worker_env_policy.ps1", "tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000168-configurable-medium-fleet-slot.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "adapters/**", ".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": ["existing provider adapter contract", "existing discovery registration", "existing fleet readiness contract", "existing CredentialRef file contract"], "may_change": []},
"semantic_owner": "cognitive-worker/capability registration boundary",
"canonical_state": [],
"state_transitions": ["MEDIUM endpoint absent -> NOT_CONFIGURED", "MEDIUM endpoint configured -> discovery/readiness registration", "available provider -> eligible gateway binding subject to core registry policy"],
"side_effects_and_privileges": ["declares one new deployment configuration surface for an existing read-only secret volume; provider key is resolved only through the existing protected file CredentialRef"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["medium probe has a bounded 10-second client timeout; absent endpoint remains NOT_CONFIGURED; fallback selection remains Lifeboat when preferred tiers are unavailable"],
"assumptions": ["A real medium OpenAI-compatible runtime may be supplied later through deployment environment injection", "No production medium runtime is claimed by this change"],
"acceptance_scenarios": ["empty medium URL produces NOT_CONFIGURED without startup failure", "configured medium URL is registered for discovery and fleet readiness", "direct medium secret environment is rejected", "medium CredentialRef file is accepted", "CPU/GPU/Lifeboat behavior remains unchanged"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "environment policy tests", "CI", "production fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000169",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "8f3920519b6a228d78b6ad775a7c19838dfbb70b",
"risk_tier": "E",
"intent": "Keep an absent optional MEDIUM provider out of runtime readiness while preserving its explicit NOT_CONFIGURED fleet member.",
"requirement_ids": ["WP-200", "WP-301", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000169-optional-medium-readiness.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "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 readiness handler", "existing fleet readiness contract"], "may_change": []},
"semantic_owner": "cognitive-worker",
"canonical_state": [],
"state_transitions": ["MEDIUM absent -> fleet NOT_CONFIGURED without runtime readiness degradation", "MEDIUM configured -> readiness probe included"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["configured MEDIUM remains bounded by its 10-second client; absent MEDIUM is not probed"],
"assumptions": ["Fleet status remains the explicit inventory surface for unconfigured optional tiers"],
"acceptance_scenarios": ["production /readyz stays ready when MEDIUM URL is empty", "fleet readiness retains MEDIUM NOT_CONFIGURED", "configured MEDIUM is included in /readyz"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000170",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "51427a246e1ccc115a6c2640790d9220beca95e9",
"risk_tier": "E",
"intent": "Refresh audit completion and production fleet evidence after the configurable MEDIUM slot and optional readiness fixes were deployed.",
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-200", "WP-301"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "docs/evidence/wp-200-wp-301-fleet-execution-status-2026-08-22-v2.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000170-refresh-fleet-audit-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["live fleet readiness evidence", "audit completion matrix"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["stale evidence -> current production evidence"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["live probe output was captured after production deployment of 51427a24", "missing external capabilities remain explicitly incomplete"],
"acceptance_scenarios": ["production revision is current", "MEDIUM configuration and readiness semantics are recorded", "agent bridge and authenticated UI gaps remain visible", "no secret value appears"],
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000171",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "503fe840741ac99499e06bd93df977acf1baad41",
"risk_tier": "E",
"intent": "Connect cognitive-worker to the already-operated llm-local CPU model runtime as the configured MEDIUM fleet member through an explicit external Docker network.",
"requirement_ids": ["WP-200", "WP-301", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["deployment-topology", "cognitive-worker"], "paths": ["deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000171-connect-existing-medium-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "adapters/**", ".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": ["existing OpenAI-compatible local provider adapter", "existing MEDIUM fleet readiness contract", "existing llm-local Docker network"], "may_change": []},
"semantic_owner": "deployment topology/cognitive-worker registration",
"canonical_state": [],
"state_transitions": ["MEDIUM unconfigured -> live provider discovery/readiness", "preferred HEAVY unavailable -> highest available fallback selection"],
"side_effects_and_privileges": ["one explicit read-only network attachment from cognitive-worker to existing llm-local runtime; no secret or database privilege added"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["if external llm-local network is absent deployment fails closed; provider timeout remains bounded; Lifeboat remains fallback"],
"assumptions": ["production llm-local_default network and llm-local-api are independently operated and present", "llm-local-api exposes OpenAI-compatible /v1/models and chat routes on port 8080"],
"acceptance_scenarios": ["compose config validates with external network declaration", "cognitive-worker resolves llm-local-api", "MEDIUM fleet member is AVAILABLE live", "runtime /readyz remains 200", "Lifeboat fallback remains available", "no secret value is added"],
"required_tests": ["docker compose config", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000172",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "2c7a85779b530e3147fbf47b71742759cc010d58",
"risk_tier": "E",
"intent": "Make fleet failover choose the best available non-Lifeboat tier below the preferred tier before using Lifeboat.",
"requirement_ids": ["WP-301", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["capability", "cognitive-worker"], "paths": ["modules/capability/domain/fleet.go", "modules/capability/domain/fleet_test.go", "cmd/cognitive-worker/fleet_status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000172-fleet-failover-priority.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "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 FleetReadiness contract", "existing FleetTier selection contract"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["preferred unavailable -> next lower ready tier -> Lifeboat fallback"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["selection remains deterministic from observed readiness; invalid preferred tier fails closed"],
"assumptions": ["HEAVY > MEDIUM > FAST is the quality/capability fallback order; LIFEBOAT is last-resort recovery"],
"acceptance_scenarios": ["HEAVY unavailable and MEDIUM ready selects MEDIUM", "HEAVY and MEDIUM unavailable and FAST ready selects FAST", "all primary tiers unavailable selects Lifeboat", "invalid preferred tier is rejected"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production fleet readiness"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000173",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "64642f4baed28f9051efe6a269769c5838b547e4",
"risk_tier": "E",
"intent": "Refresh audit and production evidence after live MEDIUM runtime availability and deterministic fleet failover priority are deployed.",
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-200", "WP-301"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "docs/evidence/wp-200-wp-301-fleet-execution-status-2026-08-22-v3.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000173-refresh-medium-failover-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["live fleet readiness evidence", "audit completion matrix"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["stale evidence -> current MEDIUM/failover evidence"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["live probes were captured after production deployment of 64642f4b", "availability is not interpreted as qualification or activation"],
"acceptance_scenarios": ["production revision is current", "MEDIUM model catalog and readiness are recorded", "preferred HEAVY falls back to MEDIUM", "remaining bridge/GPU/browser gaps remain explicit", "no secret value appears"],
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,32 @@
{
"task_id": "DEV-000174",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "6e0d1a227918775b906e3462406c1505813f51c6",
"risk_tier": "E",
"intent": "Record the bounded production qualification result for the reachable MEDIUM model without changing lifecycle state.",
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-200", "WP-301"],
"allowed_change_scope": {
"modules": [],
"paths": [
"docs/evidence/wp-200-medium-qualification-2026-08-22.json",
"docs/evidence/audit-completion-matrix-2026-08-22-v1.json",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000174-medium-qualification-evidence.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["creator-advisory synthetic benchmark", "production cognitive registry revisions"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["unqualified MEDIUM availability -> recorded qualification result"],
"side_effects_and_privileges": ["read-only registry lookup and one-shot benchmark invocation; no lifecycle mutation"],
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
"failure_retry_restart": ["benchmark timeout is recorded as UNSUPPORTED; no retry changes lifecycle state"],
"assumptions": ["the benchmark binary used the repository cognitive adapter and the production llm-local network", "binding and model revisions were obtained read-only from production registry"],
"acceptance_scenarios": ["all four synthetic cases are recorded", "timeout and unsupported result are explicit", "no canary or activation is performed", "no secret value appears", "audit matrix retains qualification gap"],
"required_tests": ["JSON validation", "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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000175",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "b29f8775e1217506098d5c9a0e99c1046494cba3",
"risk_tier": "E",
"intent": "Make local OpenAI-compatible provider invocation deadlines explicit per provider so slow but reachable fleet members can be qualified through the existing adapter path.",
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
"allowed_change_scope": {"modules": ["cognitive adapter", "cognitive worker", "benchmark runtime"], "paths": ["adapters/models/openai_compatible_local.go", "adapters/models/openai_compatible_local_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-benchmark/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000175-medium-invocation-timeout.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "modules/**", "adapters/postgres/**", ".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 provider-neutral cognitive adapter contract", "existing provider environment configuration"], "may_change": ["provider-specific invocation timeout configuration"]},
"semantic_owner": "cognitive adapter/runtime",
"canonical_state": ["adapter request deadline"],
"state_transitions": ["fixed local adapter deadline -> explicit positive provider-specific deadline"],
"side_effects_and_privileges": ["none beyond existing provider requests"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["non-positive configuration falls back to safe default; request context remains authoritative"],
"assumptions": ["the existing Medium runtime is reachable but may require more than 10 seconds for a complete response", "default CPU behavior remains 10 seconds unless separately configured"],
"acceptance_scenarios": ["adapter accepts positive explicit timeout", "adapter rejects non-positive timeout", "benchmark passes its case timeout to the adapter", "production Medium defaults to 60 seconds", "no provider call bypasses the adapter"],
"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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000176",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "389dd0c23d818e3bde08d8353287938837179da8",
"risk_tier": "E",
"intent": "Record the post-timeout-change production qualification retest for the reachable MEDIUM model.",
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-medium-qualification-2026-08-22-v2.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000176-medium-qualification-retest.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["production cognitive adapter qualification artifact"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["timeout-policy retest -> verified model-generation gap"],
"side_effects_and_privileges": ["one-shot benchmark only; no lifecycle mutation"],
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
"failure_retry_restart": ["45-second case deadline is recorded; unsupported result is not promoted"],
"assumptions": ["retest ran through the updated cognitive adapter in production networks", "no activation or canary was attempted"],
"acceptance_scenarios": ["all four cases record approximately 45-second timeout", "unsupported result remains explicit", "production status retains Medium qualification gap", "no secrets appear"],
"required_tests": ["JSON validation", "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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000177",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "d60fb8697a577b113da2043bddca3b88e9e562fd",
"risk_tier": "E",
"intent": "Record bounded production qualification results for currently readiness-available Heavy model candidates.",
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-heavy-qualification-2026-08-22.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000177-heavy-qualification-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["production Ollama adapter qualification artifact", "fleet readiness evidence"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["Heavy readiness available -> Heavy model candidates qualification recorded"],
"side_effects_and_privileges": ["one-shot benchmark only; no lifecycle mutation"],
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
"failure_retry_restart": ["MODEL_UNAVAILABLE is recorded as unsupported; no activation or promotion"],
"assumptions": ["all four candidates were taken from the production registry read-only", "readiness availability does not imply model invocability"],
"acceptance_scenarios": ["all four Heavy candidates are represented", "MODEL_UNAVAILABLE results are explicit", "audit status distinguishes readiness from qualification", "no secret values appear"],
"required_tests": ["JSON validation", "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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000178",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "dc1ee7ff3c5e284548bf5d9e1e26c9dc9f2c88fd",
"risk_tier": "E",
"intent": "Record refreshed Heavy registry discovery and successful bounded qualification for the currently installed model candidate.",
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-heavy-qualification-2026-08-22-v2.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000178-heavy-qualification-refresh.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["provider discovery endpoint", "Ollama adapter qualification artifact"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["stale Heavy inventory -> current gemma4:26b supported qualification evidence"],
"side_effects_and_privileges": ["provider discovery refresh and one-shot benchmark; no lifecycle promotion"],
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
"failure_retry_restart": ["stale candidates remain historical evidence; current candidate is separately identified by immutable revisions"],
"assumptions": ["discovery was invoked through cognitive-worker's Ollama adapter path", "qualification does not activate an INACTIVE binding"],
"acceptance_scenarios": ["current gemma4:26b revisions are recorded", "all four synthetic cases pass", "stale qwen candidate result is not treated as current", "activation remains explicit and unclaimed", "no secrets appear"],
"required_tests": ["JSON validation", "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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000179",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "5960844fcb7b9bfba9c24e18a5d15b569d0d3a08",
"risk_tier": "E",
"intent": "Synchronize audit projections with current production runtime evidence after the Heavy qualification deployment.",
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-301", "WP-400", "WP-102", "WP-600"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/audit-runtime-status-2026-08-22-v4.json", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000179-runtime-status-consolidation.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["live health/readiness/status endpoints"], "may_change": []},
"semantic_owner": "none; audit projection",
"canonical_state": [],
"state_transitions": ["stale audit projection -> current production runtime evidence"],
"side_effects_and_privileges": ["read-only live probes"],
"persistence_and_migrations": ["immutable JSON evidence only"],
"failure_retry_restart": ["not applicable"],
"assumptions": ["production revision is read from creator-web runtime environment", "runtime readiness is not treated as capability qualification"],
"acceptance_scenarios": ["public health is current", "research/work-graph readiness is current", "agent recovery-only state is explicit", "fleet tiers and selected tier are current", "no secrets appear"],
"required_tests": ["JSON validation", "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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000180",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "2ed2302526f6915c9ddef852ed17a8c47f716cc2",
"risk_tier": "E",
"intent": "Allow the existing synthetic qualification benchmark to bound maximum output bytes per case for slow local providers.",
"requirement_ids": ["WP-200", "AUDIT-COMPLETION-TRUTH"],
"allowed_change_scope": {"modules": ["cognitive benchmark"], "paths": ["cmd/cognitive-benchmark/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000180-bounded-medium-qualification-output.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/cognitive-worker/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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 advisory request contract"], "may_change": ["benchmark CLI parameter only"]},
"semantic_owner": "cognitive benchmark",
"canonical_state": [],
"state_transitions": ["fixed benchmark output cap -> explicit bounded qualification run"],
"side_effects_and_privileges": ["none"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["non-positive output cap rejected; default preserves previous 4096-byte behavior"],
"assumptions": ["bounded output is valid only for synthetic exact-answer corpus and does not alter production provider configuration"],
"acceptance_scenarios": ["default behavior remains unchanged", "positive output cap is forwarded to advisory request", "invalid cap fails closed", "no provider boundary is bypassed"],
"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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000181",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "6f0e099632a79af90eab439b4f026cd31abaad26",
"risk_tier": "E",
"intent": "Record the final bounded Medium qualification retest after adding an explicit benchmark output cap.",
"requirement_ids": ["WP-200", "AUDIT-COMPLETION-TRUTH"],
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-medium-qualification-2026-08-22-v3.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000181-medium-bounded-retest-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".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": ["bounded cognitive benchmark artifact"], "may_change": []},
"semantic_owner": "none; audit evidence projection",
"canonical_state": [],
"state_transitions": ["Medium output-cap hypothesis -> verified runtime performance gap"],
"side_effects_and_privileges": ["one-shot benchmark only; no lifecycle mutation"],
"persistence_and_migrations": ["immutable JSON evidence only"],
"failure_retry_restart": ["60-second case timeout and 256-byte cap are recorded; unsupported result is not promoted"],
"assumptions": ["benchmark used the existing cognitive adapter and current production Medium revisions", "a bounded exact-answer corpus is sufficient to disprove the output-cap hypothesis"],
"acceptance_scenarios": ["all four cases record approximately 60-second timeout", "quality remains 0‰ and critical failure 1000‰", "Medium remains unqualified", "no secret values appear"],
"required_tests": ["JSON validation", "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 -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000182",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "4709ef46076cf5c22522bbfea785fdb3c6220c14",
"risk_tier": "E",
"intent": "Expose an evidence-based model rating and adaptive routing policy so model category follows qualification and observed performance rather than provider or hardware names.",
"requirement_ids": ["WP-200", "WP-601", "adaptive model routing", "CPU-first current operation"],
"allowed_change_scope": {"modules": ["capability", "cognitive-worker"], "paths": ["contracts/capability/cognitive_registry.go", "modules/capability/domain/cognitive_registry.go", "modules/capability/domain/fleet.go", "modules/capability/domain/fleet_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status_test.go", "modules/capability/domain/model_rating_test.go", "docs/MODEL_FLEET_POLICY.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000182-model-rating-and-adaptive-routing.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", ".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": ["EvaluatedCapability", "PerformanceEvidence", "CognitiveRequirement"], "may_change": ["provider-neutral model rating projection"]},
"semantic_owner": "capability",
"canonical_state": ["existing owner-local qualification and performance evidence; rating is a derived projection"],
"state_transitions": ["unrated -> rated only after supported qualification evidence; category is recomputed from current evidence; routing chooses an eligible rated candidate and falls back on invocation failure"],
"side_effects_and_privileges": ["none; no provider invocation or external mutation"],
"persistence_and_migrations": ["no migration; rating is derived and serializable"],
"failure_retry_restart": ["missing or stale evidence yields UNRATED and cannot silently claim quality; existing gateway fallback remains authoritative"],
"assumptions": ["CPU is the current operational choice, but routing remains provider- and hardware-neutral", "discovery never activates a binding"],
"acceptance_scenarios": ["supported evidence produces a visible rating and category", "missing evidence remains UNRATED", "quality objective prefers higher evidence rating", "speed objective prefers lower observed latency", "provider identity alone cannot raise a rating", "CPU is preferred now but all ready capability classes remain valid failover candidates"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000184",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "16a0e8c4924a2e644294e12f048e747c00f940b9",
"risk_tier": "E",
"intent": "Connect an optional OpenAI-compatible GPU provider through protected Domovoy storage without literal provider credentials or hardwired environment configuration, while preserving CPU/Lifeboat degraded operation.",
"requirement_ids": ["GPU provider endpoint is authenticated through protected storage", "GPU absence or invalid configuration does not stop cognitive-worker", "fleet readiness and discovery use OpenAI-compatible /v1/models", "CPU and Lifeboat remain available fallback paths"],
"allowed_change_scope": {"modules": ["cognitive-worker", "deployment"], "paths": ["cmd/cognitive-worker/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000184-gpu-openai-protected-provider.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "contracts/**", "modules/**", "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": ["protected Domovoy secret files", "OpenAI-compatible /v1/models and /v1/chat/completions", "existing cognitive gateway and fleet readiness contracts"], "may_change": []},
"semantic_owner": "capability provider adapter projection",
"canonical_state": [],
"state_transitions": ["missing/invalid protected GPU config -> provider omitted and degraded readiness", "valid config -> authenticated discovery/readiness and optional heavy-tier candidate"],
"side_effects_and_privileges": ["outbound authenticated requests to the explicitly configured GPU endpoint; no credentials forwarded to Core or user responses"],
"persistence_and_migrations": ["no schema changes; protected file storage only"],
"failure_retry_restart": ["GPU timeout/unavailability falls back through existing router to CPU/Lifeboat", "restart reloads protected files", "invalid config disables only optional GPU provider"],
"assumptions": ["the supplied endpoint implements OpenAI-compatible /v1/models and /v1/chat/completions", "the protected secret volume is mounted read-only into cognitive-worker", "docker-test remains prohibited"],
"acceptance_scenarios": ["GPU /v1/models succeeds with protected key", "GPU key never appears in env/logs/responses", "GPU discovery creates provider-neutral records", "fleet readiness reports GPU heavy tier only when configured", "stopping GPU leaves CPU/Lifeboat ready", "chat remains available without GPU files"],
"required_tests": ["go test ./cmd/cognitive-worker ./adapters/models", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000184-gpu-openai-protected-provider.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000184-gpu-openai-protected-provider.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "CI", "production protected-key models/readiness/live fallback checks"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000185",
"task_revision": 1,
"architecture_revision": "ADR-0017",
"base_repo_revision": "7ab4b02afa78d5c65eb1545bfd6e117cc1cdfad7",
"risk_tier": "E",
"intent": "Load the optional external Agent Execution bridge endpoint from Domovoy protected storage and preserve recovery-only operation when it is absent or invalid.",
"requirement_ids": ["protected bridge configuration", "degraded recovery without bridge", "durable dispatch when bridge is configured"],
"allowed_change_scope": {"modules": ["agent-worker", "deployment"], "paths": ["cmd/agent-worker/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000185-agent-bridge-protected-config.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "contracts/**", "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": ["existing agentbridge HTTP executor", "protected runtime configuration files", "existing work dispatch contract"], "may_change": []},
"semantic_owner": "work agent execution adapter",
"canonical_state": [],
"state_transitions": ["missing/invalid protected bridge URL -> recovery-only", "valid protected bridge URL -> execution mode"],
"side_effects_and_privileges": ["outbound HTTP calls only to protected configured bridge endpoint"],
"persistence_and_migrations": ["no schema changes; protected file storage only"],
"failure_retry_restart": ["bridge errors remain handled by existing worker cycle", "restart reloads protected URL", "missing bridge never prevents recovery"],
"assumptions": ["protected secret volume is mounted read-only", "bridge URL file is deployment-owned"],
"acceptance_scenarios": ["protected URL enables execution mode", "missing URL keeps recovery-only mode", "URL is not read from environment", "URL value never appears in status"],
"required_tests": ["go test ./cmd/agent-worker/...", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000185-agent-bridge-protected-config.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000185-agent-bridge-protected-config.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI", "production status and recovery checks"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}

Some files were not shown because too many files have changed in this diff Show More