@sema-agent/server 1.322.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +59 -3
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -106,6 +106,8 @@ import { WorkflowAgentRegistry } from "./orchestration/workflow-agent-steer.js";
|
|
|
106
106
|
import { SubagentSteerRegistry } from "./orchestration/subagent-steer.js";
|
|
107
107
|
import { FleetEventBus, fleetBackgroundChildPublisher } from "./fleet/fleet-bus.js";
|
|
108
108
|
import { createKeyResolver } from "./key-resolver.js";
|
|
109
|
+
/** Service version for GET /v1/capabilities — read from the package.json shipped next to src/ in the
|
|
110
|
+
* image (Dockerfile COPYs it). Best-effort: an unreadable file reports "unknown", never crashes boot. */
|
|
109
111
|
function serviceVersion() {
|
|
110
112
|
try {
|
|
111
113
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -115,37 +117,100 @@ function serviceVersion() {
|
|
|
115
117
|
return "unknown";
|
|
116
118
|
}
|
|
117
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Service entrypoint. Wires the three planes:
|
|
122
|
+
* Brain = external LLM gateway (createBrain)
|
|
123
|
+
* Session = pluggable store (TiDB durable event-log center, or in-memory)
|
|
124
|
+
* Hands = tools/MCP injected per request in resolveSpec (deployment-owned)
|
|
125
|
+
* Persistence (L1 sessions + L2 memory) lives in the external TiDB center; this container is stateless.
|
|
126
|
+
*/
|
|
118
127
|
async function main() {
|
|
119
128
|
const config = loadConfig();
|
|
129
|
+
// Liveness invariant: the running instance refreshes updated_at every HEARTBEAT_MS; the reaper fails
|
|
130
|
+
// a run whose updated_at is older than runStaleSec. If the stale window were ≤ the heartbeat, the
|
|
131
|
+
// reaper would race a perfectly alive run. Fail fast on a misconfiguration instead of reaping live work.
|
|
120
132
|
if (config.runStaleSec * 1000 <= HEARTBEAT_MS) {
|
|
121
133
|
throw new Error(`REAP_RUN_STALE_SEC (${config.runStaleSec}s) must exceed the ${HEARTBEAT_MS / 1000}s liveness heartbeat`);
|
|
122
134
|
}
|
|
123
135
|
const logger = createLogger(config.logLevel);
|
|
124
136
|
const metrics = createMetrics();
|
|
137
|
+
// S20 (SILENT-FALLBACK P1) + [792]④ boot notices: config parsing runs before the logger exists — drain both
|
|
138
|
+
// pre-logger collectors via the shared helper (F10: run-local uses the same one, so entrypoints can't drift).
|
|
125
139
|
logConfigDiagnostics(logger);
|
|
140
|
+
// design/158 B4 交接小件:plugins 数值旋钮(HOST_BG_*/K8S_BG_*/E2B_*,模块级 import 时读)的非法
|
|
141
|
+
// 字面量告警——通道在 remote-shell.ts 自带(不造 plugins→config 依赖边),与上面同一 boot 诊断点排空。
|
|
126
142
|
for (const w of drainNumEnvWarnings())
|
|
127
143
|
logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
|
|
144
|
+
// M1(接入审计 2026-07-08):core verify 契约=verifier 必须异于 implementer(去相关性是 🔴 部署契约)。
|
|
145
|
+
// verifier 角色落 "default" 时对抗验证者=实现者自评自批——比无验证更糟(假安全感)。曝光而非静默:
|
|
146
|
+
// 单模型部署配 MODEL_VERIFIER 指异构模型即消警(typo/不在 catalog 同样落到这里)。
|
|
128
147
|
if ((config.roles?.verifier ?? "default") === "default") {
|
|
129
148
|
logger.warn("verify_decorrelation_unavailable", {
|
|
130
149
|
hint: "verifier role = default (= implementer) — adversarial verification grades its own work; set MODEL_VERIFIER=<catalog model id> to a heterogeneous model",
|
|
131
150
|
...(process.env.MODEL_VERIFIER ? { rejected: process.env.MODEL_VERIFIER, reason: "not in catalog" } : {}),
|
|
132
151
|
});
|
|
133
152
|
}
|
|
153
|
+
// S24: redaction rewrites by pattern; LOW: web-search non-array payloads by provider.
|
|
134
154
|
setRedactionObserver((pattern, n) => metrics.inc("redactions_applied_total", { pattern }, n));
|
|
135
155
|
setWebSearchBadPayloadObserver((provider) => {
|
|
136
156
|
metrics.inc("web_search_bad_payload_total", { provider });
|
|
137
157
|
logger.warn("web_search_non_array_payload", { provider });
|
|
138
158
|
});
|
|
159
|
+
// Sema registry (sema-registry): pull the effective config on boot and apply it OVER the env
|
|
160
|
+
// defaults — env is the fallback. Unset or unreachable → keep the env-derived config. Applied BEFORE
|
|
161
|
+
// the Runner is built so models/roles/teams take effect. (Logical config only; secrets stay in env.)
|
|
139
162
|
let ccEtag;
|
|
163
|
+
// Kept past the fetch so the deferred skills/mcp apply (which needs the live `skills` array + async
|
|
164
|
+
// content lazy-pull) can run right before buildScenarios, after applyEffective handled models/roles/teams.
|
|
165
|
+
// `effective` is the BOOT snapshot — the refresh loop never reassigns it, so restartReasons() compares each
|
|
166
|
+
// pull against what THIS process baked in (change-detection, not presence → no auto-restart loop).
|
|
140
167
|
let effective;
|
|
168
|
+
// 提示词装配协议 §9.5-2 three-store split: `effective` above = the boot snapshot (diff baseline);
|
|
169
|
+
// `latestEffective` = the last candidate that passed validation AND was applied — the explicit
|
|
170
|
+
// last-known-good (LKG). A refresh candidate failing validation is REJECTED whole (§9.5-5): never
|
|
171
|
+
// applied, so the live config keeps serving this LKG instead of a domain falling to schema-default/
|
|
172
|
+
// empty. The third store (promptCandidate — a staged prompts-domain artifact awaiting activation)
|
|
173
|
+
// lands with the prompts domain itself; `lastRejectedCandidate` is its observability forerunner and
|
|
174
|
+
// the prompts-domain validation (core S1 schema) hooks into the same gate.
|
|
141
175
|
let latestEffective;
|
|
142
176
|
let lastRejectedCandidate;
|
|
177
|
+
// codex R10 (models-tiers 窗收口): the last EffectiveConfig whose MODEL PLANE (models/roles/tiers/default) was
|
|
178
|
+
// actually applied to the live config — undefined = env plane (no center plane applied yet). When the Runner is
|
|
179
|
+
// tier-frozen, a refresh whose plane differs from THIS is deferred to restart (applyEffective deferModelPlane),
|
|
180
|
+
// keeping admission and the Runner on one catalog generation through the restart window.
|
|
143
181
|
let appliedPlaneEff;
|
|
182
|
+
// codex R18: the persistent "deferred WITHOUT next-boot handoff" state — the one-shot warn is not enough
|
|
183
|
+
// (the etag advances → later ticks 304 and never re-enter the defer branch). Set when a plane defers with no
|
|
184
|
+
// restart-surviving LKG; cleared when a plane finally applies or a forced restart reason ships. Surfaced on
|
|
185
|
+
// /health (modelPlaneDeferred) + re-warned every refresh tick (incl. 304s).
|
|
144
186
|
let planeDeferredNoHandoff;
|
|
187
|
+
// codex R35: STICKY live-skill application debt — the boot-baked skill table never rebuilds inside this
|
|
188
|
+
// process, so once the live application is known incomplete the debt survives every refresh (a
|
|
189
|
+
// same-generation 200 recomputes reasons vs boot as [] and would otherwise silently clear the signal).
|
|
190
|
+
// Set at the boot writer (R34 arms); folded into every tick's reasons; only a restart resolves it.
|
|
145
191
|
let bootSkillStale = false;
|
|
192
|
+
// Sema-registry restart signal: set by the refresh loop when a pull's restart-to-apply slices
|
|
193
|
+
// (skills/mcp/scenarios/runtime-gates) differ from boot; read by GET /health for orchestrator auto-restart.
|
|
146
194
|
let pendingRestart;
|
|
195
|
+
// DUAL-MODE-DESIGN §4: the config SOURCE is a backend-selectable seam — remote (sema-registry HTTP, the fleet
|
|
196
|
+
// default) | local (.env + config.d via @sema-agent/registry-core FileConfigStore, the single-machine/TOC source).
|
|
197
|
+
// The provider exists when SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL) is set OR CONFIG_PROVIDER=local is explicit; neither = pure-env
|
|
198
|
+
// (byte-identical to before). The remote provider DELEGATES verbatim to fetchEffective, so the fleet path is
|
|
199
|
+
// unchanged; applyEffective consumes either source's EffectiveConfig identically (one resolver → no drift).
|
|
147
200
|
const cc = config.configCenter;
|
|
201
|
+
// 🔴 ONE local root for BOTH the config provider (models/skills/mcp) AND the remoteExec loader (avoids a
|
|
202
|
+
// split-brain — createConfigProvider defaulted to process.cwd() while loadRemoteExec used
|
|
203
|
+
// AGENT_DATA_DIR, so config.d and remote-exec.json could resolve to DIFFERENT directories). run-local already
|
|
204
|
+
// computes this once; main.ts now does too. Used below for loadRemoteExec.
|
|
148
205
|
const localRoot = config.configLocalDir ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
206
|
+
// Sealed-box key custody: ensure this host's X25519 keypair(s) under
|
|
207
|
+
// <localDataRoot>/sealed-key/ (dir 0700, files 0600; SEVERAL may coexist — rotation keeps old private
|
|
208
|
+
// keys decrypting old ciphertexts) BEFORE the first effective pull, so a roster carrying
|
|
209
|
+
// `sealedApiKey` unseals on the boot apply. The public-key report is best-effort fire-and-forget:
|
|
210
|
+
// the center's execution-keys face is admin-gated and a worker only holds the pull token, so without
|
|
211
|
+
// an operator-provided SEMA_REGISTRY_ADMIN_TOKEN it prints the full public key for manual
|
|
212
|
+
// registration instead (honest posture — see reportExecutionPublicKey). Store failure is non-fatal:
|
|
213
|
+
// sealed models then warn per-model in applyEffective (fail-loud, never a silent apiKeyEnv fallback).
|
|
149
214
|
let sealedKeys;
|
|
150
215
|
try {
|
|
151
216
|
sealedKeys = await ensureSealedKeyStore(join(config.localDataRoot ?? localRoot, "sealed-key"), logger);
|
|
@@ -167,6 +232,9 @@ async function main() {
|
|
|
167
232
|
...(config.configProvider === "local" ? { localDir: localRoot } : {}),
|
|
168
233
|
})
|
|
169
234
|
: undefined;
|
|
235
|
+
// boot ready 门 latch:仅「远程 registry 部署(非 dryRun)+ 无显式 env MODEL_ID」时等 roster——
|
|
236
|
+
// 首次 effective 落 ≥1 enabled 模型即翻 true(one-way)。其余姿势(env 模型在/local provider/dryRun/纯 env)
|
|
237
|
+
// 恒 ready=现有部署零影响。E3 废除 workers.model 后,fleet worker 的占位模型窗口由此门 fail-closed。
|
|
170
238
|
const modelReadyState = { ready: !(cc && !cc.dryRun) || Boolean(process.env.MODEL_ID) };
|
|
171
239
|
const markRosterLanded = (eff) => {
|
|
172
240
|
if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
|
|
@@ -174,7 +242,14 @@ async function main() {
|
|
|
174
242
|
logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
|
|
175
243
|
}
|
|
176
244
|
};
|
|
245
|
+
// center prompts ref([1057]/[1062] 投影腿):**热采用,新任务边界生效**——core 1.316([1068] 裁 a/
|
|
246
|
+
// [1070]d)把 provider 声明纳入 epoch artifactDigest:声明变=digest 变=下任务边界自动重 pin,进行中
|
|
247
|
+
// 会话由 session epoch pin 保稳,resume 记账诚实(1.222 的 restart-to-apply 是 core 件落地前的正确
|
|
248
|
+
// 过渡姿势,[1068]① 官方确认后随 1.316 采用降级为热生效——与 center「发布=新任务生效」产品语义对齐)。
|
|
249
|
+
// 判形通过才采用(形校验=防 core-assemble-THROW 底线,非语义复检 [1057]①f);坏形=warn+沿用旧包。
|
|
177
250
|
let centerPrompts;
|
|
251
|
+
/** codex-3 F1:一次失败的 catalog 过渡的目标面(undefined 目标=欠 disable)。etag 已推进时 provider
|
|
252
|
+
* 304 短路 adopt——本 marker 让每个 refresh 拍(含 304 拍)独立重试,直到过渡落地。 */
|
|
178
253
|
let pendingCatalogFaces;
|
|
179
254
|
const retryPendingCatalog = async (phase) => {
|
|
180
255
|
if (!pendingCatalogFaces)
|
|
@@ -185,8 +260,25 @@ async function main() {
|
|
|
185
260
|
pendingCatalogFaces = undefined;
|
|
186
261
|
}
|
|
187
262
|
};
|
|
263
|
+
// e/b(catalog 轴消费面,[1236]① seam):core 的 CenterPromptSource——server 只给根路径,core 文件形
|
|
264
|
+
// 自管 `<root>/prompt-epoch/{artifacts,source-state.json}`;boot restore=磁盘 LKG(离线重启 candidate
|
|
265
|
+
// 存续/disabled 不复活,状态机全在 core)。RunnerDeps.promptSource 挂它;adopt 时经 applyCatalogToSource
|
|
266
|
+
// 推进两态过渡。restore 失败=warn(candidate 缺席,catalog 轴等下一次 adopt;declaration 轴/内置照跑)。
|
|
188
267
|
const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
|
|
268
|
+
// codex-3 F2:effective 拉取是 worker 轴的(config LKG 同理 worker-keyed)——同根多 worker 进程共享
|
|
269
|
+
// content-addressed artifacts/(by-digest,天然安全),但 **candidate 状态必须按 worker 分箱**:共写一份
|
|
270
|
+
// source-state.json 会让 A 的撤下压掉 B 的 candidate、restart 恢复到别人 worker 的 catalog(错 worker
|
|
271
|
+
// 的系统提示词跑任务)。encodeURIComponent 消毒(worker 名进路径)。
|
|
272
|
+
// codex-5:无撞形分箱——缺席=字面 "global",配置了 worker= "worker-<sha256 前 16>"(哈希后天然单
|
|
273
|
+
// 段/无 dot-segment/与 "global" 不可能同名;worker 名任意字节也安全)。
|
|
189
274
|
const promptStateDir = join(promptEpochDir, "state", cc?.worker === undefined ? "global" : `worker-${createHash("sha256").update(cc.worker).digest("hex").slice(0, 16)}`);
|
|
275
|
+
// codex-6 F1:File 店构造同步 mkdir——只读/不可写数据根会把「可选的 catalog 特性」变成整服务 boot
|
|
276
|
+
// 失败。守护工厂:文件形失败 ⇒ warn + Memory 店兜底(candidate 不跨重启持久,catalog 采用仍工作;
|
|
277
|
+
// 内置/declaration 轴完全不受影响)。
|
|
278
|
+
// [985]b / [1285] — §9.4 by-digest 补拉组合店:local miss(本副本从未采用过该 candidate / 盘失)⇒
|
|
279
|
+
// 从 center 按 digest 补拉 envelope ⇒ core verifyPromptArtifact 全管线(canonical 复算;**且回验
|
|
280
|
+
// 「拿到的=要的」digest 逐位**,center 答非所问=丢弃)⇒ best-effort 回写盘(下次直命中)。pinned
|
|
281
|
+
// resume 的 fail-loud miss 自此只剩「center 也没有」的真缺席。仅 remote 治理形(cc.baseUrl)挂补拉。
|
|
190
282
|
const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc.baseUrl, cc.token, d), logger) : inner;
|
|
191
283
|
const promptSource = (() => {
|
|
192
284
|
try {
|
|
@@ -201,6 +293,10 @@ async function main() {
|
|
|
201
293
|
return new CenterPromptSource({ store: withByDigestBackfill(new MemoryPromptArtifactStore()), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
|
|
202
294
|
}
|
|
203
295
|
})();
|
|
296
|
+
// codex-2 F4:只有「有配置源在治理且非 dry-run」才 restore 磁盘 candidate——provider-off/dry-run 姿势下
|
|
297
|
+
// 旧部署遗留的 active candidate 不得继续影响新任务(publish-gate-off/compare-only 语义)。不 restore 只
|
|
298
|
+
// 影响 candidate 面;pinned resume 的 by-digest 解析走 FilePromptArtifactStore.get(不依赖 restore),
|
|
299
|
+
// 旧会话恢复不受损。
|
|
204
300
|
if (configProvider && !cc?.dryRun) {
|
|
205
301
|
try {
|
|
206
302
|
await promptSource.restore();
|
|
@@ -212,6 +308,8 @@ async function main() {
|
|
|
212
308
|
const adoptCenterPrompts = async (eff, phase) => {
|
|
213
309
|
const raw = eff?.prompts;
|
|
214
310
|
if (raw === undefined) {
|
|
311
|
+
// fail-closed 语义([1057]①d):publish 闸关=key 整体不出现 ⇒ 收敛回内置提示词(不是「保持旧包」——
|
|
312
|
+
// 闸关是显式撤下,沿用旧包会让「关闸」失效)。
|
|
215
313
|
if (centerPrompts)
|
|
216
314
|
logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
|
|
217
315
|
const cleared = await applyCatalogToSource(promptSource, undefined, centerPrompts, logger, phase);
|
|
@@ -220,7 +318,7 @@ async function main() {
|
|
|
220
318
|
pendingCatalogFaces = undefined;
|
|
221
319
|
}
|
|
222
320
|
else {
|
|
223
|
-
pendingCatalogFaces = { target: undefined };
|
|
321
|
+
pendingCatalogFaces = { target: undefined }; // failed disable — retried by the refresh hook (codex-3 F1)
|
|
224
322
|
}
|
|
225
323
|
return;
|
|
226
324
|
}
|
|
@@ -230,6 +328,7 @@ async function main() {
|
|
|
230
328
|
return;
|
|
231
329
|
}
|
|
232
330
|
if (centerPrompts?.identity !== v.value.identity) {
|
|
331
|
+
// [985]a §3:axes 观测两轴共存窗;declaration 轴字段照旧,catalog 轴报 artifactDigest(内容身份)。
|
|
233
332
|
logger.info("center_prompts_adopted", {
|
|
234
333
|
phase,
|
|
235
334
|
axes: v.value.axes,
|
|
@@ -241,6 +340,9 @@ async function main() {
|
|
|
241
340
|
});
|
|
242
341
|
}
|
|
243
342
|
const transitioned = await applyCatalogToSource(promptSource, v.value, centerPrompts, logger, phase);
|
|
343
|
+
// codex-2 F2 + codex-3 F1:catalog 过渡失败 ⇒ 身份不推进(centerPrompts 保持 prev),且目标面记入
|
|
344
|
+
// pendingCatalogFaces——etag 会照常推进(其他域的采用不为 prompts 的盘错扣住),此后 provider 304
|
|
345
|
+
// 不再进本函数,重试由 refresh 拍上的 retryPendingCatalog 钩独立驱动(见 refresh 循环)。
|
|
244
346
|
if (transitioned) {
|
|
245
347
|
centerPrompts = v.value;
|
|
246
348
|
pendingCatalogFaces = undefined;
|
|
@@ -249,15 +351,57 @@ async function main() {
|
|
|
249
351
|
pendingCatalogFaces = { target: v.value };
|
|
250
352
|
}
|
|
251
353
|
};
|
|
354
|
+
// Boot-deferred pull (clay 2026-07-17): when the boot fetch outlives its budget, the SAME promise is
|
|
355
|
+
// stashed here and, once the refresh tick exists (defined further down), chained to trigger an immediate
|
|
356
|
+
// hot-apply on arrival — the server meanwhile starts on the env fallback in <1s instead of hanging up to
|
|
357
|
+
// ~8s on a black-holed center (真机实测 5.7s = the "local server takes 5s" report).
|
|
252
358
|
let bootConfigPending;
|
|
359
|
+
// LKG 落盘(clay 拍 2026-07-18;config-lkg.ts 顶注=全案由):remote lane 的干净候选持久化——boot 的
|
|
360
|
+
// 第三级来源(中心 > 盘上 LKG > env)。`lkgBooted` 标志决定 deferred continuation 的形:LKG 起服=有
|
|
361
|
+
// 完整 boot 基线 → 到货走普通 refreshTick(候选门+hot+restartReasons 对 LKG 比较,通常零差异=无环);
|
|
362
|
+
// 无 LKG=env 起服 → 到货走「迟到 boot」路径(deferredBootApply)。
|
|
253
363
|
const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
|
|
254
364
|
const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
|
|
365
|
+
// codex R17/R20: a successful LOCAL write proves nothing about the NEXT instance seeing it — a stateless pod
|
|
366
|
+
// replaces its filesystem on restart, so treating a pod-local rename as "handoff confirmed" re-opens the R16
|
|
367
|
+
// rolling-restart storm. Next-instance visibility is a DEPLOYMENT contract only the operator can assert, and
|
|
368
|
+
// ONLY via the explicit knob: CONFIG_LKG_DURABLE=true declares the LKG path rides restart-surviving storage
|
|
369
|
+
// (host disk / PVC / shared volume). CONFIG_LKG_PATH is strictly a LOCATION override (R20: it can point at an
|
|
370
|
+
// ephemeral container path — a location says nothing about durability). Unset ⇒ the models-tiers restart
|
|
371
|
+
// reason stays suppressed on a deferred plane (old generation keeps serving; /health modelPlaneDeferred +
|
|
372
|
+
// per-tick warn carry the state) while normal LKG writes continue (they still help same-instance restarts).
|
|
255
373
|
const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
|
|
256
374
|
let lkgBooted = false;
|
|
257
|
-
let bootLkgCandidate;
|
|
375
|
+
let bootLkgCandidate; // F7:skills 装载后才落盘
|
|
376
|
+
// F1+F2(LKG 复审):awaited 落盘 + skill 正文预热——refresh 只落 manifest 的话,重启后 LKG-boot 在
|
|
377
|
+
// 中心不可达时新 skill 正文缓存缺失=软跳过(静默缺 skill)。预热=枚举 enabled manifest,把缺盘的正文
|
|
378
|
+
// 拉齐进 content-addressed 缓存(失败=warn 软降级,与 boot 的 skill_failed 同语义等级——skill 拉取
|
|
379
|
+
// 失败从不升级为配置拒绝);await 保证 restart 信号可见前 LKG+正文已落盘。
|
|
380
|
+
/** Returns TRUE iff the candidate is durably persisted for the NEXT boot (codex R16: the FORCED models-tiers
|
|
381
|
+
* restart reason must never fire without a confirmed handoff — LKG disabled/unwritable + over-budget boot
|
|
382
|
+
* pulls would otherwise turn the deferral into a rolling-restart storm: every restarted process boots the env
|
|
383
|
+
* plane again, defers again, re-signals). Change-DETECTED reasons still ride regardless (their loop is the
|
|
384
|
+
* pre-existing, documented no-LKG residual). */
|
|
385
|
+
// codex R37 loop-breaker proof: TRUE iff the LAST persistLkgDurable call hash-verified EVERY enabled skill
|
|
386
|
+
// body on disk. This is the "a restart can actually repair the live skill table" proof the NON-durable lane
|
|
387
|
+
// requires before publishing a skills restart — a successful manifest pull only proves the CONFIG endpoint
|
|
388
|
+
// works, not the SEPARATE body fetches; signaling on manifest-success alone lets a persistent body failure
|
|
389
|
+
// (404 / hash mismatch / timeout) restart-storm a stateless deployment (every replacement process fails the
|
|
390
|
+
// same body, signals again, is restarted again). With the proof, a storm cannot form: a boot that fails the
|
|
391
|
+
// body lacks the proof and only retains visible debt (no signal) until a later verify succeeds.
|
|
258
392
|
let lastPrewarmBodiesVerified = false;
|
|
259
393
|
const persistLkgDurable = async (effectiveObj, etag) => {
|
|
260
394
|
lastPrewarmBodiesVerified = false;
|
|
395
|
+
// codex R21: the LOCAL provider's candidate IS the next-boot source (config.d on the same disk boot reads
|
|
396
|
+
// BEFORE Runner construction) — the handoff is inherent, no LKG needed. Without this arm a polled config.d
|
|
397
|
+
// edit that defers the plane (tier activation / tier-frozen change) could never authorize its own restart
|
|
398
|
+
// (lkgEnabled is remote-only, so even CONFIG_LKG_DURABLE couldn't help) — stuck in modelPlaneDeferred forever.
|
|
399
|
+
// codex R28 requalified: this TRUE covers the MODEL PLANE (what the deferral gate adjudicates). Skills are
|
|
400
|
+
// NOT part of the local handoff because the local lane does not consume a center-style skills domain AT ALL
|
|
401
|
+
// yet — the only boot consumer (applyCenterSkills) requires config.configCenter (remote HTTP), absent on
|
|
402
|
+
// CONFIG_PROVIDER=local; bodies live inline in config.d (LocalConfigProvider.fetchSkillContent) but no boot
|
|
403
|
+
// path reads them. Pre-existing local-lane gap (predates this train, nothing lost across restart that was
|
|
404
|
+
// ever applied); wiring boot skills through the ConfigProvider seam is the tracked follow-up (board).
|
|
261
405
|
if (configProvider?.kind === "local")
|
|
262
406
|
return true;
|
|
263
407
|
if (!lkgEnabled)
|
|
@@ -266,6 +410,15 @@ async function main() {
|
|
|
266
410
|
if (effectiveObj.skills && config.configCenter) {
|
|
267
411
|
await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
|
|
268
412
|
}
|
|
413
|
+
// codex R25/R26: skill BODIES are part of the handoff contract, and completeness gates PUBLICATION —
|
|
414
|
+
// applyCenterSkills swallows fetch/cache-write failures, so awaiting it proves nothing; verify every
|
|
415
|
+
// ENABLED manifest hash on disk (content-true) BEFORE the atomic LKG replace (R26: verifying after left
|
|
416
|
+
// an incomplete candidate as the boot-visible LKG — any crash/manual restart with the center down then
|
|
417
|
+
// boots that manifest, silently skips the uncached skills, and never re-signals because the manifest is
|
|
418
|
+
// the new boot baseline). On any miss/corruption the PREVIOUS good LKG stays in place; the per-tick
|
|
419
|
+
// retained-candidate retry re-attempts prewarm + verify + publish.
|
|
420
|
+
// Residual seam (honest, partial): the skill cache roots at SEMA_CONFIG_DIR while lkgPath can point
|
|
421
|
+
// elsewhere — CONFIG_LKG_DURABLE's contract requires BOTH on restart-surviving storage.
|
|
269
422
|
const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
|
|
270
423
|
const cacheDir = defaultSkillCacheDir();
|
|
271
424
|
for (const m of enabledSkills) {
|
|
@@ -284,8 +437,11 @@ async function main() {
|
|
|
284
437
|
return false;
|
|
285
438
|
}
|
|
286
439
|
}
|
|
287
|
-
lastPrewarmBodiesVerified = true;
|
|
440
|
+
lastPrewarmBodiesVerified = true; // every enabled body verified on disk — the repair proof stands even if the LKG write below fails
|
|
288
441
|
await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
|
|
442
|
+
// codex R17: TRUE = next-BOOT visibility, not merely a successful local rename — gated on the operator's
|
|
443
|
+
// restart-surviving-storage declaration (lkgSurvivesRestart above). A stateless pod's local write still
|
|
444
|
+
// helps same-instance restarts but must not authorize a forced restart.
|
|
289
445
|
return lkgSurvivesRestart;
|
|
290
446
|
}
|
|
291
447
|
catch (err) {
|
|
@@ -294,7 +450,9 @@ async function main() {
|
|
|
294
450
|
}
|
|
295
451
|
};
|
|
296
452
|
if (configProvider) {
|
|
297
|
-
const dryRun = cc?.dryRun ?? false;
|
|
453
|
+
const dryRun = cc?.dryRun ?? false; // dry-run posture is remote-only (a local source has nothing to compare against)
|
|
454
|
+
// LKG 读腿(boot 超窗与硬失败两个入口共用):盘上有验证过的干净候选 → 当 boot 配置整套应用
|
|
455
|
+
// (skills/mcp 进 buildScenarios——这正是 env 兜底给不了的),etag 落位 → refresh 首拍 304 或正常增量。
|
|
298
456
|
const bootFromLkg = async (why) => {
|
|
299
457
|
if (!lkgEnabled || dryRun)
|
|
300
458
|
return null;
|
|
@@ -312,7 +470,7 @@ async function main() {
|
|
|
312
470
|
await adoptCenterPrompts(lkgR.effective, "boot-lkg");
|
|
313
471
|
markRosterLanded(lkgR.effective);
|
|
314
472
|
effective = lkgR.effective;
|
|
315
|
-
appliedPlaneEff = lkgR.effective;
|
|
473
|
+
appliedPlaneEff = lkgR.effective; // model plane applied pre-Runner — this IS the Runner's generation
|
|
316
474
|
latestEffective = lkgR.effective;
|
|
317
475
|
if (lkgR.etag !== undefined)
|
|
318
476
|
ccEtag = lkgR.etag;
|
|
@@ -325,7 +483,7 @@ async function main() {
|
|
|
325
483
|
let lkgFallback = null;
|
|
326
484
|
if (raced === BOOT_FETCH_DEFERRED) {
|
|
327
485
|
bootConfigPending = bootFetch;
|
|
328
|
-
bootFetch.catch(() => { });
|
|
486
|
+
bootFetch.catch(() => { }); // settled in the background — failure handling lives on the refresh cadence
|
|
329
487
|
lkgFallback = await bootFromLkg("boot fetch deferred (budget exceeded)");
|
|
330
488
|
lkgBooted = lkgFallback !== null;
|
|
331
489
|
logger.warn("config_boot_fetch_deferred", {
|
|
@@ -337,6 +495,9 @@ async function main() {
|
|
|
337
495
|
: "center slow/unreachable and no usable LKG — serving on the env fallback; the pull continues in the background (late-boot adoption on arrival). Raise CONFIG_BOOT_FETCH_BUDGET_MS to block boot instead.",
|
|
338
496
|
});
|
|
339
497
|
}
|
|
498
|
+
// F4(LKG 复审):LKG 载荷的 apply 用专属 catch——走主分支会让 applyEffective 半程炸落到外层
|
|
499
|
+
// catch 的「config_source_unreachable_using_env」误导日志(中心明明只是慢)。坏 LKG=点名+env 兜底
|
|
500
|
+
// (in-place mutate 的混态风险与 live 坏载荷同级,浅域判形在 loadLkg 已挡结构性坏形)。
|
|
340
501
|
if (raced === BOOT_FETCH_DEFERRED && lkgFallback) {
|
|
341
502
|
try {
|
|
342
503
|
await bootApplyLkgInline(lkgFallback);
|
|
@@ -348,26 +509,41 @@ async function main() {
|
|
|
348
509
|
}
|
|
349
510
|
const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
|
|
350
511
|
if (r) {
|
|
512
|
+
// [898] tolerant read(registry-core 0.10.12,local lane):catalog 域坏文件不再连坐整包回落 env——
|
|
513
|
+
// 坏域按 default 落+per-domain warn 点名(文件路径在 error 文案里),好域照常生效。gate 域坏文件
|
|
514
|
+
// tolerant 仍 throw(READ 容错不放宽 gate fail-loud)⇒ 走下面既有整包 catch。
|
|
515
|
+
// §9.5-5 boot 与 refresh 的分工(codex F1:重启不得洗白坏候选):boot 照旧 tolerant APPLY(无 LKG
|
|
516
|
+
// 可保,带着好域起服务),但一个带错误的候选 ①不推进 ccEtag——refresh 首 poll 拿到同一棵树,候选门
|
|
517
|
+
// 当场整拒并持续 warn 到修复;②不写入 latestEffective——LKG 永远只指向完全干净的候选,拒绝日志的
|
|
518
|
+
// lkgVersion 也诚实(boot 带错=「(env)」)。markRosterLanded 保留:models 域自身干净才可能开门
|
|
519
|
+
// (坏 models 域 tolerant 落空=enabled 0=门不动),其他域的错误不构成扣住就绪门的理由。
|
|
351
520
|
const bootPromptsRaw = r.effective.prompts;
|
|
352
521
|
const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
|
|
522
|
+
// 坏 prompts 面同 domainErrors 计入 not-clean(F6 boot 半场):etag 不进 → refresh 首拍同树整拒持续
|
|
523
|
+
// warn;LKG 不指向带坏 prompts 的候选。adoptCenterPrompts 自身会拒采用+点名。
|
|
353
524
|
const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
|
|
354
525
|
if (bootClean)
|
|
355
526
|
ccEtag = r.etag;
|
|
356
527
|
for (const de of r.domainErrors ?? [])
|
|
357
528
|
logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "tolerated at BOOT (corrupt file → schema defaults; dangling ref → reported as-is); the refresh path rejects such a candidate whole and keeps the LKG (§9.5-5)" });
|
|
358
529
|
if (dryRun) {
|
|
359
|
-
logEffectiveDiff(config, r.effective, logger);
|
|
530
|
+
logEffectiveDiff(config, r.effective, logger); // compare-only: log the would-be changes, keep env
|
|
360
531
|
}
|
|
361
532
|
else {
|
|
362
|
-
applyEffective(config, r.effective, logger, { sealedKeys });
|
|
363
|
-
await adoptCenterPrompts(r.effective, "boot");
|
|
364
|
-
markRosterLanded(r.effective);
|
|
365
|
-
effective = r.effective;
|
|
366
|
-
appliedPlaneEff = r.effective;
|
|
533
|
+
applyEffective(config, r.effective, logger, { sealedKeys }); // sealedKeys: unseal sealedApiKey → in-memory key pipeline
|
|
534
|
+
await adoptCenterPrompts(r.effective, "boot"); // [1057] additive prompts key(判形采用;缺席=内置)
|
|
535
|
+
markRosterLanded(r.effective); // boot pull 即落 roster 的常态:门在启动窗内就打开
|
|
536
|
+
effective = r.effective; // skills/mcp applied below, before buildScenarios (restart-to-apply)
|
|
537
|
+
appliedPlaneEff = r.effective; // model plane applied pre-Runner — this IS the Runner's generation
|
|
367
538
|
if (bootClean)
|
|
368
|
-
latestEffective = r.effective;
|
|
539
|
+
latestEffective = r.effective; // LKG seed (§9.5-2): only a fully clean candidate may become the LKG
|
|
540
|
+
// F7(LKG 复审三轮):boot 写点**延后**到 center skills 装载(applyCenterSkills+盘缓存)之后——
|
|
541
|
+
// 先落 LKG 会让「新 skill 的 manifest 可见但正文没进缓存」窗口存在(此窗内崩+中心断=离线 boot
|
|
542
|
+
// 静默缺 skill)。这里只记 candidate,真正落盘在 skills 装载后(main 后段)。
|
|
369
543
|
if (bootClean && !lkgBooted)
|
|
370
544
|
bootLkgCandidate = { effective: r.effective, etag: r.etag };
|
|
545
|
+
// [875]b:带上模型数与(local lane)真实目录来源——复验实录:CONFIG_LOCAL_DIR 误配(只设
|
|
546
|
+
// LOCAL_DATA_ROOT)时 config.d 静默读不到、0 模型且无告警,排障只能靠猜。
|
|
371
547
|
const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
|
|
372
548
|
const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
|
|
373
549
|
logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
|
|
@@ -378,6 +554,7 @@ async function main() {
|
|
|
378
554
|
}
|
|
379
555
|
catch (err) {
|
|
380
556
|
logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
|
|
557
|
+
// 硬失败(fast-fail RST 等,非超窗)同样值得 LKG:上一进程的干净候选好过纯 env。
|
|
381
558
|
const lkgFallback = await bootFromLkg(`boot fetch failed: ${String(err).slice(0, 120)}`);
|
|
382
559
|
if (lkgFallback) {
|
|
383
560
|
lkgBooted = true;
|
|
@@ -393,10 +570,21 @@ async function main() {
|
|
|
393
570
|
}
|
|
394
571
|
if (!modelReadyState.ready)
|
|
395
572
|
logger.warn("model_roster_pending", { note: "registry deployment without an env model and no roster yet — billable submissions 503 until the first effective pull lands models" });
|
|
573
|
+
// DUAL-MODE §4/§5 (center #1): in LOCAL config mode, source remoteExec from the SHARED
|
|
574
|
+
// `config.d/remote-exec.json` via the registry `loadRemoteExec` — the SAME file + zod validation that the
|
|
575
|
+
// client's `agent-cli config` writes, so client doctor and the engine read one source of truth (zero drift).
|
|
576
|
+
// Semantics (center, deliberate): file MISSING → undefined → keep the env/host default (config.ts already
|
|
577
|
+
// defaults host in local mode); file PRESENT → it wins; file empty/corrupt → the loader throws → fail-closed
|
|
578
|
+
// (an empty file silently falling back to host would be an isolation downgrade if the user meant local-docker).
|
|
396
579
|
if (configProvider?.kind === "local") {
|
|
397
580
|
try {
|
|
398
|
-
const fileRx = await loadRemoteExec(localRoot);
|
|
581
|
+
const fileRx = await loadRemoteExec(localRoot); // SAME root as the config provider above (no split-brain)
|
|
399
582
|
if (fileRx) {
|
|
583
|
+
// 🔴 center contract: the package RemoteExecSpec is the CONTRACT form (env-NAME refs: apiKeyEnv/tokenEnv/…);
|
|
584
|
+
// our config.remoteExec is the RESOLVED form (values). `host` carries NO secrets so it maps 1:1 (contract
|
|
585
|
+
// `workdir` ≡ our `workspaceBase`). The ISOLATED lanes (e2b/k8s/ssh/adb) need a NAME→value resolution
|
|
586
|
+
// before use — a follow-on; until then they are sourced from REMOTE_EXEC env, so a file-source isolated
|
|
587
|
+
// arm is ignored (NOT cast verbatim — that would feed an env-NAME where a value is expected).
|
|
400
588
|
if (fileRx.provider === "host") {
|
|
401
589
|
config.remoteExec = {
|
|
402
590
|
provider: "host",
|
|
@@ -406,6 +594,10 @@ async function main() {
|
|
|
406
594
|
logger.info("remote_exec_from_file", { provider: "host", root: localRoot });
|
|
407
595
|
}
|
|
408
596
|
else if (fileRx.provider === "local-docker") {
|
|
597
|
+
// local-docker carries NO secret env-NAMEs (image/memory/cpus/network/workdir/mountPath only), so it
|
|
598
|
+
// maps 1:1 from the contract form — no NAME→value resolution needed (unlike e2b/k8s/ssh). The contract
|
|
599
|
+
// `workdir` is the in-container workspace ROOT ≡ our `mountPath`. DOCKER_HOST/secret env stay in the
|
|
600
|
+
// worker's .env (the adapter reads its own process.env via the env-forward, not the file).
|
|
409
601
|
config.remoteExec = {
|
|
410
602
|
provider: "local-docker",
|
|
411
603
|
image: fileRx.image,
|
|
@@ -423,20 +615,31 @@ async function main() {
|
|
|
423
615
|
}
|
|
424
616
|
catch (err) {
|
|
425
617
|
logger.error("remote_exec_file_invalid", { root: localRoot, err: String(err) });
|
|
426
|
-
throw err;
|
|
618
|
+
throw err; // fail-closed (center semantics): a corrupt remote-exec.json must not boot a wrong/weaker lane
|
|
427
619
|
}
|
|
428
620
|
}
|
|
621
|
+
// One shared SQL store backend for L1 + L2 (TiDB/MySQL or PostgreSQL per DB_BACKEND). Owned here.
|
|
429
622
|
let backend;
|
|
430
|
-
let storeBackendDegraded = false;
|
|
623
|
+
let storeBackendDegraded = false; // S5: auto-probe degraded this replica to in-memory (surfaced via gauge + /health)
|
|
624
|
+
// `auto` probes a configured DB and degrades to in-memory if it's unreachable (local/intranet binaries:
|
|
625
|
+
// "use my DB if I can reach it, else memory"). Explicit `tidb` fails fast instead — silently dropping
|
|
626
|
+
// persistence the operator asked for would lose runs. (`tidb` here = "the SQL DB"; DB_BACKEND picks the engine.)
|
|
431
627
|
const wantDb = config.sessionBackend === "tidb" ||
|
|
432
|
-
config.dbBackend === "local" ||
|
|
628
|
+
config.dbBackend === "local" || // local (clay 2026-06-25): always build the DB-less in-memory/file StoreBackend
|
|
433
629
|
(config.sessionBackend === "auto" && !!(config.tidb || config.pg));
|
|
434
630
|
if (wantDb) {
|
|
631
|
+
// 构造 + ensureSchema 同罩一层降级臂(2026-07-28 修):原先 try 只罩 ensureSchema,而 local 形真正
|
|
632
|
+
// 会抛的是构造里的 mkdir + 数据根 BootLock ——「裸 boot 不得拒启」那条口径对 local 从未生效过。
|
|
633
|
+
// 降级/fail-loud 的判据与理由见 openStoreBackendWithFallback 顶注。
|
|
435
634
|
const opened = await openStoreBackendWithFallback(config, logger);
|
|
436
635
|
backend = opened.backend;
|
|
437
636
|
storeBackendDegraded = opened.degraded;
|
|
438
637
|
}
|
|
638
|
+
// S5 review LOW-1: always render the series (0 = healthy) — gauge absence is indistinguishable from
|
|
639
|
+
// "old build without this metric", which breaks `== 0`-style alert rules.
|
|
439
640
|
metrics.setGauge("store_backend_degraded", storeBackendDegraded ? 1 : 0);
|
|
641
|
+
// S21 (SILENT-FALLBACK P1): MINIO_* partially set silently falls back to SQL-blob snapshots. Warn on the
|
|
642
|
+
// partial config and always render which backend blobs actually use.
|
|
440
643
|
{
|
|
441
644
|
const minioReq = ["MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"];
|
|
442
645
|
const present = minioReq.filter((v) => (process.env[v] ?? "") !== "");
|
|
@@ -445,14 +648,34 @@ async function main() {
|
|
|
445
648
|
}
|
|
446
649
|
metrics.setGauge("snapshot_blob_backend", 1, { backend: config.snapshotBlobStore ? "minio" : "sql" });
|
|
447
650
|
}
|
|
651
|
+
// Resolve `auto` to the concrete backend the rest of the wiring understands (createSessionStore,
|
|
652
|
+
// runStore, the startup summary). A DB-backed L2 with no backend degrades to in-memory the same way.
|
|
448
653
|
if (config.sessionBackend === "auto")
|
|
449
654
|
config.sessionBackend = backend ? "tidb" : "memory";
|
|
655
|
+
// local backend: route the session store through backend.session() (LocalSessionStore — the §0.5 OwnerAware twin),
|
|
656
|
+
// NOT the bare TtlSessionStore the "memory" path returns; "tidb" here means "the backend's durable store" generically.
|
|
450
657
|
if (config.dbBackend === "local" && backend)
|
|
451
658
|
config.sessionBackend = "tidb";
|
|
659
|
+
// Long-term memory (design/138 S1, clay 2026-07-08): the injection-first file-based memory ENGINE is the
|
|
660
|
+
// only memory plane — `RunnerDeps.memoryBackend` is core's switch (materialize → session file ops →
|
|
661
|
+
// harvest; no remember/recall tools). SINGLE-USER TURNKEY ONLY: the file basement has no tenant
|
|
662
|
+
// isolation, so a multi-tenant deployment (requirePrincipal) gets `undefined` = memory dark, fail-closed
|
|
663
|
+
// (memoryEngineBackendFor + memoryScopeFor enforce the same gate). The legacy MemoryStore plane
|
|
664
|
+
// (MEMORY_BACKEND/EMBEDDING_*) was dropped without migration; `scripts/drop-memory-tables.sql` drops the table.
|
|
665
|
+
// S3-TOB(设计 §1.3,后经改判与边界重切):backend 选择器——file=现状(单用户/host);pg|tidb=
|
|
666
|
+
// DB durable 真身(零卷主档:memory/ 文件面=materialize 的 ephemeral 工作副本,harvest 回 DB=持久化
|
|
667
|
+
// 时点)。DB backend 是多租户点亮的唯一门(显式 opt-in);选了 DB 但池不可用/方言不匹配=fail-loud 拒启
|
|
668
|
+
// (no-stopgap:绝不静默降 File/dark)。
|
|
452
669
|
let memoryEngine;
|
|
670
|
+
// 142-S2.5:per-(scope,peer) 同步游标面(sync_cursors)——POST /v1/memory/sync/:scope 的持久化半场。
|
|
671
|
+
// 只在 DB memory plane 上点亮(file 形态路由 501,不接);与 entry plane 同池同方言。
|
|
453
672
|
let memorySyncCursors;
|
|
454
673
|
if (config.memoryEngineBackend !== "file" && config.memoryEngineEnabled) {
|
|
455
674
|
const dialect = config.memoryEngineBackend;
|
|
675
|
+
// 142-S5.1 production history sink: every applied patch appends one
|
|
676
|
+
// agent_memory_engine_history row via the backend's optional history opts. The wrapper's ONLY
|
|
677
|
+
// job is the metrics leg — count lost rows BEFORE rethrowing (the backend swallows + warns with
|
|
678
|
+
// its own cumulative count; the audit line's breakage must itself be auditable).
|
|
456
679
|
const countedHistorySink = (store) => ({
|
|
457
680
|
appendHistory: async (rows) => {
|
|
458
681
|
try {
|
|
@@ -471,9 +694,11 @@ async function main() {
|
|
|
471
694
|
const q = async (text, params) => { const r = await pool.query(text, params); return { rows: r.rows }; };
|
|
472
695
|
await ensurePgMemoryEngineSchema(q);
|
|
473
696
|
await ensurePgMemoryHistorySchema(q);
|
|
474
|
-
await ensurePgMemorySyncSchema(q);
|
|
697
|
+
await ensurePgMemorySyncSchema(q); // 142-S2.5: sync_cursors(+push_queue)同池 ensure,幂等
|
|
475
698
|
memorySyncCursors = new PgMemorySyncStore(q);
|
|
476
699
|
const pgMem = new PgMemoryEngineBackend(q, { history: countedHistorySink(new PgMemoryHistoryStore(q)) });
|
|
700
|
+
// 工作面根:DB 形态下 memory/ 目录=per-worker ephemeral 物化区(丢了重建,控制面同);
|
|
701
|
+
// 复用 localDataRoot 下独立子树,绝不与 File backend 的持久 memory/ 混写。
|
|
477
702
|
memoryEngine = { backend: pgMem, root: join(config.localDataRoot ?? localRoot, "memory-work") };
|
|
478
703
|
}
|
|
479
704
|
else {
|
|
@@ -482,7 +707,7 @@ async function main() {
|
|
|
482
707
|
throw new Error("MEMORY_ENGINE_BACKEND=tidb requires DB_BACKEND=mysql/tidb — refusing to start half-configured");
|
|
483
708
|
await ensureTiDBMemoryEngineSchema(pool);
|
|
484
709
|
await ensureTiDBMemoryHistorySchema(pool);
|
|
485
|
-
await ensureTiDBMemorySyncSchema(pool);
|
|
710
|
+
await ensureTiDBMemorySyncSchema(pool); // 142-S2.5: sync_cursors(+push_queue)同池 ensure,幂等
|
|
486
711
|
memorySyncCursors = new TiDBMemorySyncStore(pool);
|
|
487
712
|
memoryEngine = { backend: new TiDBMemoryEngineBackend(pool, { history: countedHistorySink(new TiDBMemoryHistoryStore(pool)) }), root: join(config.localDataRoot ?? localRoot, "memory-work") };
|
|
488
713
|
}
|
|
@@ -490,6 +715,10 @@ async function main() {
|
|
|
490
715
|
}
|
|
491
716
|
else {
|
|
492
717
|
memoryEngine = memoryEngineBackendFor(config);
|
|
718
|
+
// [1845]②(cli 桌面撞坑;壳侧此前同坑已各自修过——两个宿主都踩=第三个宿主还会踩):操作员用
|
|
719
|
+
// LOCAL_DATA_ROOT 明确要了隔离数据根,MEMORY_ENGINE_DIR 却缺省 ⇒ file 形 memory engine 落 core
|
|
720
|
+
// 默认链(AGENT_DATA_DIR → ~/.ai-agent)= **静默共享全机 memory 库**。不改行为(单机用户可能就要
|
|
721
|
+
// 共享),只把「你现在在共享」说出来。
|
|
493
722
|
if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
|
|
494
723
|
logger.warn("memory_engine_dir_defaulted", {
|
|
495
724
|
root: memoryEngine.root,
|
|
@@ -497,6 +726,10 @@ async function main() {
|
|
|
497
726
|
});
|
|
498
727
|
}
|
|
499
728
|
}
|
|
729
|
+
// RosterStore(agent-team S1 server 半场,[1070]① 提货单;core 1.316 `RunnerDeps.rosterStore`):
|
|
730
|
+
// 具名子 agent 持久名册。形态跟 StoreBackend:tidb/pg=SQL twins(语义三条+真双库验证,
|
|
731
|
+
// roster-store-sql.ts 顶注);local=core FileRosterStore(dataRoot 下 roster.json,core 自带原子写/
|
|
732
|
+
// 损坏安全);无 backend(纯内存 dev)=不挂(core 用活注册表,跨 run 指针面自然缺席——诚实)。
|
|
500
733
|
let rosterStore;
|
|
501
734
|
{
|
|
502
735
|
const mysqlPool = backend?.mysqlPool?.();
|
|
@@ -516,6 +749,15 @@ async function main() {
|
|
|
516
749
|
if (rosterStore)
|
|
517
750
|
logger.info("roster_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
518
751
|
}
|
|
752
|
+
// BackgroundAgentStore(design/151 S1 server 半场,[1503] 提货单;core 1.364 durable background agents):
|
|
753
|
+
// 后台子代 `a*` 行的 durable 执行记录——settle 后转录 session 不再 eager release(TaskOutput/
|
|
754
|
+
// AgentTranscript 完成后照读全程),清理移交 reapDurableAgents(reaper 区)。形态跟 roster:
|
|
755
|
+
// tidb/pg=SQL twins(真双库验证,background-agent-store-sql.ts 顶注);local=core
|
|
756
|
+
// FileBackgroundAgentStore(dataRoot 下 background-agents/,core 自带 ledger+snapshot 原子写);
|
|
757
|
+
// 无 backend(纯内存 dev)=不挂(core 契约:store 不接线=pre-151 逐字节等价——单进程 live 注册表
|
|
758
|
+
// 已覆盖读面,诚实缺席)。⚠️ 同实例双点挂载:RunnerDeps.backgroundAgentStore(读半场)+
|
|
759
|
+
// ScenarioDeps→SubagentToolOptions.background.agentStore(写半场)都用这一个引用——engine 无法核对
|
|
760
|
+
// 配对,半接=静默死特性(RB-37①)。
|
|
519
761
|
let backgroundAgentStore;
|
|
520
762
|
{
|
|
521
763
|
const mysqlPool = backend?.mysqlPool?.();
|
|
@@ -530,16 +772,28 @@ async function main() {
|
|
|
530
772
|
backgroundAgentStore = new TiDBBackgroundAgentStore(mysqlPool);
|
|
531
773
|
}
|
|
532
774
|
else if (backend?.kind === "local") {
|
|
775
|
+
// core 1.368([1516]②)listScopes 落地:file 实现自带枚举——1.248 拍的「local+多租户 reap 缺口」
|
|
776
|
+
// warn-once 已撤(reaper 腿现在真枚举,不再假设单 scope)。
|
|
533
777
|
backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot);
|
|
534
778
|
}
|
|
535
779
|
if (backgroundAgentStore)
|
|
536
780
|
logger.info("background_agent_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
537
781
|
}
|
|
782
|
+
// D-1 通用文件上传(clay 拍 2026-07-27):独立附件 store(不骑 snapshot blob 面——那套的 orphan GC
|
|
783
|
+
// 会把非 manifest 引用的 bytes 误收;生命周期也不同:附件跟 task/session 绑定)。形态跟 StoreBackend
|
|
784
|
+
// 三态:tidb/pg=SQL twins(真双库验证)、local=文件店;无 backend(纯内存 dev)=不挂 ⇒ 路由 501。
|
|
785
|
+
// 四半场:①上传/取回/删除(http/server.ts 路由)②提交时绑定+objective 告知+host lane 物化
|
|
786
|
+
// (resolveSpec 内,下方)③远程 lane env 建立时物化(executionEnvFactory 包装,下方)④生命周期
|
|
787
|
+
// (E21 purge 级联 + 未绑定 TTL reaper)。
|
|
538
788
|
let taskAttachmentStore;
|
|
539
789
|
{
|
|
540
790
|
const mysqlPool = backend?.mysqlPool?.();
|
|
541
791
|
const pgPool = backend?.pgPool?.();
|
|
542
792
|
if (pgPool || mysqlPool) {
|
|
793
|
+
// clay 裁(2026-07-27):**云形态对象存储必配**——附件字节本体进 MinIO(SQL 只存 meta/门/生命周期)。
|
|
794
|
+
// 复用快照 lane 的同一 MinIO 部署配置(MINIO_ENDPOINT/ACCESS/SECRET,零重映射),但**附件专属
|
|
795
|
+
// keyPrefix**:对象键=sha256,与快照 blob 同 sha 不同生命周期,同名字空间会互删。未配 ⇒ 附件面
|
|
796
|
+
// 不接线(路由 501)+ error 级日志——这是部署配置错误,不静默降级回「字节进 DB」(该形已被裁掉)。
|
|
543
797
|
const minio = config.snapshotBlobStore;
|
|
544
798
|
if (!minio) {
|
|
545
799
|
logger.error("attachments_disabled_object_store_required", {
|
|
@@ -565,6 +819,11 @@ async function main() {
|
|
|
565
819
|
if (taskAttachmentStore)
|
|
566
820
|
logger.info("task_attachment_store_enabled", { backend: pgPool ? "pg+minio" : mysqlPool ? "tidb+minio" : "file", maxBytes: config.attachmentMaxBytes });
|
|
567
821
|
}
|
|
822
|
+
// S3c(core 1.374 行为车,[1531]):teammate 信箱——SendMessage 对 SETTLED named teammate 的 tier-3
|
|
823
|
+
// 懒复活链(durable 行解析→claim-CAS→mailbox append→reviveSpawn→attach 屏障后 ack)。激活三件套=
|
|
824
|
+
// RunnerDeps.mailboxStore + backgroundAgentStore 同挂 + delegation 工具在场,三缺一=1.373 文本字节
|
|
825
|
+
// 不变(诚实拒)。形态同上:tidb/pg=SQL twins(mailbox-store-sql.ts,发车复审八修顶注);local=
|
|
826
|
+
// core FileMailboxStore;纯内存 dev=不挂。回执句族(details.error 枚举)wire 透传零改动。
|
|
568
827
|
let mailboxStore;
|
|
569
828
|
{
|
|
570
829
|
const mysqlPool = backend?.mysqlPool?.();
|
|
@@ -583,23 +842,35 @@ async function main() {
|
|
|
583
842
|
if (mailboxStore)
|
|
584
843
|
logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
585
844
|
}
|
|
845
|
+
// 142-S5 §1.4: the per-scope export READ face lights up only on the DB memory plane (the multi-tenant
|
|
846
|
+
// truth lives in the scope-partitioned tables; the file posture exports by copying the memory dir, so
|
|
847
|
+
// the route 501s honestly there). History-table WRITE wiring: inside the backend construction above
|
|
848
|
+
// (S5.1, opts.history — the write point lives in the handed-over backend code).
|
|
586
849
|
const memoryExportBackend = config.memoryEngineBackend !== "file" ? memoryEngine?.backend : undefined;
|
|
587
|
-
const memoryLane = memoryEngineRemoteLanePosture(config);
|
|
850
|
+
const memoryLane = memoryEngineRemoteLanePosture(config); // N0: worker/sandbox file-plane split posture
|
|
588
851
|
if (config.memoryEngineBackend === "file" || !config.memoryEngineEnabled) {
|
|
589
852
|
logger.info("memory_engine_enabled", memoryEngine
|
|
590
853
|
? { enabled: true, backend: "file", dir: memoryEngine.root }
|
|
591
854
|
: {
|
|
592
855
|
enabled: false,
|
|
856
|
+
// 复审 F-11:按最终门结果报因,多租户 file=隔离判据,其余照旧。
|
|
593
857
|
reason: config.requirePrincipal === true ? "multi-tenant on the FILE backend (no tenant isolation; set MEMORY_ENGINE_BACKEND=pg|tidb to light up)"
|
|
594
858
|
: !config.memoryEngineEnabled ? "MEMORY_ENGINE=off"
|
|
595
859
|
: `remote exec lane "${memoryLane?.lane}" (worker/sandbox file planes split; MEMORY_ENGINE_REMOTE_LANE=allow overrides)`,
|
|
596
860
|
});
|
|
597
861
|
}
|
|
862
|
+
// N0 boot warn: loud in BOTH postures — "dark" so an upgrade that silently turns memory off is visible,
|
|
863
|
+
// "forced" so an operator override states what it depends on (harvest only sees the WORKER fs).
|
|
598
864
|
if (memoryLane && config.memoryEngineEnabled && config.requirePrincipal !== true) {
|
|
599
865
|
logger.warn("memory_engine_remote_lane", memoryLane.posture === "dark"
|
|
600
866
|
? { lane: memoryLane.lane, effect: "memory dark (fail-closed): the file engine works the worker's local fs while this lane routes model file tools to the sandbox fs — sandbox writes are never harvested. Set MEMORY_ENGINE_REMOTE_LANE=allow ONLY if both are one fs." }
|
|
601
867
|
: { lane: memoryLane.lane, effect: "MEMORY_ENGINE_REMOTE_LANE=allow: memory engine ON over a remote lane — harvest only sees files landing on the WORKER fs; verify the lane really shares it." });
|
|
602
868
|
}
|
|
869
|
+
// 142-S2.5-W1: TOC 同步 client 腿——只在 file memory 形态接线(loadConfig 已拒 DB backend
|
|
870
|
+
// 上的 MEMORY_SYNC_*,这条分支到不了)。boot 后 fire-and-forget 一轮(失败 warn 不阻断——纯本地现状
|
|
871
|
+
// 是安全降级面);之后 harvest 真有 patch 落地时再触发(onMemoryHarvestReport 站点,inflight 节流)。
|
|
872
|
+
// 引擎 dark(多租户/MEMORY_ENGINE=off/remote lane)⇒ 无本地盘可同步:warn 不 throw(dark 的三个成因
|
|
873
|
+
// 各有自己的 loud 日志在上方,这里补“sync 因此没跑”这半句,operator 可见不半配)。
|
|
603
874
|
let memorySyncRunner;
|
|
604
875
|
if (config.memorySync) {
|
|
605
876
|
if (memoryEngine) {
|
|
@@ -609,6 +880,7 @@ async function main() {
|
|
|
609
880
|
memoryRoot: memoryEngine.root,
|
|
610
881
|
transport: createMemorySyncTransport({ url: config.memorySync.url, token: config.memorySync.token }),
|
|
611
882
|
log: logger,
|
|
883
|
+
// S2.5 分批(core 1.284):未设=不分批 wire 字节不变;设了=续轮 loop 至收敛(MAX_SYNC_ROUNDS 警戒)。
|
|
612
884
|
...(config.memorySync.maxPushEntries !== undefined ? { maxPushEntries: config.memorySync.maxPushEntries } : {}),
|
|
613
885
|
...(config.memorySync.maxPullEntries !== undefined ? { maxPullEntries: config.memorySync.maxPullEntries } : {}),
|
|
614
886
|
});
|
|
@@ -619,11 +891,18 @@ async function main() {
|
|
|
619
891
|
logger.warn("memory_sync_configured_but_memory_dark", { note: "MEMORY_SYNC_URL is set but the file memory engine is dark (multi-tenant / MEMORY_ENGINE=off / remote exec lane) — no sync rounds will run" });
|
|
620
892
|
}
|
|
621
893
|
}
|
|
622
|
-
const sessionStore = createSessionStore(config, backend, metrics);
|
|
894
|
+
const sessionStore = createSessionStore(config, backend, metrics); // S25: stale-affinity evict fingerprint
|
|
895
|
+
// S6 startup guard: multi-tenant isolation needs an owner-aware (TiDB) store. Refuse to start with
|
|
896
|
+
// REQUIRE_PRINCIPAL on an in-memory store that can't enforce session ownership.
|
|
623
897
|
if (config.requirePrincipal && !sessionStore.ownerOf) {
|
|
624
898
|
throw new Error("REQUIRE_PRINCIPAL=true needs an owner-aware session store (SESSION_BACKEND=tidb); " +
|
|
625
899
|
"the in-memory store cannot enforce session ownership.");
|
|
626
900
|
}
|
|
901
|
+
// The local in-memory backend HAS an ownerOf (LocalSessionStore), so the guard above passes — but its owner map is
|
|
902
|
+
// process-local + lost on restart, so it cannot DURABLY enforce multi-tenant ownership (post-restart a session id is
|
|
903
|
+
// re-claimable by whoever attaches first). Refuse REQUIRE_PRINCIPAL on it: a real multi-tenant
|
|
904
|
+
// boundary needs the durable DB backend; single-user local runs with REQUIRE_PRINCIPAL=false (the principal may
|
|
905
|
+
// still ride for memory-scoping). The file-backed follow-on (durable owners) can revisit this.
|
|
627
906
|
if (config.requirePrincipal && backend?.kind === "local") {
|
|
628
907
|
throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
|
|
629
908
|
"durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 — durable " +
|
|
@@ -631,17 +910,27 @@ async function main() {
|
|
|
631
910
|
"(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
|
|
632
911
|
"still inject x-agent-principal per request for memory scoping and audit attribution).");
|
|
633
912
|
}
|
|
913
|
+
// (c)(clay 拍 a+c,2026-07-27)云形快照 blob 姿势门:mysql|pg 后端缺 MinIO ⇒ fail-loud(bytes-in-DB
|
|
914
|
+
// 撞包墙已两役实证);SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true 显式逃生(单机/测试台,吃 (a) 的 per-blob 帽)。
|
|
634
915
|
if (backend)
|
|
635
916
|
assertCloudSnapshotBlobPosture(backend.kind, config);
|
|
917
|
+
// Posture warning (audit B, security.ts): an owner-aware store without REQUIRE_PRINCIPAL means the
|
|
918
|
+
// principal layer is optional per request. Owned sessions are still protected (the authorizer rejects
|
|
919
|
+
// anonymous/mismatched attach), but new headerless submissions create anonymous sessions any token
|
|
920
|
+
// holder can attach to — a multi-tenant deployment should set REQUIRE_PRINCIPAL=true.
|
|
636
921
|
if (!config.requirePrincipal && sessionStore.ownerOf) {
|
|
637
922
|
logger.warn("principal_optional", {
|
|
638
923
|
note: "owner-aware session store with REQUIRE_PRINCIPAL=false — owned sessions are protected, but headerless callers can create/share anonymous sessions; set REQUIRE_PRINCIPAL=true for multi-tenant",
|
|
639
924
|
});
|
|
640
925
|
}
|
|
641
926
|
metrics.dynamicGauge("sessions_warm", "Sessions held in the warm cache", () => sessionStore.size);
|
|
927
|
+
// 1.38 cross-replica circuit-breaker state: shared via TiDB when a pool exists AND the breaker is
|
|
928
|
+
// enabled; otherwise core's per-process Map (single replica / no DB / breaker off). Refresh loop is
|
|
929
|
+
// unref'd so it never holds the process open.
|
|
642
930
|
const breakerState = backend?.breaker && config.resilience.circuitBreaker
|
|
643
931
|
? backend
|
|
644
932
|
.breaker((streak) => {
|
|
933
|
+
// LOW (SILENT-FALLBACK P1): cross-replica breaker write-through failures were a bare swallow.
|
|
645
934
|
metrics.setGauge("counter_flush_fail_streak", streak, { table: "circuit_breaker", kind: "write_through" });
|
|
646
935
|
if (streak > 0)
|
|
647
936
|
metrics.inc("breaker_writethrough_failed_total", { backend: backend.kind });
|
|
@@ -651,9 +940,15 @@ async function main() {
|
|
|
651
940
|
logger.info("breaker_writethrough_recovered", {});
|
|
652
941
|
})
|
|
653
942
|
.startRefresh()
|
|
654
|
-
: undefined;
|
|
943
|
+
: undefined; // local omits breaker() → core's per-process Map
|
|
655
944
|
const brain = createBrain(config, { breakerState });
|
|
945
|
+
// ③ Cost + ④ trace + ⑤ budget (1.37): inject per-model pricing (authoritative costMicroUsd), bridge
|
|
946
|
+
// core's content-free trace events to /metrics, and accumulate per-principal spend into the cost
|
|
947
|
+
// quota (the tracer reads the principal ALS so council/team sub-task cost is attributed too).
|
|
656
948
|
const pricing = buildPricing(config.models);
|
|
949
|
+
// S9 (SILENT-FALLBACK P0-e): write-behind flush/refresh failures previously vanished in a bare catch —
|
|
950
|
+
// the fleet silently degrades to per-replica counting (soft limits stop being fleet-wide). Every failure
|
|
951
|
+
// ticks the counter + streak gauge; warn once per episode (streak hits 3), info on the recovery edge.
|
|
657
952
|
const counterDegradeHook = (info) => {
|
|
658
953
|
metrics.setGauge("counter_flush_fail_streak", info.streak, { table: info.table, kind: info.kind });
|
|
659
954
|
if (info.streak > 0)
|
|
@@ -663,33 +958,79 @@ async function main() {
|
|
|
663
958
|
else if (info.streak === 0 && info.prevStreak >= 3)
|
|
664
959
|
logger.info("counter_flush_recovered", { table: info.table, kind: info.kind, prevStreak: info.prevStreak });
|
|
665
960
|
};
|
|
961
|
+
// Per-principal cumulative cost quota: TiDB-backed (cross-replica, atomic write-behind) when a pool
|
|
962
|
+
// exists, else in-memory per-replica (single-replica / no DB). Both satisfy QuotaTracker. (design/30 P0-1)
|
|
666
963
|
const costQuota = config.maxPrincipalCostUsd > 0
|
|
667
|
-
? backend?.costQuota
|
|
964
|
+
? backend?.costQuota // local omits costQuota() → in-memory per-process CostQuota (single-replica)
|
|
668
965
|
? backend.costQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000, counterDegradeHook).startRefresh()
|
|
669
966
|
: new CostQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000)
|
|
670
967
|
: undefined;
|
|
968
|
+
// E8 (shell-host contract): per-task × per-model usage accumulator, fed by the tracer and drained by the durable run
|
|
969
|
+
// lifecycle into `model_usage` events (the `TaskStats.modelUsage` echo). One instance, shared tracer↔server.
|
|
671
970
|
const modelUsageTracker = new ModelUsageTracker();
|
|
971
|
+
// [998]② web 观测切片 A 数据面:prompt.assembled 的 manifest 经 tracer 记录、由 durable run 生命周期
|
|
972
|
+
// drain 成 `prompt_assembled` 事件(turns/stream 可读)。与 modelUsageTracker 同款 register 篱笆/生命周期。
|
|
672
973
|
const promptManifestTracker = new PromptManifestTracker();
|
|
974
|
+
// fleet 用量批报的 principal×model 窗口累计器 — 仅当 fleet 面可能启用(center lane 配置了)才建,
|
|
975
|
+
// 否则 tracer 走 undefined = 零行为(非 fleet 部署不多攒一份内存)。真正的启动门在 startFleetClientFromEnv。
|
|
673
976
|
const fleetUsage = config.configCenter && process.env.FLEET_ADVERTISE_ADDRESS ? new FleetUsageAccumulator() : undefined;
|
|
977
|
+
// fleet lease 消费(worker 侧第三接线,D4 AP 模式):协商式 —— center lane 配齐即启(lease 是出站消费面,
|
|
978
|
+
// 不需要 FLEET_ADVERTISE_ADDRESS);非 lease-enforced principal 一次 409 负缓存后走既有本地 CostQuota,
|
|
979
|
+
// 单用户(无 principal)提交门早退 —— 两者零行为变化。
|
|
674
980
|
const fleetLease = createFleetLeaseFromEnv(config, { logger, metrics });
|
|
981
|
+
// weight-at-burn 回调:读 config.modelQuotaWeights(registry hot-apply 的活表;env lane 空 ⇒ 1 兜底)。
|
|
675
982
|
const tracer = createTracer(metrics, costQuota, modelUsageTracker, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1, promptManifestTracker);
|
|
983
|
+
// [1469] side-query 记账 seam:与 tracer 同一装配点、同一组 sink 实例(codex R3——sideQuery 不发
|
|
984
|
+
// brain.call,四路 sink 必须由 server 半场喂,两臂同语义见 createSideQueryAccountant 头注)。
|
|
676
985
|
const sideQueryAccounting = createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1);
|
|
677
|
-
|
|
986
|
+
// Durable offload store (core 1.47/1.49): large tool results survive a cross-replica wake. Without a
|
|
987
|
+
// pool, core's task-scoped in-memory default applies (graceful: cross-wake fetch misses → preview stands).
|
|
988
|
+
const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined; // tidb/pg = SQL twins; local = core's FileToolResultStore (core 1.219 — restart-durable refs); no backend → core's in-memory default
|
|
989
|
+
// E6 durable SessionPolicyStore — operator-tightened per-session tool rules core reads at prepare-time (subtract-only).
|
|
990
|
+
// Present on every backend (incl local = core's InMemorySessionPolicyStore); undefined only on the env-only/no-backend
|
|
991
|
+
// worker → feature OFF (core reads no rules). Wired into the PRIMARY Runner below + the PUT/GET policy route + E21 purge.
|
|
678
992
|
const sessionPolicyStore = backend ? backend.sessionPolicy() : undefined;
|
|
993
|
+
// E19 rewind-files — working-tree snapshot/restore keyed by entryId (the same anchor as E18), + the 2c artifact-transfer
|
|
994
|
+
// store. core AUTOMATICALLY snapshots each completed turn + restores on resumeAt when `spec.rewindFiles` is set, for ANY
|
|
995
|
+
// ExecutionEnv when this store is wired (gate-split 1.134.0 removed the isRemoteExecutionEnv skip → host/local-mode
|
|
996
|
+
// + e2b/k8s/ssh/adb/local-docker all active). Durable on tidb/pg (cross-replica + 2c), local = core's
|
|
997
|
+
// InMemoryFileSnapshotStore. Wired into the PRIMARY Runner + the `rewindFiles` cap + E21 purge.
|
|
679
998
|
const fileSnapshotStore = backend ? backend.fileSnapshot() : undefined;
|
|
999
|
+
// [805]③a / [806]④: E19-fix observability — core 1.287 captureManifest TOLERATES enumerate→read-window
|
|
1000
|
+
// vanished files (skip, not fail) and reports the count ONLY in its return value; the SQL twins expose it via
|
|
1001
|
+
// this optional sink. Metric = the live face of "the fix is absorbing real races" (a warn would be noise — the
|
|
1002
|
+
// skip is the CORRECT behavior). GAP (honest): the local lane uses core's own store, which drops the count
|
|
1003
|
+
// internally — surfacing it there needs a core seam (noted on the board, not faked here).
|
|
680
1004
|
if (fileSnapshotStore && "onSkippedVanished" in fileSnapshotStore) {
|
|
681
1005
|
fileSnapshotStore.onSkippedVanished = (count) => metrics.inc("snapshot_skipped_vanished_total", {}, count);
|
|
682
1006
|
}
|
|
1007
|
+
// design/48 v1b: deployment-level remote execution(部署级路由 + 懒汉连接). When
|
|
1008
|
+
// REMOTE_EXEC=e2b, this deployment is a "code-agent worker" — each task's hand runs in a per-task E2B VM,
|
|
1009
|
+
// provisioned lazily on first hand use (a plan-only task pays zero VM cost). Unset → in-process stub env.
|
|
1010
|
+
// design/48 v1b + design/61: pick the remote-exec backend by provider (peer adapters). SSH/ADB target REAL
|
|
1011
|
+
// systems (not isolated/suspendable) — the leader's autonomy + the design/37 gate + HITL run accordingly.
|
|
1012
|
+
// §7 P0.5 per-task sandbox image: the trusted-control-plane bridge from resolveSpec (resolves the requested
|
|
1013
|
+
// profile → digest with the caller's principal, fail-closed) to the k8s factory (applies it per-pod). See
|
|
1014
|
+
// per-task-image.ts + resolveSpec below. Worker-global default image is the fallback when no profile is requested.
|
|
683
1015
|
const perTaskImage = new PerTaskImageRegistry();
|
|
1016
|
+
// RFC A2: session-keyed environment selection (PROFILE intent, never a digest) written by the model-facing
|
|
1017
|
+
// SelectEnvironment tool; resolveSpec re-resolves it FAIL-CLOSED per task (same re-admit as a body profile).
|
|
684
1018
|
const sessionEnvSelection = new SessionEnvironmentSelection();
|
|
1019
|
+
// [#40 / TOC cwd seam] per-session launch dir → the host lane's agent workspace (resolveSpec registers it gated by
|
|
1020
|
+
// cwdHonored; the host factory reads it by ctx.sessionId). Only ever written for the single-user host lane.
|
|
1021
|
+
// BOUNDED (avoid unbounded growth over the persistent local engine's lifetime): an LRU cap; the shell
|
|
1022
|
+
// re-sends cwd on every request, so evicting a stale session is harmless (it re-registers on next use).
|
|
685
1023
|
const MAX_CWD_SESSIONS = 4096;
|
|
686
1024
|
const perSessionCwd = new Map();
|
|
687
1025
|
const setSessionCwd = (sid, cwd) => {
|
|
688
|
-
perSessionCwd.delete(sid);
|
|
1026
|
+
perSessionCwd.delete(sid); // re-insert at the tail = most-recently-used
|
|
689
1027
|
perSessionCwd.set(sid, cwd);
|
|
690
1028
|
if (perSessionCwd.size > MAX_CWD_SESSIONS)
|
|
691
|
-
perSessionCwd.delete(perSessionCwd.keys().next().value);
|
|
1029
|
+
perSessionCwd.delete(perSessionCwd.keys().next().value); // evict oldest
|
|
692
1030
|
};
|
|
1031
|
+
// [R-survey / TOC shellEnv seam, core PLAN批注] per-session `settings.env` → the host lane's agent shell env
|
|
1032
|
+
// (resolveSpec registers it gated by cwdHonored — single-user host lane only; the host factory merges it by
|
|
1033
|
+
// ctx.sessionId). design/107 "env = capability axis". Same LRU bound + re-send-on-every-request semantics as cwd.
|
|
693
1034
|
const perSessionShellEnv = new Map();
|
|
694
1035
|
const setSessionShellEnv = (sid, env) => {
|
|
695
1036
|
perSessionShellEnv.delete(sid);
|
|
@@ -698,6 +1039,8 @@ async function main() {
|
|
|
698
1039
|
perSessionShellEnv.delete(perSessionShellEnv.keys().next().value);
|
|
699
1040
|
};
|
|
700
1041
|
let executionEnvFactory;
|
|
1042
|
+
// SVC-3 worktree isolation: the reaper (defined far below) reuses ONE long-lived git base env + repoRoot to
|
|
1043
|
+
// `git worktree prune` crash-orphaned worktrees. Holders are populated when the wrapper is wired (host lane).
|
|
701
1044
|
let worktreeReap;
|
|
702
1045
|
if (config.remoteExec?.provider === "e2b") {
|
|
703
1046
|
executionEnvFactory = e2bExecutionEnvFactory({
|
|
@@ -706,7 +1049,11 @@ async function main() {
|
|
|
706
1049
|
...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
|
|
707
1050
|
...(config.remoteExec.livenessMs != null ? { livenessMs: config.remoteExec.livenessMs } : {}),
|
|
708
1051
|
...(config.remoteExec.allowInternetAccess != null ? { allowInternetAccess: config.remoteExec.allowInternetAccess } : {}),
|
|
1052
|
+
// [1452]/[1454] CWD-A: workspace 根透传(E2B_MOUNT_PATH)——缺省仍 /home/user(adapter 默认)。
|
|
709
1053
|
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
1054
|
+
// RFC B5: region package-source env set rides the operator-trusted sandboxEnv — ENV-FIRST, the derived
|
|
1055
|
+
// ecosystem overrides (pip/uv/npm/go/rustup/flutter/…) beat the image's baked CN ENV on existing images,
|
|
1056
|
+
// no rebake needed. Explicit E2B_SANDBOX_ENV keys win over derived on collision (deliberate override).
|
|
710
1057
|
...(() => {
|
|
711
1058
|
const merged = { ...derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv()), ...(config.remoteExec.sandboxEnv ?? {}) };
|
|
712
1059
|
return Object.keys(merged).length > 0 ? { sandboxEnv: merged } : {};
|
|
@@ -726,14 +1073,26 @@ async function main() {
|
|
|
726
1073
|
...(config.remoteExec.runtimeClass != null ? { runtimeClass: config.remoteExec.runtimeClass } : {}),
|
|
727
1074
|
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
728
1075
|
...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
|
|
1076
|
+
// Resource-profile knobs. config.ts reads K8S_MEMORY/K8S_CPU; this passthrough was
|
|
1077
|
+
// missing, so the adapter silently stayed on its 2Gi default (Kata VM = default_memory 2G + limit 2G = 4G
|
|
1078
|
+
// MemTotal — task02b run9/10 integration gradle OOM'd there even after the env was set).
|
|
729
1079
|
...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
|
|
730
1080
|
...(config.remoteExec.cpu ? { cpu: config.remoteExec.cpu } : {}),
|
|
731
1081
|
...(config.remoteExec.s3Snapshot ? { s3Snapshot: config.remoteExec.s3Snapshot } : {}),
|
|
1082
|
+
// RFC B5: region package-source env set → pod container env (operator-trusted, never task-controlled).
|
|
1083
|
+
// ENV-FIRST: pod env beats image ENV and reaches every exec — the derived ecosystem overrides switch
|
|
1084
|
+
// region on existing images; the SEMA_PKG_SOURCE marker drives the in-image hook (file-bound pieces).
|
|
732
1085
|
...(() => {
|
|
733
1086
|
const derived = derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv());
|
|
734
1087
|
return Object.keys(derived).length > 0 ? { podEnv: derived } : {};
|
|
735
1088
|
})(),
|
|
736
1089
|
};
|
|
1090
|
+
// §7 P0.5: per-pod image override. resolveSpec resolved the requested profile→digest (with the caller's
|
|
1091
|
+
// principal, fail-closed) and registered it by sessionId — the ONLY identifier stable across the factory ctx
|
|
1092
|
+
// on every path (adversarial-review round-2: /v1/runs mints its own durable taskId that CLOBBERS spec.taskId,
|
|
1093
|
+
// so taskId-keying misses on the primary path; sessionId survives). get() is non-removing (the factory may be
|
|
1094
|
+
// invoked >once per logical task). Absent ⇒ the worker-global k8sCfg.image. Building the factory per task is a
|
|
1095
|
+
// cheap closure alloc; the ref is immutable (repo@digest). podSpecPatch can NOT carry the image (hard invariant).
|
|
737
1096
|
executionEnvFactory = (ctx) => k8sExecutionEnvFactory({ ...k8sCfg, image: perTaskImage.get(ctx.sessionId) ?? k8sCfg.image })(ctx);
|
|
738
1097
|
}
|
|
739
1098
|
else if (config.remoteExec?.provider === "ssh") {
|
|
@@ -753,10 +1112,20 @@ async function main() {
|
|
|
753
1112
|
});
|
|
754
1113
|
}
|
|
755
1114
|
else if (config.remoteExec?.provider === "host") {
|
|
1115
|
+
// DUAL-MODE §5: the TOC `host` lane — run on THIS machine, no container (isolation=none). Fan-out still
|
|
1116
|
+
// works (bounded by one box). Secrets are env-NAMEs the host resolves from its own process.env.
|
|
756
1117
|
{
|
|
1118
|
+
// R7 self-wake: inject the self-wake SchedulerCapability backend on the single-user TOC host lane
|
|
1119
|
+
// (opt-in SCHEDULER_ENABLED). `hasScheduler(env)` then mounts CronCreate/CronDelete/CronList(旧名 CronCancel、Sleep 已 design/136 撤除——L8 注释订正,且挂载在 core prepare-task 非 createHandsToolkit); the TOC shell
|
|
1120
|
+
// daemon reads the SAME ~/.sema/scheduled_tasks.json store (registry-core /node binding) and fires due intents.
|
|
1121
|
+
// Multi-tenant (requirePrincipal) routes scheduling to center, NOT this host daemon → gate it off there. ONE
|
|
1122
|
+
// backend instance at boot → every host task shares the same store file.
|
|
757
1123
|
const hostScheduler = config.schedulerEnabled && config.requirePrincipal !== true
|
|
758
1124
|
? new FileSchedulerBackend({
|
|
759
1125
|
...(config.schedulerStorePath ? { storePath: config.schedulerStorePath } : {}),
|
|
1126
|
+
// [1009]② host-signal: the spawning shell knows whether a resident daemon will honor session
|
|
1127
|
+
// wakeups — SCHEDULER_SESSION_WAKEUP=false flips the capability off at CONSTRUCTION (instance-
|
|
1128
|
+
// lifetime snapshot, core TOCTOU 契约) so core's ScheduleWakeup refuses up front.
|
|
760
1129
|
...(config.schedulerSessionWakeup === false ? { caps: { supportsSessionWakeup: false } } : {}),
|
|
761
1130
|
})
|
|
762
1131
|
: undefined;
|
|
@@ -766,21 +1135,37 @@ async function main() {
|
|
|
766
1135
|
...(config.remoteExec.workspaceBase ? { workspaceBase: config.remoteExec.workspaceBase } : {}),
|
|
767
1136
|
...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
|
|
768
1137
|
...(hostScheduler ? { scheduler: hostScheduler } : {}),
|
|
1138
|
+
// design/103 background shell: single-user host lane only (parity with scheduler/cwd). Multi-tenant host
|
|
1139
|
+
// lane (which shouldn't exist — host runs on the worker's own box) → INERT (run_in_background/etc. don't mount).
|
|
769
1140
|
backgroundShell: config.requirePrincipal !== true,
|
|
770
1141
|
};
|
|
1142
|
+
// [#40 / TOC cwd seam] per-request workspace: if resolveSpec registered a caller `cwd` for this session (gated by
|
|
1143
|
+
// cwdHonored — single-user host lane only), run the agent VERBATIM in that dir (persistent, never deleted — the
|
|
1144
|
+
// user's project). Else the boot-time host config (ephemeral random subdir). Mirrors the k8s perTaskImage wrap.
|
|
1145
|
+
// core 1.219 `ctx.parentCwd` (dogfood finding: sub-agents landed in an EMPTY sandbox): a DELEGATED child
|
|
1146
|
+
// (workflow ctx.agent / Task tool) now roots at its PARENT's working dir — CC parity — via core's TRUSTED
|
|
1147
|
+
// RunInternals channel. Precedence in effectiveHostWorkspace: session cwd → parentCwd (unless the child asked
|
|
1148
|
+
// for isolation:"worktree" — that wrapper wins below) → ephemeral. Container lanes never consult this.
|
|
771
1149
|
executionEnvFactory = (ctx) => {
|
|
772
1150
|
const cwd = effectiveHostWorkspace(perSessionCwd.get(ctx.sessionId), ctx);
|
|
773
1151
|
const shellEnv = perSessionShellEnv.get(ctx.sessionId);
|
|
774
1152
|
const env = hostExecutionEnvFactory({
|
|
775
1153
|
...hostCfg,
|
|
776
1154
|
...(cwd ? { workspaceDir: cwd } : {}),
|
|
777
|
-
...(shellEnv ? { env: shellEnv } : {}),
|
|
1155
|
+
...(shellEnv ? { env: shellEnv } : {}), // R-survey: per-task shell env (merged UNDER per-command options.env by the host adapter)
|
|
778
1156
|
})(ctx);
|
|
1157
|
+
// core 1.191: an EPHEMERAL host workspace (no honored `cwd` → a per-task `sema-host-<id>` dir destroy()
|
|
1158
|
+
// rm's) evicts its LSP servers at task-end so a busy server doesn't accumulate one heavy language server per
|
|
1159
|
+
// task. cwdHonored (persistent project) is NOT evicted — its stable root stays warm across turns (CC-parity);
|
|
1160
|
+
// worktree/fan-out is bounded by maxSessions=16. Only wraps when a NodeLspManager is actually active.
|
|
779
1161
|
return lspManager && !cwd ? evictLspOnDestroy(env, lspManager) : env;
|
|
780
1162
|
};
|
|
781
1163
|
}
|
|
782
1164
|
}
|
|
783
1165
|
else if (config.remoteExec?.provider === "local-docker") {
|
|
1166
|
+
// DUAL-MODE §5: the TOC `local-docker` lane — a per-task container on THIS machine's docker daemon
|
|
1167
|
+
// (isolation:true, suspendable:false). The base image comes ENTIRELY from config (no docker.io default —
|
|
1168
|
+
// domestic-images iron rule). Secrets are resolved env-NAME→value here (config.ts already did the forward).
|
|
784
1169
|
executionEnvFactory = localDockerExecutionEnvFactory({
|
|
785
1170
|
image: config.remoteExec.image,
|
|
786
1171
|
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
@@ -793,10 +1178,24 @@ async function main() {
|
|
|
793
1178
|
...(config.remoteExec.env ? { env: config.remoteExec.env } : {}),
|
|
794
1179
|
});
|
|
795
1180
|
}
|
|
1181
|
+
// [848] remote scratchpad: decorate the REMOTE sandbox lanes (e2b/k8s/local-docker/ssh) so each env
|
|
1182
|
+
// lazily `mkdir -p`s `/tmp/scratchpad/<sessionId>` on its FIRST exec — never at factory time (e2b is
|
|
1183
|
+
// lazy-VM; a factory-time exec would force-boot the VM). adb is skipped (no standard /tmp on Android);
|
|
1184
|
+
// host keeps its worker-local scratchpad (ensureScratchpadDir at the envFacts consumer). The envFacts
|
|
1185
|
+
// advertisement below uses the SAME remoteScratchpadDirFor rule, so fact and mkdir can never disagree.
|
|
796
1186
|
if (executionEnvFactory && isRemoteScratchpadLane(config.remoteExec?.provider)) {
|
|
797
1187
|
executionEnvFactory = withRemoteScratchpad(executionEnvFactory, logger);
|
|
798
1188
|
}
|
|
1189
|
+
// SVC-3 (design/97 CORE-6): wrap the factory with git-worktree isolation. When core marks an agent
|
|
1190
|
+
// `isolation:"worktree"` (TRUSTED RunInternals — never a TaskSpec), the wrapper mints a per-agent detached
|
|
1191
|
+
// worktree under the operator-trusted repoRoot; otherwise it passes the base env through untouched. v1
|
|
1192
|
+
// covers the `host` lane (the TOC fan-out lane). Other lanes would need a worktree-ROOTED REMOTE env from
|
|
1193
|
+
// `rootEnvAt` (a follow-on; a host/NodeExecutionEnv worktree is not durable-suspendable — see the plugin
|
|
1194
|
+
// header) — they are left unwrapped here, isolation:"worktree" is then a no-op for them (the base env wins).
|
|
799
1195
|
if (config.worktreeIsolation && executionEnvFactory) {
|
|
1196
|
+
// M4 (adversarial-review): a mis-wired WORKTREE_REPO_ROOT must fail LOUD at boot, not per-task at runtime. The
|
|
1197
|
+
// root must exist AND be a git repo (`.git` dir or worktree-link file). On a bad root, log an error + LEAVE
|
|
1198
|
+
// isolation OFF (the factory unwrapped) rather than wiring a factory that throws on every isolated agent.
|
|
800
1199
|
const wtRoot = config.worktreeIsolation.repoRoot;
|
|
801
1200
|
const wtValid = existsSync(wtRoot) && existsSync(join(wtRoot, ".git"));
|
|
802
1201
|
if (config.remoteExec?.provider === "host" && !wtValid) {
|
|
@@ -807,29 +1206,56 @@ async function main() {
|
|
|
807
1206
|
});
|
|
808
1207
|
}
|
|
809
1208
|
else if (config.remoteExec?.provider === "host") {
|
|
1209
|
+
// The SHARED base env that runs `git worktree add/remove/prune` (cwd:repoRoot is passed explicitly by
|
|
1210
|
+
// core on every git call, so this env's own root is irrelevant — it just needs `exec` + git on PATH).
|
|
1211
|
+
// ONE long-lived env per deployment; the reaper reuses it. `inheritEnv:"all"` so the operator's git
|
|
1212
|
+
// config/credentials are visible (this is the operator's own machine + own repo, the TOC posture).
|
|
1213
|
+
// systematic-audit (gate-fail-direction): every worktree-lane RemoteHostExecutionEnv carries the SAME
|
|
1214
|
+
// background-shell gate as the primary host factory (main.ts ~442) — not the env's `?? true` default. The
|
|
1215
|
+
// git-base env is git-ops only (agents run on rootEnvAt below), but gating it keeps the policy uniform / future-proof.
|
|
810
1216
|
const gitBaseEnv = new RemoteHostExecutionEnv({ workspaceDir: config.worktreeIsolation.repoRoot, inheritEnv: "all", backgroundShell: config.requirePrincipal !== true });
|
|
811
1217
|
executionEnvFactory = withWorktreeIsolation(executionEnvFactory, {
|
|
812
1218
|
repoRoot: config.worktreeIsolation.repoRoot,
|
|
813
1219
|
...(config.worktreeIsolation.allowedRoots ? { allowedRoots: config.worktreeIsolation.allowedRoots } : {}),
|
|
814
1220
|
...(config.worktreeIsolation.commit ? { commit: config.worktreeIsolation.commit } : {}),
|
|
815
1221
|
baseEnvForGit: gitBaseEnv,
|
|
1222
|
+
// Host-lane worktree-rooted env: a host adapter in PERSISTENT-DIR mode — its own destroy() does NOT
|
|
1223
|
+
// rm the dir (so core's `git worktree remove` owns teardown), and it keeps the host adapter's secret
|
|
1224
|
+
// scrub + timeout semantics (vs a bare NodeExecutionEnv). inheritEnv defaults to "scrub" (model-driven).
|
|
816
1225
|
rootEnvAt: (dir) => {
|
|
817
1226
|
const env = new RemoteHostExecutionEnv({ workspaceDir: dir, backgroundShell: config.requirePrincipal !== true });
|
|
1227
|
+
// A worktree root is per-agent + ephemeral (core's `git worktree remove` reclaims it at
|
|
1228
|
+
// task-end), so evict its LSP servers DETERMINISTICALLY too — not just via the maxSessions LRU backstop. The
|
|
1229
|
+
// worktree dir IS the LSP cache root (env.cwd), so evict(env.cwd) on destroy hits it. (`lspManager` is declared
|
|
1230
|
+
// below but this closure only runs per-task, well after it's initialized — same forward-ref as the host factory.)
|
|
818
1231
|
return lspManager ? evictLspOnDestroy(env, lspManager) : env;
|
|
819
1232
|
},
|
|
820
1233
|
logger,
|
|
821
1234
|
});
|
|
822
1235
|
worktreeReap = () => reapOrphanWorktrees(gitBaseEnv, config.worktreeIsolation.repoRoot, logger);
|
|
823
1236
|
logger.info("worktree_isolation_enabled", { provider: "host", repoRoot: config.worktreeIsolation.repoRoot });
|
|
1237
|
+
// BOOT recovery sweep: reap any worktrees orphaned by a crash BEFORE this process started (the periodic
|
|
1238
|
+
// reaper would otherwise wait a full interval). Best-effort, fire-and-forget; never blocks boot.
|
|
824
1239
|
void worktreeReap();
|
|
825
1240
|
}
|
|
826
1241
|
else {
|
|
1242
|
+
// Not a no-op-silent: an operator who set WORKTREE_ISOLATION_ENABLED on a non-host lane should know it
|
|
1243
|
+
// doesn't take effect yet (the worktree-rooted REMOTE env is a follow-on). Fail-loud-ish via a warning.
|
|
827
1244
|
logger.warn("worktree_isolation_unsupported_lane", {
|
|
828
1245
|
provider: config.remoteExec?.provider,
|
|
829
1246
|
note: "git-worktree isolation v1 covers only the 'host' lane — isolation:'worktree' is a no-op here (the base env is used). Worktree-rooted remote envs are a follow-on.",
|
|
830
1247
|
});
|
|
831
1248
|
}
|
|
832
1249
|
}
|
|
1250
|
+
// SendUserFile v2(clay 拍 2026-07-14,Plan B 直传):沙箱 lane 的文件源需要「工具执行时拿到当前
|
|
1251
|
+
// 任务的 env」——core 把 per-task env 关在 prepare-task 闭包里(ToolExecuteContext 无 env 面),但
|
|
1252
|
+
// 工厂是我们装配的、工厂 ctx 自带 taskId,包一层登记 taskId→env(destroy 时注销)即可,零 core 改动
|
|
1253
|
+
// (withWorktreeIsolation 同款包装先例)。lane 门+租户门=sandboxSendLaneEnabled(判定依据在其 doc):
|
|
1254
|
+
// e2b/k8s=一任务一沙箱(沙箱文件系统=租户边界)任意租户可用;ssh(后补 2026-07-14)=同一 exec 契约
|
|
1255
|
+
// (timeout 秒×1000/Result shape 同形)直传链零适配,但 peer=一台跨任务共享的常驻真实主机
|
|
1256
|
+
// (sshExecutionEnvFactory 忽略 ctx、destroy=断连不删文件、isolation:false)→「沙箱 scoped 读=租户
|
|
1257
|
+
// 隔离」不成立,与 host 同门单用户 only(requirePrincipal!==true 恒关多租户,不留口子)。
|
|
1258
|
+
// 其余 lane 不包(零开销);签发面未配同样不包。
|
|
833
1259
|
const sendUserFileTaskEnvs = (() => {
|
|
834
1260
|
const provider = config.remoteExec?.provider;
|
|
835
1261
|
if (!config.sendUserFile || !executionEnvFactory || !sandboxSendLaneEnabled(provider, config.requirePrincipal))
|
|
@@ -838,6 +1264,11 @@ async function main() {
|
|
|
838
1264
|
executionEnvFactory = registry.wrapFactory(executionEnvFactory);
|
|
839
1265
|
return registry;
|
|
840
1266
|
})();
|
|
1267
|
+
// D-1 附件物化包装(半场③):env 每次建立(fresh + resume/重建沙箱)都把该 session 绑定的附件写进
|
|
1268
|
+
// 工作目录 `attachments/`。全 lane 统一(host/e2b/k8s/ssh/adb 都走 factory;ExecutionEnv.writeFile
|
|
1269
|
+
// 双向通用);路径与 objective 告知共用 materializedRelPaths(确定性,两处不漂)。**fail-loud**:
|
|
1270
|
+
// objective 已向模型宣告文件在场,静默缺文件=模型按幻影文件行动,比任务失败更糟 ⇒ 写失败即抛
|
|
1271
|
+
// (env 建立失败,任务带明确错误)。链尾最外层:worktree 隔离等内层先定 cwd,附件落在最终 cwd。
|
|
841
1272
|
if (taskAttachmentStore && executionEnvFactory) {
|
|
842
1273
|
const inner = executionEnvFactory;
|
|
843
1274
|
const attStore = taskAttachmentStore;
|
|
@@ -850,43 +1281,103 @@ async function main() {
|
|
|
850
1281
|
};
|
|
851
1282
|
}
|
|
852
1283
|
if (executionEnvFactory) {
|
|
1284
|
+
// k8s (Kata) is isolated; it becomes WORKSPACE-suspendable when an S3 snapshot store is configured
|
|
1285
|
+
// (tar→S3→fresh pod restore — files durable, in-VM memory not); e2b is fully suspendable.
|
|
853
1286
|
const isolated = config.remoteExec.provider === "e2b" ||
|
|
854
1287
|
config.remoteExec.provider === "k8s" ||
|
|
855
|
-
config.remoteExec.provider === "local-docker";
|
|
1288
|
+
config.remoteExec.provider === "local-docker"; // a container is a real OS-level isolation boundary
|
|
856
1289
|
const suspendable = config.remoteExec.provider === "e2b" ||
|
|
857
1290
|
(config.remoteExec.provider === "k8s" && !!config.remoteExec.s3Snapshot);
|
|
1291
|
+
// Out-of-band sandbox env (e2b): log the KEY NAMES only (never the secret values) so ops can confirm
|
|
1292
|
+
// injection is active without leaking the credential.
|
|
858
1293
|
const sandboxEnvKeys = config.remoteExec.provider === "e2b" ? Object.keys(config.remoteExec.sandboxEnv ?? {}) : [];
|
|
859
1294
|
logger.info("remote_exec_enabled", { provider: config.remoteExec.provider, isolated, suspendable, ...(sandboxEnvKeys.length > 0 ? { sandboxEnvKeys } : {}) });
|
|
860
1295
|
if (!isolated)
|
|
861
1296
|
logger.warn("remote_exec_real_system", { provider: config.remoteExec.provider, note: "non-isolated target — actions are permanent; rely on the policy gate + HITL (design/61 §5)" });
|
|
862
1297
|
}
|
|
1298
|
+
// LSP sidecar (design/64 §13.1, the 1.86.2 seam): ONE stateless manager on RunnerDeps — core passes each
|
|
1299
|
+
// task's env into sessionFor at the tool mount point, so the manager reaches the SAME sandbox the agent edits
|
|
1300
|
+
// (no sessionId registry / per-task construction). Opt-in (LSP_ENABLED — the SANDBOX lane's knob; the host lane
|
|
1301
|
+
// below has its own, LSP_HOST_ENABLED, because its default is the opposite); with the baked `sema-code-lsp`
|
|
1302
|
+
// template the first call is fast, without it the language server installs on first use (slow once per sandbox).
|
|
1303
|
+
// k8s lane (gate#2 alignment): same manager/bridge over the pod network — `ws://podIP:port` instead of E2B's
|
|
1304
|
+
// public wss proxy, so it needs an IN-CLUSTER worker and a sandbox image with node + the language servers baked
|
|
1305
|
+
// (no on-the-fly npm path on Kata; a miss degrades the lsp tool gracefully, same as E2B).
|
|
863
1306
|
const lspProvider = config.remoteExec?.provider;
|
|
864
1307
|
const lspManager = (lspProvider === "e2b" || lspProvider === "k8s") && config.lspEnabled
|
|
865
1308
|
? createE2bLspManager({ log: (event, fields) => logger.info(event, fields), scheme: lspProvider === "k8s" ? "ws" : "wss" })
|
|
866
|
-
:
|
|
1309
|
+
: // TOC local LSP (core 1.190): the host lane runs on THIS machine, so core's `NodeLspManager` spawns the
|
|
1310
|
+
// language server as a LOCAL child_process over stdio (CC `services/lsp` parity). Its default resolveRoot uses
|
|
1311
|
+
// `env.cwd` — and core passes each task's executionEnv (the per-agent WORKTREE env, so the server roots in the
|
|
1312
|
+
// worktree not the base repo) into sessionFor. host ONLY: ssh/adb target another host/device and local-docker a
|
|
1313
|
+
// container, none of which a local child_process can reach (those keep the E2B-style bridge / stay degraded).
|
|
1314
|
+
// 🔒 `requirePrincipal !== true` gate = uniform with the sibling host powers (scheduler/backgroundShell/
|
|
1315
|
+
// loadProjectMemory): the host lane is single-user by design; a (discouraged) multi-tenant host fails SAFE to
|
|
1316
|
+
// grep/read rather than fanning a per-tenant×language×root pool of heavy language servers (double-review B).
|
|
867
1317
|
lspProvider === "host" && config.lspHostEnabled && config.requirePrincipal !== true
|
|
868
1318
|
? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) })
|
|
869
1319
|
: undefined;
|
|
870
1320
|
if (lspManager)
|
|
871
1321
|
logger.info("lsp_enabled", { provider: lspProvider });
|
|
1322
|
+
// prove-before-scale stopgap: a no-op tool tracer (logs every tool call incl. resumed legs that
|
|
1323
|
+
// /events hides) so we can diagnose single-agent coding behaviour. Off unless TOOL_TRACE=true.
|
|
872
1324
|
const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
|
|
873
1325
|
if (toolTracer)
|
|
874
1326
|
logger.info("tool_trace_enabled", {});
|
|
1327
|
+
// Goal B4 (2026-07-08): the ALWAYS-ON gate-deny meter (`permission_denied_total{source}`) — an
|
|
1328
|
+
// operational/security signal that must not sit behind the TOOL_TRACE diagnostic switch. Composed
|
|
1329
|
+
// UNDER the tracer (deployment slot order: meter first, tracer second — composeHooks double-runs the
|
|
1330
|
+
// void permissionDenied slot); this composite is what every hooks site below uses as the deployment
|
|
1331
|
+
// baseline, so a task-hooks fold (resolveSpec) shadows neither.
|
|
875
1332
|
const deploymentHooks = toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics);
|
|
1333
|
+
// S8 / SVC-1 workflow run store (design/97 S1b): records LLM-authored BACKGROUND workflow
|
|
1334
|
+
// runs for the /v1/workflows list+detail view (live subscribe is in-process via subscribeWorkflow,
|
|
1335
|
+
// store-independent). Only present when self-orchestration is enabled (else no runs to record).
|
|
1336
|
+
//
|
|
1337
|
+
// SVC-1: the DEFAULT is now `FileWorkflowRunStore` (durable, crash-safe ledger) — NOT InMemory — so a
|
|
1338
|
+
// background workflow's record SURVIVES a replica restart, which is the prerequisite for the at-least-once
|
|
1339
|
+
// completion notify below to re-derive a run's terminal state after a crash. InMemory stays available only for
|
|
1340
|
+
// an explicit ephemeral opt-out (WORKFLOW_RUN_STORE=memory); a durable cross-replica TiDB/PG WorkflowRunStore
|
|
1341
|
+
// is a clean follow-on port (the workflowRunStoreContract makes it a drop-in like the run-store).
|
|
1342
|
+
// P1 (fleet failover, 2026-07-05): `auto` (the default) prefers the SQL backend's cross-replica twin when
|
|
1343
|
+
// present — a failover-landed session sees the run's history + its pending completion push on ANY replica.
|
|
1344
|
+
// Explicit WORKFLOW_RUN_STORE=file/memory still wins (single-box File posture unchanged: local backend has
|
|
1345
|
+
// no workflowRun() so auto falls to File there).
|
|
876
1346
|
const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.() : undefined;
|
|
877
1347
|
const baseWorkflowRunStore = config.selfOrchestrationEnabled
|
|
878
1348
|
? config.workflowRunStoreBackend === "memory"
|
|
879
1349
|
? new InMemoryWorkflowRunStore()
|
|
880
1350
|
: (sqlWorkflowRunStore ?? new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows")))
|
|
881
1351
|
: undefined;
|
|
1352
|
+
// SVC-1 at-least-once completion-notify TRUST GATE (the half that is service's, [[core-service-boundary]]):
|
|
1353
|
+
// core's `run_workflow` fires an in-process at-MOST-once notify on terminal — a crash between terminal and the
|
|
1354
|
+
// receiver loses it. The gate makes it at-LEAST-once: a durable notify-journal records every STARTED run
|
|
1355
|
+
// (observed via the JournalingWorkflowRunStore decorator's `put`), the LIVE notify path delivers-then-acks, and
|
|
1356
|
+
// a BOOT recovery sweep re-derives every un-acked run's terminal state from the run store + re-delivers (the
|
|
1357
|
+
// receiver is idempotent on runId). The default journal is the zero-dependency crash-safe File ledger; with no
|
|
1358
|
+
// run store (self-orchestration off) there's nothing to journal, so the gate is absent.
|
|
1359
|
+
// 1.108 review fix (lens③ HIGH): the journal FOLLOWS the run-store axis like the inbox — a SQL run store +
|
|
1360
|
+
// inbox with a replica-LOCAL File journal meant a replica that died holding an un-acked notify stranded it
|
|
1361
|
+
// forever (no surviving replica could recover it: at-least-once silently degraded to at-most-once across
|
|
1362
|
+
// replica death — the exact fleet-failover gap P1 exists to close).
|
|
882
1363
|
const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
|
|
883
1364
|
? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
|
|
884
1365
|
: undefined;
|
|
1366
|
+
// P1 ①②: the async-workflow COMPLETION INBOX — the push half. A finished background
|
|
1367
|
+
// workflow's completion is enqueued here keyed by the ORIGINATING session, and drained + emitted as a
|
|
1368
|
+
// `workflow_complete` out-of-band SSE frame when that session next opens a stream (see server.ts). File-backed
|
|
1369
|
+
// (crash-safe) when self-orchestration is on with a durable store; the ephemeral opt-out uses in-memory.
|
|
1370
|
+
// P1: the inbox FOLLOWS the run-store choice (one axis, no split-brain: a SQL run record with a File inbox
|
|
1371
|
+
// would re-open the cross-replica double-push the fence rows exist to close).
|
|
885
1372
|
const workflowCompletionInbox = config.selfOrchestrationEnabled
|
|
886
1373
|
? config.workflowRunStoreBackend === "memory"
|
|
887
1374
|
? new InMemoryWorkflowCompletionInbox((msg, meta) => logger.warn(msg, meta))
|
|
888
1375
|
: (sqlWorkflowRunStore ? backend.completionInbox((msg, meta) => logger.warn(msg, meta)) : new FileWorkflowCompletionInbox(join(config.localDataRoot ?? localRoot, "workflows"), (msg, meta) => logger.warn(msg, meta)))
|
|
889
1376
|
: undefined;
|
|
1377
|
+
// The deployment's REAL completion delivery: log + meter, THEN push into the originating session's inbox so the
|
|
1378
|
+
// model that launched the workflow learns it finished (the `WorkflowStatus` poll is the deterministic floor;
|
|
1379
|
+
// this is the proactive push). The gate guards this so it fires at-least-once + at-most-once-per-run-steady.
|
|
1380
|
+
// `runStore` (below) is resolved at CALL time (post-boot) — a forward reference into the same boot scope.
|
|
890
1381
|
const deliverWorkflowCompletion = async (p) => {
|
|
891
1382
|
logger.info("workflow_completed", {
|
|
892
1383
|
runId: p.runId,
|
|
@@ -894,12 +1385,20 @@ async function main() {
|
|
|
894
1385
|
...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
|
|
895
1386
|
});
|
|
896
1387
|
metrics.inc("workflow_runs_total", { status: p.status });
|
|
1388
|
+
// Routing (core 1.208): session = payload's `originatingSessionId` (lookup-free); owner = run-row
|
|
1389
|
+
// owner → payload `principal` (= VERIFIED spec.principal, the 1.55 F-fix invariant — covers the sync/resume
|
|
1390
|
+
// legs whose sourceTaskId is a sessionId and misses getRun) → multi-tenant fail-closed /
|
|
1391
|
+
// single-user null. Full rationale on `resolveCompletionRoute` (tested there).
|
|
897
1392
|
if (!workflowCompletionInbox)
|
|
898
1393
|
return;
|
|
899
1394
|
try {
|
|
900
1395
|
const route = await resolveCompletionRoute(p, runStore ? (id) => runStore.getRun(id) : undefined, config.requirePrincipal === true);
|
|
901
1396
|
if (!route)
|
|
902
|
-
return;
|
|
1397
|
+
return; // no route / unverifiable owner → the WorkflowStatus poll floor covers it
|
|
1398
|
+
// `p.summary` is HUMAN-READABLE at the source since core 1.232 (completed lane = bounded
|
|
1399
|
+
// one-liner with name/elapsed/agents + a TaskOutput pointer; failed lane = the bounded redacted error)
|
|
1400
|
+
// — pass through verbatim, single-sourced (the 1.101.0 service-side re-wrap was superseded by core's
|
|
1401
|
+
// own half and removed to avoid two drifting formats).
|
|
903
1402
|
await workflowCompletionInbox.enqueue({
|
|
904
1403
|
sessionId: route.sessionId,
|
|
905
1404
|
owner: route.owner,
|
|
@@ -918,33 +1417,93 @@ async function main() {
|
|
|
918
1417
|
onError: (stage, runId, err) => logger.warn("workflow_notify_failed", { stage, runId, err: String(err) }),
|
|
919
1418
|
})
|
|
920
1419
|
: undefined;
|
|
1420
|
+
// MF-Fleet (data contract): the in-process fleet aggregation bus backing GET /v1/fleet/stream. Created HERE
|
|
1421
|
+
// (before the workflow run store) so the JournalingWorkflowRunStore can publish workflow rows to it. Always
|
|
1422
|
+
// present (lightweight); the run (POST /v1/runs) + workflow lifecycle publish per-row deltas. Replica-local.
|
|
921
1423
|
const fleetBus = new FleetEventBus();
|
|
1424
|
+
// The store handed to core (+ the /v1/workflows reader): ALWAYS wrapped when a run store exists ([1262]
|
|
1425
|
+
// fleet decoupling) — the wrap publishes the workflow's MF-Fleet row on each put/update (the write-observation
|
|
1426
|
+
// point the shell's /workflows panel lives on), and ADDITIONALLY journals the start-time `put` when the notify
|
|
1427
|
+
// gate is active (durable backends). The old `baseWorkflowRunStore && workflowNotifyGate` guard silently
|
|
1428
|
+
// dropped the FLEET half on the memory backend (gate needs a durable journal; fleet needs neither) — clay's
|
|
1429
|
+
// workflow panel went empty on exactly that shape.
|
|
922
1430
|
const workflowRunStore = baseWorkflowRunStore
|
|
923
1431
|
? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
|
|
924
1432
|
: undefined;
|
|
1433
|
+
// SVC-2 (core CORE-7/CORE-9 Part A): the durable resume journal. core 1.145.0 added `RunnerDeps.workflowJournalStore`
|
|
1434
|
+
// (twin of workflowRunStore) + auto-wires it into the run_workflow tool's startWorkflow(RunWorkflowOptions.journalStore);
|
|
1435
|
+
// the tool's `resumeFromRunId` input then replays the longest unchanged prefix. tidb/pg = durable cross-replica resume;
|
|
1436
|
+
// local = core's InMemory (single-process — in-process resume only, not restart-durable). Only when self-orchestration
|
|
1437
|
+
// is on AND a backend is present (the load-bearing journal needs a real store).
|
|
925
1438
|
const workflowJournalStore = config.selfOrchestrationEnabled && backend ? backend.workflowJournal() : undefined;
|
|
1439
|
+
// design/73 §1 consumption sink (see RunnerDeps.onTaskOutcome below). Present whenever a StoreBackend is —
|
|
1440
|
+
// NOT gated on self-orchestration (goal-mode/harness emissions are orthogonal to workflows).
|
|
926
1441
|
const outcomeSink = backend?.outcomeSink();
|
|
1442
|
+
// SVC-5 (CORE-9 Part B): the process-local registry of STEERABLE workflow-agent handles live on THIS replica,
|
|
1443
|
+
// backing POST /v1/workflows/:id/agents/:label/steer. core 1.145.0 added `RunnerDeps.onWorkflowAgentSpawn` (the
|
|
1444
|
+
// opt-in deployment handle-sink): when set, the run_workflow tool's `agent()` runs steerable + emits the handle
|
|
1445
|
+
// HERE (the handle is a core object handed only to the trusted deployment sink — it NEVER enters the script/runner,
|
|
1446
|
+
// so the script-sandbox host-context membrane stays closed). We register by runId+label; the steer route looks it up + calls
|
|
1447
|
+
// handle.steer (core fences via CORE-5) behind our steer-in redaction + owner gate.
|
|
927
1448
|
const workflowAgentRegistry = config.selfOrchestrationEnabled ? new WorkflowAgentRegistry() : undefined;
|
|
1449
|
+
// C2 (core 1.219): the replica-local registry of STEERABLE Task-subagent handles, backing
|
|
1450
|
+
// POST /v1/runs/:runId/subagents/:target/steer. Core emits a handle to the per-run `onSubagentSpawn` sink
|
|
1451
|
+
// (wired in each run leg) the moment a SYNC delegation spawns; the handle never reaches the model. Always
|
|
1452
|
+
// present (lightweight Map) — the sink is only threaded where a durable runId exists to address it by.
|
|
928
1453
|
const subagentSteerRegistry = new SubagentSteerRegistry();
|
|
1454
|
+
// E23 (shell-host contract): inbound MCP elicitation coordinator (live-only HITL). Present ONLY when MCP_ELICITATION_ENABLED
|
|
1455
|
+
// — absent ⇒ onElicit is not wired ⇒ core advertises no elicitation capability to any server (fail-closed). Shared
|
|
1456
|
+
// by the runner (the onElicit seam) and the HTTP layer (the respond route + the per-run ALS context wraps).
|
|
929
1457
|
const elicitation = config.mcpElicitation.enabled ? new ElicitationCoordinator(config.mcpElicitation.throttle) : undefined;
|
|
1458
|
+
// §4④: AskUserQuestion LIVE-stream HITL coordinator — the sibling of `elicitation`. Present ⇒ wired onto
|
|
1459
|
+
// `RunnerDeps.onQuestion` (core mounts the AskUserQuestion tool + routes live asks here) AND consumed by the HTTP layer
|
|
1460
|
+
// (the `POST /v1/questions/:id/respond` route + the per-run ALS context wraps on the streaming legs). Absent ⇒ core
|
|
1461
|
+
// mounts the tool with the headless default (a run never hangs; the model just can't get a live answer).
|
|
930
1462
|
const question = config.askQuestionEnabled ? new QuestionCoordinator() : undefined;
|
|
1463
|
+
// [816]/[820]②: the live tool-approval HITL coordinator — the sibling of `question` on core's `RunnerDeps.onAsk`
|
|
1464
|
+
// seam (1.290 sync-ask leg). Present ⇒ a policy `ask` on a LIVE streaming leg becomes a `tool_approval` frame the
|
|
1465
|
+
// shell renders as the CC three-choice card, answered via POST /v1/tool-approvals/:id/respond.
|
|
1466
|
+
// [1535] 正名(旧文「background/workflow ⇒ headless auto-deny stands」已过时):宿主 sync 腿现同时装
|
|
1467
|
+
// `spec.onAsk = boundAsk(ctx)`(server.ts 装配点)——core 继承链把闭包冻给委派子代,**bg/嵌套子代的
|
|
1468
|
+
// ask 浮到宿主 live 流**(带 sourceTaskId);真正无宿主流的腿(durable-submit/headless resume)才留
|
|
1469
|
+
// 「unavailable → durable park;无 park 设施 core fail-closed deny」。durable 部署 G1 语义不变。
|
|
931
1470
|
const toolApproval = config.toolApprovalEnabled ? new ToolApprovalCoordinator() : undefined;
|
|
1471
|
+
// [1.294 G1] durable 姿势必须在 runnerDeps 字面量之前可判(onAsk 的「在场性」本身就是 core suspendAsk 的
|
|
1472
|
+
// 分路条件,不能再靠闭包惰性读)——判据与 checkpointStore 的构造条件同源(backend?.checkpoint 存在 ∧
|
|
1473
|
+
// DURABLE_APPROVAL;store 本体已上移至 subRunner 构造前,[1535] 断点② 双 Runner 挂载)。
|
|
932
1474
|
const durableEnabled = backend?.checkpoint !== undefined && config.durableApproval === true;
|
|
1475
|
+
// SendUserFile(真 CC 契约,clay 2026-07-14)两种 lane 形态,其余缺席=诚实(工具不进 roster):
|
|
1476
|
+
// - host lane:本地盘读+服务端上传,【单用户 only】——host 多租户=任意路径读→公网 URL 的 exfil 面,
|
|
1477
|
+
// clay 拍永久关死,不留口子。路径=绝对或相对 process.cwd(用户目录语义,与 CC 一致)。
|
|
1478
|
+
// - e2b/k8s lane(v2,Plan B 直传):沙箱内 stat+`curl -T` 打 server 预签的单对象 PUT(字节不中转、
|
|
1479
|
+
// 凭据不进沙箱);沙箱自身文件系统=租户边界,故任意租户可用。
|
|
1480
|
+
// - ssh lane(后补 2026-07-14):同一直传链,但 peer=跨任务共享的真实主机(非租户边界)→
|
|
1481
|
+
// 【单用户 only】,租户门在 sandboxSendLaneEnabled(多租户下注册表不建,工具诚实缺席)。
|
|
1482
|
+
// adb/local-docker 仍后补。
|
|
1483
|
+
// 签发面缺 S3_PUBLIC_ENDPOINT 时 issue-time fail-loud(工具在但用即报修法);100MiB/文件帽在各 send 面。
|
|
933
1484
|
const sendUserFileEmitter = (() => {
|
|
934
1485
|
if (!config.sendUserFile)
|
|
935
1486
|
return undefined;
|
|
936
1487
|
if (sendUserFileTaskEnvs)
|
|
937
|
-
return new SendUserFileEmitter();
|
|
1488
|
+
return new SendUserFileEmitter(); // e2b/k8s/ssh(单用户):注册表已就位(lane+租户+签发面判定)
|
|
1489
|
+
// unset REMOTE_EXEC(in-process/TOC 本地形态)有意归入 host 语义:exec 与文件都在本机,单用户门
|
|
1490
|
+
// 同样把关(codex 复审提出收紧到显式 "host";判定=不收——unset 单机+配了 S3 是合法 dev/TOC 场景,
|
|
1491
|
+
// 且与 1.187.0 已发行为一致;真正的红线是 requirePrincipal 多租户,这里恒关)。
|
|
938
1492
|
const provider = config.remoteExec?.provider ?? "host";
|
|
939
1493
|
if (provider === "host" && config.requirePrincipal !== true)
|
|
940
1494
|
return new SendUserFileEmitter();
|
|
941
1495
|
return undefined;
|
|
942
1496
|
})();
|
|
1497
|
+
// SendUserFile 账本(多租户治理面,[768]②a 后半):key 里的 scope 段是哈希(URL 不泄 principal),
|
|
1498
|
+
// 按租户列/撤销的 scope↔对象映射记在 server 侧账本(四后端孪生,sendfile_link / JSONL)。有 backend 即建
|
|
1499
|
+
// (签发面也配了才有写点);无 backend(env-only worker)= 无账本 = 不记账不 501 治理面(诚实缺席,
|
|
1500
|
+
// capabilities.sendUserFileLedger=false)。
|
|
943
1501
|
const sendFileLedger = config.sendUserFile && backend ? backend.sendFileLedger() : undefined;
|
|
944
1502
|
const sendUserFileToolSpec = (() => {
|
|
945
1503
|
if (!sendUserFileEmitter || !config.sendUserFile)
|
|
946
1504
|
return undefined;
|
|
947
1505
|
const issuer = createSendUserFileIssuer(config.sendUserFile);
|
|
1506
|
+
// 两条 lane 的 send 都把 ctx.principal(core VERIFIED)传给签发面 → key 带哈希 scope 段 + link 携原文 scope。
|
|
948
1507
|
const rawSend = sendUserFileTaskEnvs
|
|
949
1508
|
? createSandboxFileSend({
|
|
950
1509
|
registry: sendUserFileTaskEnvs,
|
|
@@ -959,10 +1518,13 @@ async function main() {
|
|
|
959
1518
|
if (st.size > SEND_USER_FILE_MAX_BYTES)
|
|
960
1519
|
throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${st.size} bytes)`);
|
|
961
1520
|
const bytes = await fsReadFile(resolved);
|
|
1521
|
+
// 读后复验(codex 复审 MED):stat→read 间文件可增长——上传的是读到的字节,帽必须按真实字节数把关。
|
|
962
1522
|
if (bytes.byteLength > SEND_USER_FILE_MAX_BYTES)
|
|
963
1523
|
throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${bytes.byteLength} bytes)`);
|
|
964
1524
|
return issuer.issue(bytes, basename(resolved), undefined, sendCtx.principal);
|
|
965
1525
|
};
|
|
1526
|
+
// 账本写点(两 lane 同语义):上传+verify 成功后、file_link 帧前记账;记账失败=该文件 fail-loud +
|
|
1527
|
+
// best-effort 删已传对象(治理面必须有行;删失败 warn)。无账本(env-only)= 直通,行为与 1.189 一致。
|
|
966
1528
|
const send = sendFileLedger
|
|
967
1529
|
? withLedgerRecording(rawSend, {
|
|
968
1530
|
ledger: sendFileLedger,
|
|
@@ -972,11 +1534,43 @@ async function main() {
|
|
|
972
1534
|
: rawSend;
|
|
973
1535
|
return sendUserFileTool({ send, emitter: sendUserFileEmitter });
|
|
974
1536
|
})();
|
|
1537
|
+
// design/99 §K (core 1.157 `RunnerDeps.runtimeCapsResolver`) — the ENFORCE last-link of the
|
|
1538
|
+
// three-stage workflows gate. core calls it once per task at prepare time with `spec.principal`; we resolve the
|
|
1539
|
+
// per-principal entitlement from center's `GET /api/config/effective?principal=` (cached + fail-closed). Wired
|
|
1540
|
+
// ONLY when a center is configured AND not in dry-run: in dry-run the service pulls+logs but does NOT APPLY
|
|
1541
|
+
// center config, so it must NOT ENFORCE center's per-principal caps either (enforcing would deny workflows = a
|
|
1542
|
+
// real behavior change, contradicting observe-only). Absent ⇒ core sees `undefined` ⇒ no per-principal
|
|
1543
|
+
// restriction (the deployment-level capability + toolPolicy gates still govern).
|
|
1544
|
+
//
|
|
1545
|
+
// 🔐 TRUST DEPENDENCY: the gate keys off `spec.principal` (= resolveSpec's `auth.principal`). F-fix (2026-07-01,
|
|
1546
|
+
// core-steered; contract-compliance per core 1.187's hardened `TaskSpec.principal` doc — it MUST be the
|
|
1547
|
+
// cryptographically VERIFIED identity, never a spoofable header): `auth.principal` now comes from
|
|
1548
|
+
// `verifiedPrincipal` (security.ts). On a GATED door that's `principalFrom` (the BFF already verified the header;
|
|
1549
|
+
// `gatedPrincipal === principalFrom` there); on a DIRECT door (`directDoorActive`) it's the verified
|
|
1550
|
+
// `x-approval-principal-token` JWT `sub`, NEVER the spoofable header. So `spec.principal` is ALWAYS the verified
|
|
1551
|
+
// identity — governance (this gate), cost, and isolation key off it safely on BOTH postures. The same verified
|
|
1552
|
+
// identity also fans out to the non-authorizer owner/cost sites that don't run createAuthorizer
|
|
1553
|
+
// (runOwnerOk / quotaExceeded / leader / idemKey — they call `gatedPrincipal` directly). The invariant:
|
|
1554
|
+
// spec.principal AND every principal-keyed security decision are VERIFIED. (Was a real direct-door spoof hole
|
|
1555
|
+
// before this fix; latent until a multi-tenant direct door ships, but it is the only wall against it.)
|
|
1556
|
+
// wpt_ scoped-token: RESOLVED (center ruling, sema-registry `aeb4979`). The earlier 1.14.0 guard
|
|
1557
|
+
// (skip the resolver on a `wpt_` token, because center 403'd it for `?principal=`) is GONE — center now lets a
|
|
1558
|
+
// worker-scoped `wpt_<self>` resolve its OWN worker's principal caps when we pass `?worker=<self>` (caps-only,
|
|
1559
|
+
// never others' config). So we ALWAYS wire the resolver (configCenter + !dryRun) and pass `worker` =
|
|
1560
|
+
// `SEMA_REGISTRY_WORKER` — it authorizes the wpt_ path and is harmless on a full token (caps are worker-
|
|
1561
|
+
// independent). Per-principal workflow gating now truly works on the scoped-token topology (no more silent
|
|
1562
|
+
// fleet-wide deny). (A wpt_ with no worker still 403s → fail-closed deny, the correct degrade.)
|
|
1563
|
+
// MISCONFIG diagnostic (review MEDIUM): a wpt_<self> token with NO SEMA_REGISTRY_WORKER will 403 every caps
|
|
1564
|
+
// fetch → fail-closed DENY ALL workflows (correct degrade, but otherwise only per-task warns). One-shot BOOT
|
|
1565
|
+
// warning so it's diagnosable. (The orchestrator normally injects the worker alongside a wpt_ — hand-misconfig.)
|
|
975
1566
|
if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
|
|
976
1567
|
logger.warn("runtime_caps_scoped_token_no_worker", {
|
|
977
1568
|
reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
|
|
978
1569
|
});
|
|
979
1570
|
}
|
|
1571
|
+
// ONE per-principal caps client serves two faces off the same fetch/cache — `resolveRuntimeCaps`
|
|
1572
|
+
// (core's seam, fail-closed) and `scenarioRuling` (resolveSpec's scenario gate, fail-open; center attaches the
|
|
1573
|
+
// resolved {scenario, allowlist} to the caps body, single semantic source in center resolve-scenario.ts).
|
|
980
1574
|
const principalCaps = config.configCenter && !config.configCenter.dryRun
|
|
981
1575
|
? createPrincipalEntitlementsClient({
|
|
982
1576
|
baseUrl: config.configCenter.baseUrl,
|
|
@@ -985,23 +1579,55 @@ async function main() {
|
|
|
985
1579
|
onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
|
|
986
1580
|
})
|
|
987
1581
|
: undefined;
|
|
1582
|
+
// 「entitlement resolver wired」语义源(enableForkFromBody / selfOrchestrationFromBody 的多租户 fail-close
|
|
1583
|
+
// 判别):只有 center 背书的 caps client 算数 —— 下面的 env observer 基线不是 entitlement 源,不得改变它。
|
|
988
1584
|
const centerRuntimeCapsResolver = principalCaps?.resolveRuntimeCaps;
|
|
1585
|
+
// observer 开闸线 env 半场:EXPERIMENTAL_OBSERVER_AGENTS=true → 单用户部署把
|
|
1586
|
+
// allowObservers:true 作部署基线合成(center caps 带键则 center 赢);多租户不认 env(boot warn 一次,
|
|
1587
|
+
// 行为零变)。center caps 的 allowObservers 键本身在 toCoreRuntimeCaps 宽读透传(缺键=core 默认 OFF)。
|
|
989
1588
|
const runtimeCapsResolver = applyObserverEnvOptIn(centerRuntimeCapsResolver, {
|
|
990
1589
|
experimentalObserverAgents: config.experimentalObserverAgents,
|
|
991
1590
|
requirePrincipal: config.requirePrincipal,
|
|
992
1591
|
warn: (msg, fields) => logger.warn(msg, fields),
|
|
993
1592
|
});
|
|
1593
|
+
// S8 (design/98) + SVC-1: mount core's `run_workflow` under the hardened-vm sandbox when
|
|
1594
|
+
// enabled (default OFF → core fail-closed, tool not mounted, zero impact). The tool STARTS a workflow and
|
|
1595
|
+
// returns its runId IMMEDIATELY (the background-run half — the caller never blocks); the workflow runs to
|
|
1596
|
+
// terminal in the background and fires core's in-process completion notify. Workflow-spawned children inherit
|
|
1597
|
+
// a CONSERVATIVE baseline (handsReadOnly — read-only by default; loosen deliberately); the script may pick
|
|
1598
|
+
// ONLY allow-listed model names (empty ⇒ it cannot pick → the workflow's default role).
|
|
1599
|
+
//
|
|
1600
|
+
// design/158 S4:三处装配点里**唯一**一组多键条件段 —— 提成带类型标注的中间 const 再整体展开进
|
|
1601
|
+
// runnerDeps。标注位 `Pick<RunnerDeps, …>` 本身触发 TS 多余属性检查(键名写错 / 键根本不属于
|
|
1602
|
+
// RunnerDeps ⇒ 编译期红),所以这不是把旁路挪个地方;组内可选键一律写成显式
|
|
1603
|
+
// `k: cond ? expr : undefined`,不再用 `...(cond ? { k } : {})`。
|
|
994
1604
|
const workflowModelAllowlist = workflowModelAllowlistFor(config);
|
|
995
1605
|
const selfOrchestrationDeps = config.selfOrchestrationEnabled
|
|
996
1606
|
? {
|
|
1607
|
+
// worker_thread isolation (heap cap + terminate) when opted in (multi-tenant DoS hardening), else
|
|
1608
|
+
// the faster in-process runner. Both run the same conformance-validated + probe-hardened membrane.
|
|
997
1609
|
workflowScriptRunner: config.selfOrchestrationWorkerIsolation
|
|
998
1610
|
? createWorkerHardenedVmRunner()
|
|
999
1611
|
: createHardenedVmRunner(),
|
|
1000
1612
|
workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
|
|
1613
|
+
// SVC-2 (CORE-9 Part A): durable resume journal — core auto-wires it into the tool's startWorkflow, the
|
|
1614
|
+
// `resumeFromRunId` tool input then replays the unchanged prefix (cross-replica when tidb/pg-backed). The
|
|
1615
|
+
// store enforces scope in its WHERE (CORE-9 audit BLOCKER: a cross-tenant resumeFromRunId → empty → live).
|
|
1001
1616
|
workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
|
|
1617
|
+
// 切片 1.5 / design/140 §6: the named-workflow registry seam — a COMPOSITE store.
|
|
1618
|
+
// persist/load = core's file store under `<localDataRoot>/workflow-scripts` (the CC scriptPath-iterate
|
|
1619
|
+
// face: every invocation's script lands on disk, the model edits + re-invokes with {scriptPath};
|
|
1620
|
+
// containment is the file store's contract). resolveName consults the collab projection FIRST (center
|
|
1621
|
+
// collab templates as `{TEAM_DISCUSSION_SCRIPT, defaultArgs}` entries; a collab id equal to a built-in
|
|
1622
|
+
// name SHADOWS it — §6 1c) and falls back to the file store's `<name>.js` face (an operator-saved
|
|
1623
|
+
// script is a named workflow too). list() = collab entries only (the file store deliberately doesn't
|
|
1624
|
+
// enumerate — its dir mixes per-run scripts with saved names, core contract note); sync, so the
|
|
1625
|
+
// entries reach the statically-built Workflow tool card.
|
|
1002
1626
|
workflowScriptStore: (() => {
|
|
1003
1627
|
const fileStore = createFileWorkflowScriptStore(join(config.localDataRoot ?? localRoot, "workflow-scripts"));
|
|
1004
1628
|
return {
|
|
1629
|
+
// core 1.366 (B-4) scope 分区一致性标:persist/load 直转 core file 实现(per-scope 子目录,
|
|
1630
|
+
// 真分区)——标是诚实声明;缺标=core 工具读侧拒/写侧跳(编译期必填字面量即为此设计)。
|
|
1005
1631
|
scopePartitioned: true,
|
|
1006
1632
|
persist: fileStore.persist.bind(fileStore),
|
|
1007
1633
|
load: fileStore.load.bind(fileStore),
|
|
@@ -1009,22 +1635,55 @@ async function main() {
|
|
|
1009
1635
|
list: () => listCollabWorkflows(),
|
|
1010
1636
|
};
|
|
1011
1637
|
})(),
|
|
1638
|
+
// SVC-5 (CORE-9 Part B): the opt-in steer handle-sink. When set, the tool's `agent()` runs STEERABLE and
|
|
1639
|
+
// emits the live handle HERE (never into the script/runner — the host-context membrane stays closed). We
|
|
1640
|
+
// register it by runId+label so POST /v1/workflows/:id/agents/:label/steer can route a steer to it, and
|
|
1641
|
+
// unregister when the agent settles (result() settles on stream completion; a stale handle would only
|
|
1642
|
+
// 409 `steering.not_running` anyway — this just bounds the map). A steered agent is NOT journaled (CORE-9.1
|
|
1643
|
+
// limitation: its resume re-runs live), which is correct + documented.
|
|
1012
1644
|
onWorkflowAgentSpawn: workflowAgentRegistry
|
|
1013
1645
|
? (handle) => {
|
|
1014
1646
|
const unregister = workflowAgentRegistry.register(handle);
|
|
1015
1647
|
void Promise.resolve(handle.result()).then(unregister, unregister);
|
|
1016
1648
|
}
|
|
1017
1649
|
: undefined,
|
|
1650
|
+
// workflowSizeGuideline: advisory size guidance injected into the
|
|
1651
|
+
// Workflow tool card (core Wvs, CC 206 verbatim). Absent/unrestricted = byte-compat card.
|
|
1018
1652
|
workflowLimits: config.workflowSizeGuideline ? { sizeGuideline: config.workflowSizeGuideline } : undefined,
|
|
1019
1653
|
workflowGovernanceBaseline: {
|
|
1654
|
+
// [824]① clay 拍 A 案(workflow 权限全面 CC parity):默认基线撤 handsReadOnly 钳 —— 主 LLM 与
|
|
1655
|
+
// 子 agent 同 root 同信任域,主 LLM 本就能写这棵树([816] ask 门照管),单独钳子 agent 的安全增益≈0
|
|
1656
|
+
// (只防绕路不防直路的门不是边界);实测产品代价=[814]A 死锁(写型 workflow agent 永远只读)。并发
|
|
1657
|
+
// 写互踩按 CC 同姿用建议解决(worktree 契约话术+design/111 advisory),不用强制钳。要保守的 TOB
|
|
1658
|
+
// 部署自己开 WORKFLOW_AGENTS_READONLY=true(config.workflowAgentsReadOnly)加回旧钳。
|
|
1020
1659
|
base: config.workflowAgentsReadOnly ? { handsReadOnly: true } : {},
|
|
1660
|
+
// [824]②/[826]-core 1.291 worktree seam(TOB 旋钮回归腿):旋钮开启时 worktreeBase 配成
|
|
1661
|
+
// overlay 形状 —— core 对 `agent({isolation:"worktree"})` 做 base 浅合并(非整体替换,
|
|
1662
|
+
// workflow-primitives effectiveBaseline:{...base,...worktreeBase}),{handsReadOnly:false} 语义=
|
|
1663
|
+
// 「只放写,其余基线全保留」。效果:保守部署里非隔离子 agent 仍只读,进了自有 worktree 的
|
|
1664
|
+
// agent 恢复可写([814]A 写型 workflow 在 TOB 形态下不死锁)。A 案默认(旋钮 off)不配 worktreeBase。
|
|
1021
1665
|
worktreeBase: config.workflowAgentsReadOnly ? { handsReadOnly: false } : undefined,
|
|
1666
|
+
// Model allowlist for the script's `agent({model})` picks: explicit
|
|
1667
|
+
// SELF_ORCHESTRATION_MODELS wins; single-user defaults to the deployment's own catalog; multi-tenant
|
|
1668
|
+
// keeps core's fail-closed empty default. Decision + rationale in workflowModelAllowlistFor.
|
|
1022
1669
|
workflowModelAllowlist: workflowModelAllowlist ? workflowModelAllowlist : undefined,
|
|
1023
1670
|
},
|
|
1671
|
+
// SVC-1 at-least-once notify: route core's terminal notify THROUGH the gate (deliver-then-ack +
|
|
1672
|
+
// journal dedup) instead of straight to log/meter, so a crash mid-delivery re-delivers on the next boot
|
|
1673
|
+
// recovery sweep. When the gate is absent (memory backend / no journal) fall back to the bare
|
|
1674
|
+
// log+meter delivery (at-most-once, the prior behavior — honest about the lost-on-crash window).
|
|
1024
1675
|
workflowCompletionNotifier: {
|
|
1676
|
+
// ⚠️ design/158 S4 有意保留的展开:两臂**都非空**(要么 gate 造的 notifier,要么裸 notify 兜底),
|
|
1677
|
+
// 不是「条件键」形——它合并的是同一必填键 `notify` 的两个来源,两来源都有类型,不构成
|
|
1678
|
+
// 多余属性检查旁路(缺 notify 反而当场编译红)。
|
|
1025
1679
|
...(workflowNotifyGate
|
|
1026
1680
|
? workflowNotifyGate.buildNotifier()
|
|
1027
1681
|
: { notify: (input) => deliverWorkflowCompletion(input) }),
|
|
1682
|
+
// core 1.232 (the CORE half of the poll-then-also-notify dedup): pollWorkflow fires this
|
|
1683
|
+
// ONCE when the ORIGINATING session polls the run to a real terminal (timeout/cross-session polls
|
|
1684
|
+
// don't) — drop that (session, runId)'s pending inbox entry + arm the served fence, so the push
|
|
1685
|
+
// that would duplicate what the model just read is dead even across a shell restart. Session
|
|
1686
|
+
// attribution mirrors resolveCompletionRoute's session half (payload id, else the run row).
|
|
1028
1687
|
ackServed: async (input) => {
|
|
1029
1688
|
if (!workflowCompletionInbox)
|
|
1030
1689
|
return;
|
|
@@ -1032,6 +1691,10 @@ async function main() {
|
|
|
1032
1691
|
const sid = await resolveServedSession(input, runStore ? (id) => runStore.getRun(id) : undefined);
|
|
1033
1692
|
if (sid) {
|
|
1034
1693
|
await workflowCompletionInbox.markTerminalServed(sid, input.runId);
|
|
1694
|
+
// The THIRD way an inbox entry disappears (besides the three stream-open drains
|
|
1695
|
+
// and the owner-mismatch drop) — the model itself polled the run to terminal, so the push is
|
|
1696
|
+
// suppressed. An "enqueue → ack within ms, but the shell showed nothing" report with THIS line
|
|
1697
|
+
// means the completion went to the MODEL in-process (core injected it), not onto any stream.
|
|
1035
1698
|
logger.info("workflow_complete_ack_served", { route: "poll-served", sessionId: sid, runId: input.runId });
|
|
1036
1699
|
}
|
|
1037
1700
|
}
|
|
@@ -1043,28 +1706,63 @@ async function main() {
|
|
|
1043
1706
|
}
|
|
1044
1707
|
: {};
|
|
1045
1708
|
const runnerDeps = {
|
|
1709
|
+
// e/b([985]):center catalog 轴的 core 消费面——candidate/pinned-digest 解析走这里(prepare-task
|
|
1710
|
+
// dist:session_start 优先 candidate、resume 按 digest 解析、miss=fail-loud prompt_snapshot_unavailable)。
|
|
1046
1711
|
promptSource,
|
|
1712
|
+
// [1070]① agent-team S1:持久名册 seam(具名 spawn advisory 写入;SendMessage 活注册表 miss 后咨询)。
|
|
1047
1713
|
rosterStore: rosterStore ? rosterStore : undefined,
|
|
1714
|
+
// core 1.364 durable bg agents 读半场(写半场=scenarioDeps.backgroundAgentStore 同实例,组装区注释)。
|
|
1048
1715
|
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
1716
|
+
// S3c tier-3 懒复活(core 1.374):SendMessage 链内消费,与 backgroundAgentStore 同实例配套挂载。
|
|
1049
1717
|
mailboxStore: mailboxStore ? mailboxStore : undefined,
|
|
1050
1718
|
brain,
|
|
1051
1719
|
models: config.models,
|
|
1052
1720
|
roles: config.roles,
|
|
1721
|
+
// core 1.265: the active tier table (tier words + CC aliases → catalog keys, expandTiers at Runner
|
|
1722
|
+
// construction; empty = INERT by core contract). Same mutateInPlace reference applyEffective fills — a
|
|
1723
|
+
// refresh-time tier change is restart-to-apply, same tier as models.
|
|
1053
1724
|
tiers: config.tiers,
|
|
1054
1725
|
pricing,
|
|
1055
1726
|
tracer,
|
|
1727
|
+
// [931]① clay 拍(core 1.300 BREAKING:缺省不署 Co-Authored-By,署名=产品身份资产归部署):
|
|
1728
|
+
// branded 形态(local provider = Sema 产品线,scenarios brandIdentity 同判据)commit 尾注接 Sema 署名;
|
|
1729
|
+
// 非 brand 部署维持 core 新缺省(不署)。seam=RunnerDeps.hands.commitCoAuthor。
|
|
1056
1730
|
hands: config.configProvider === "local" ? { commitCoAuthor: "Sema <noreply@vivi-ai.com>" } : undefined,
|
|
1731
|
+
// design/73 §1 (core 1.226 seam, clay 拍 2026-07-04 接): consume mechanical TaskOutcome facts into the
|
|
1732
|
+
// outcome ledger — tidb/pg = SQL rows (coreOutcomeToLedgerRow mapping + verbatim `core_outcome` JSON so
|
|
1733
|
+
// red-line ② oracleHadRedRun survives lossless), local = owner-only JSONL. Read-only v1: records facts,
|
|
1734
|
+
// drives NO policy (§7.4 backtest gate stands). Fire-and-forget — a sink failure logs and never touches
|
|
1735
|
+
// the run (core's emitTaskOutcome swallow-guards too). Emitters today: core runGoal terminal + any
|
|
1736
|
+
// harness with a REAL mechanical oracle calling runner.emitTaskOutcome; plain runTask never auto-emits.
|
|
1057
1737
|
onTaskOutcome: outcomeSink
|
|
1058
1738
|
? (o) => {
|
|
1059
1739
|
metrics.inc("task_outcomes_total", { status: o.status, green: String(o.oracle?.green ?? "unknown") });
|
|
1060
1740
|
void outcomeSink.recordCore(o).catch((err) => logger.warn("task_outcome_record_failed", { runId: o.runId, err: String(err) }));
|
|
1061
1741
|
}
|
|
1062
1742
|
: undefined,
|
|
1743
|
+
// E23: the live-only inbound-elicitation seam. core invokes it only for servers that opted in via
|
|
1744
|
+
// McpServerSpec.elicitation (default OFF) AND only when this is wired — both must hold (doubly fail-closed).
|
|
1063
1745
|
onElicit: elicitation ? elicitation.elicit : undefined,
|
|
1746
|
+
// §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here; the
|
|
1747
|
+
// DURABLE leg's spec.onQuestion (QUESTION_AWAITS_RESUME) OVERRIDES this per-task so a disconnected-human ask suspends.
|
|
1064
1748
|
onQuestion: question ? question.question : undefined,
|
|
1749
|
+
// [816]/[820]②: the live tool-approval seam (core `resolveAsk` — `spec.onAsk ?? deps.onAsk`). ALS-routed like
|
|
1750
|
+
// onQuestion: a leg wrapped by the coordinator's runWithContext reaches the human.
|
|
1751
|
+
//
|
|
1752
|
+
// [879] G1 终态(core 1.295 OnAsk 三值化):恒 wire。回调逐 ask 时刻判活人——ALS 附着腿 ⇒ 同步三选卡;
|
|
1753
|
+
// 无附着/卡送达失败 ⇒ 返 "unavailable",core 以 approverUnavailable 回路把该 ask 交回 suspendAsk 走
|
|
1754
|
+
// durable park(prepare-task 分路带 `approverUnavailable !== true` 豁免位,dist 亲读)——park 与 live 卡
|
|
1755
|
+
// 两全,1.199 的「durable 部署不 wire deps.onAsk」止血撤除。无 park 设施的部署 core 自己 fail-closed
|
|
1756
|
+
// deny(resolveAsk 的 unavailable 文案),与旧姿势同向。
|
|
1065
1757
|
onAsk: toolApproval
|
|
1066
1758
|
? (req, signal) => toolApproval.ask(req, signal)
|
|
1067
1759
|
: undefined,
|
|
1760
|
+
// [822]② auto permission mode, deployment half: RunnerDeps.autoMode is the operator TRUST face of core's
|
|
1761
|
+
// auto-mode classifier (1.276/1.277) — wiring it alone arms NOTHING (core requires the per-principal entitlement
|
|
1762
|
+
// runtimeCaps.autoMode === true from center, fail-closed dark by default; see runtime-caps-resolver.ts). With
|
|
1763
|
+
// both present, core screens every policy `ask` through the classifier (allow safe / deny hostile / leave the
|
|
1764
|
+
// rest on the ask path → this bridge or durable park). Defaults-only config (core's rules/window); breaker-open
|
|
1765
|
+
// is surfaced for observability.
|
|
1068
1766
|
autoMode: {
|
|
1069
1767
|
onBreakerOpen: (info) => {
|
|
1070
1768
|
metrics.inc("auto_mode_breaker_open_total");
|
|
@@ -1072,8 +1770,14 @@ async function main() {
|
|
|
1072
1770
|
},
|
|
1073
1771
|
},
|
|
1074
1772
|
sessionStore,
|
|
1773
|
+
// design/138 S1: the memory-engine switch — when present (single-user + engine on), core replaces the
|
|
1774
|
+
// legacy memory path WHOLESALE per task (materialize → session file ops → harvest; no remember/recall
|
|
1775
|
+
// tools). `memoryEngineDir` = the resolved config root (core derives the B3 control plane beside the
|
|
1776
|
+
// memory dir). Absent (multi-tenant / MEMORY_ENGINE=off) ⇒ no deps.memoryBackend ⇒ memory dark.
|
|
1075
1777
|
memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
|
|
1076
1778
|
memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
|
|
1779
|
+
// S3-TOB 复审 F-9(operator 可观测底座):harvest 报告 → metrics(拒收/incident/patch 计数从此可见;
|
|
1780
|
+
// core swallow-guard 保证 throwing consumer 不伤边界)。
|
|
1077
1781
|
onMemoryHarvestReport: memoryEngine
|
|
1078
1782
|
? (report, info) => {
|
|
1079
1783
|
metrics.inc("memory_harvest_total", { ok: String(report.ok), phase: info.phase, incident: report.incident?.kind ?? "none" });
|
|
@@ -1081,47 +1785,89 @@ async function main() {
|
|
|
1081
1785
|
metrics.inc("memory_harvest_patches_total", { phase: info.phase }, (report.patches.add ?? 0) + (report.patches.update ?? 0));
|
|
1082
1786
|
if (report.incident)
|
|
1083
1787
|
logger.warn("memory_harvest_incident", { kind: report.incident.kind, phase: info.phase });
|
|
1788
|
+
// 142-S2.5-W1: 成功 harvest 真有 patch 落地 = 本地记忆变了 ⇒ fire-and-forget 一轮同步
|
|
1789
|
+
// (trigger 自带 inflight 节流:上一轮在飞则跳过,漏掉的变更下一轮全量补上)。
|
|
1084
1790
|
if (memorySyncRunner && report.ok && (report.patches?.add ?? 0) + (report.patches?.update ?? 0) > 0)
|
|
1085
1791
|
memorySyncRunner.trigger("harvest");
|
|
1086
1792
|
}
|
|
1087
1793
|
: undefined,
|
|
1088
1794
|
toolResultStore,
|
|
1795
|
+
// E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
|
|
1796
|
+
// carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
|
|
1089
1797
|
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
|
|
1798
|
+
// design/99 §K: per-principal runtime entitlements (allowWorkflows / forceDurableGate) resolved from center.
|
|
1799
|
+
// Opt-in: undefined ⇒ NO per-principal restriction (tighten-only; deployment default governs). See above.
|
|
1090
1800
|
runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
|
|
1801
|
+
// E19: working-tree snapshot/restore for rewind (+ the 2c artifact store). core snapshots each completed turn +
|
|
1802
|
+
// restores on resumeAt when spec.rewindFiles is set, for ANY env when this store is wired (gate-split 1.134.0).
|
|
1091
1803
|
fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
|
|
1092
1804
|
executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
|
|
1093
1805
|
lspManager: lspManager ? lspManager : undefined,
|
|
1806
|
+
// design/129-B (core 1.240.0): the PROCESS-LEVEL background-child observer — spawn/tick/terminal
|
|
1807
|
+
// for every bg delegation child, never dying with a leg. Feeds the fleet rows (launch 即有行, session-scoped
|
|
1808
|
+
// children stay visible past the turn — the 缺口① fix) + the bg_notification frame on the
|
|
1809
|
+
// always-open fleet stream (缺口②: idle completion delivery is immediate). Wired once per process.
|
|
1094
1810
|
onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
|
|
1811
|
+
// design/113 C4: inject CLAUDE.md + git narrative so a chat opens project-aware (like CC). The helper reads cwd via
|
|
1812
|
+
// local fs/execFile — only meaningful when cwd is LOCAL (host/file lane); it stats the dir and returns null on a
|
|
1813
|
+
// remote-sandbox cwd, so this is safe to wire generally, but we gate it to single-user (cwdHonored posture — the
|
|
1814
|
+
// same lane where reading the local project for the caller isn't a confused-deputy hole) + the opt-out kill-switch.
|
|
1095
1815
|
loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
|
|
1816
|
+
// core 1.302 instructions-change lane (板 [952]②): re-fingerprint the declared instruction file each turn so a
|
|
1817
|
+
// mid-run edit surfaces as a tail attachment. STRICTLY host lane — on a remote-sandbox lane the instruction file
|
|
1818
|
+
// lives in the container; a local re-read would fingerprint the wrong tree, so the probe is not wired there.
|
|
1096
1819
|
probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && config.projectMemoryEnabled
|
|
1097
1820
|
? makeProbeInstructionSources()
|
|
1098
1821
|
: undefined,
|
|
1099
1822
|
hooks: deploymentHooks,
|
|
1100
|
-
...selfOrchestrationDeps,
|
|
1823
|
+
...selfOrchestrationDeps, // S8 self-orchestration 全家桶(成组条件段;类型标注在上方 const)
|
|
1824
|
+
// F8: surface best-effort compaction failures + the 1.22 prompt-cache low-hit warning (an
|
|
1825
|
+
// unstable/poisoned prefix tanking cost) instead of silently swallowing them.
|
|
1101
1826
|
onError: (err, ctx) => {
|
|
1827
|
+
// "degraded" (1.40) is an operational event, NOT a failure — the task still completes (on a cheaper
|
|
1828
|
+
// model). Log it as a warning so it doesn't pollute error rates/alerts. The degraded_total metric
|
|
1829
|
+
// comes from the tracer's task.degraded event.
|
|
1102
1830
|
if (ctx.phase === "degraded") {
|
|
1103
1831
|
logger.warn("task_degraded", { sessionId: ctx.sessionId, info: String(err) });
|
|
1104
1832
|
return;
|
|
1105
1833
|
}
|
|
1834
|
+
// "prompt-constitution" (core 1.243): a stableSystem provider returned an ALREADY-
|
|
1835
|
+
// assembled prompt (constitution anchor found); core passed it through un-doubled. The task is safe
|
|
1836
|
+
// (guard un-doubles), but the provider is pre-1.243-shaped and should be upgraded to return only the
|
|
1837
|
+
// role base. Warn — paired with the tracer's prompt_constitution_total{mode="provider-assembled"}.
|
|
1106
1838
|
if (ctx.phase === "prompt-constitution") {
|
|
1107
1839
|
logger.warn("prompt_provider_needs_upgrade", { sessionId: ctx.sessionId, info: String(err) });
|
|
1108
1840
|
return;
|
|
1109
1841
|
}
|
|
1842
|
+
// "rewind" (E19 per-turn file snapshot): best-effort — a failed snapshot only makes that one turn
|
|
1843
|
+
// non-rewindable; the task itself is unaffected. `too_large` is the EXPECTED shape on a big working
|
|
1844
|
+
// tree (TOC user opening a 20G folder: enumerate hits the 256MB bound EVERY turn → an error-level
|
|
1845
|
+
// line per turn reads like the run is broken). Downgrade to warn + count by code so a real store
|
|
1846
|
+
// fault (enumerate_failed on a readable tree, blob-write errors) still stands out in the metric.
|
|
1110
1847
|
if (ctx.phase === "rewind") {
|
|
1111
1848
|
const code = /\((\w+)\)/.exec(String(err))?.[1] ?? "unknown";
|
|
1112
1849
|
logger.warn("rewind_snapshot_failed", { sessionId: ctx.sessionId, code, info: String(err) });
|
|
1113
1850
|
metrics.inc("rewind_snapshot_failed_total", { code });
|
|
1114
1851
|
return;
|
|
1115
1852
|
}
|
|
1853
|
+
// "memory" (1.62, design/41): a best-effort post-task consolidation pass failed. The task already
|
|
1854
|
+
// completed and its notes are safely appended (just not reconciled this round) — warn, fail-open,
|
|
1855
|
+
// don't pollute error rates. (Consolidation cost still lands in model_cost_micro_usd via the tracer.)
|
|
1116
1856
|
if (ctx.phase === "memory") {
|
|
1117
1857
|
logger.warn("memory_consolidation_failed", { sessionId: ctx.sessionId, info: String(err) });
|
|
1118
1858
|
return;
|
|
1119
1859
|
}
|
|
1860
|
+
// "mcp" (1.68): a broken MCP server was SKIPPED (fail-open) — the task still ran without
|
|
1861
|
+
// that server's tools. Surface it as a warning + metric so a misconfigured server is observable, not silent.
|
|
1120
1862
|
if (ctx.phase === "mcp") {
|
|
1121
1863
|
logger.warn("mcp_server_unavailable", { sessionId: ctx.sessionId, info: String(err) });
|
|
1122
1864
|
metrics.inc("mcp_server_unavailable_total");
|
|
1123
1865
|
return;
|
|
1124
1866
|
}
|
|
1867
|
+
// "prompt-cache" (1.22 low-hit + design/31 break detector): since core 1.89 the detector passes its
|
|
1868
|
+
// root cause as ctx.classification — "server-or-ttl" is usually benign in agentic tasks (slow tools ⇒
|
|
1869
|
+
// 5min+ request gaps expire provider caches) → warn; the prefix-bug causes
|
|
1870
|
+
// (model-switch/tool-schema/tool-set/system-prefix) stay errors that warrant attention.
|
|
1125
1871
|
if (ctx.phase === "prompt-cache") {
|
|
1126
1872
|
metrics.inc("prompt_cache_low_hit_total");
|
|
1127
1873
|
const fields = { sessionId: ctx.sessionId, ...(ctx.classification ? { classification: ctx.classification } : {}), err: String(err) };
|
|
@@ -1131,6 +1877,9 @@ async function main() {
|
|
|
1131
1877
|
logger.error("prompt_cache_break", fields);
|
|
1132
1878
|
return;
|
|
1133
1879
|
}
|
|
1880
|
+
// "config" (core 1.300 [931]②): assembly-time advisories — e.g. toolPolicy 名单池审计
|
|
1881
|
+
// (config.toolpolicy.unmatched_names:名单里写了实挂宇宙不存在的工具名=typo 探测,enforcement
|
|
1882
|
+
// 不变)。观测性提示,warn+metric,不进 error 告警面。
|
|
1134
1883
|
if (ctx.phase === "config") {
|
|
1135
1884
|
logger.warn("runner_config_advisory", { sessionId: ctx.sessionId, info: String(err) });
|
|
1136
1885
|
metrics.inc("runner_config_advisory_total");
|
|
@@ -1140,8 +1889,28 @@ async function main() {
|
|
|
1140
1889
|
},
|
|
1141
1890
|
};
|
|
1142
1891
|
const runner = new Runner(runnerDeps);
|
|
1892
|
+
// codex R10: TRUE ⇒ the Runner just froze a PRIVATE tier-expanded catalog copy (core runtask.js constructor,
|
|
1893
|
+
// dist-read) — in-place model-plane mutation no longer reaches it, so refresh-time plane changes must be
|
|
1894
|
+
// DEFERRED to restart (see appliedPlaneEff / applyEffective deferModelPlane). Captured HERE, at the same
|
|
1895
|
+
// config generation the constructor read.
|
|
1143
1896
|
const runnerTierFrozen = Object.keys(config.tiers).length > 0;
|
|
1897
|
+
// hooks 阶段三b(design/HOOKS-PHASE3B-PROMPT-AGENT.md):prompt/agent 条目的模型调用载体。
|
|
1898
|
+
// - hookLlm(prompt 条目)= 直连网关一次非流式 completion——一次判定调用不起 core task(重炮打蚊子
|
|
1899
|
+
// 且引入可重入面);无系统注入(hook 作者的 prompt 即全部)。默认模型=summarize 角色(cheap tier,
|
|
1900
|
+
// council/压缩同款;缺角色→主模型);entry.model 覆盖须命中部署 catalog(单用户=自家目录全开,与
|
|
1901
|
+
// workflow 模型 allowlist 同纪律),未命中=该条目 fail(非阻断记账)。费用不折 task budget(hook=
|
|
1902
|
+
// 部署策略面开销;hook_llm_calls_total/hook_llm_cost 由 metrics 记)。
|
|
1903
|
+
// - hookAgent(agent 条目)= runner.runTask 读-only 子代理:handsReadOnly + enableFork:false +
|
|
1904
|
+
// 不装 hooks(🔴 可重入禁——结构性:这个 spec 永不携带 TaskSpec.hooks)+ maxTurns 10 + maxCostUsd
|
|
1905
|
+
// 0.05 独立小预算,影响范围受限。final result 文本即输出(与 prompt 同一 SyncHookOutput 决策面)。
|
|
1906
|
+
// hookModelFor/hookLlm 抽到 src/hooks/hook-llm.ts(可注入 fetch/keyResolver ⇒ 可单测):api 形分派
|
|
1907
|
+
// (openai /chat/completions 与 anthropic /v1/messages 双腿)、缺省 baseUrl 按 api 分家、per-model key
|
|
1908
|
+
// 恒胜 + foreign-no-key 连 prompt 都不发的 fail-closed 纪律,全在该模块内(注释含 audit 原文)。
|
|
1909
|
+
// getKeyResolver 取函数:keyResolver 是下方 let,registry 热应用会整个换引用,这里必须每次现取。
|
|
1144
1910
|
const { hookModelFor, hookLlm } = createHookLlm({ config, getKeyResolver: () => keyResolver, metrics });
|
|
1911
|
+
// hook agent 的 session 绝不进 durable store——每次 runTask 会写 session_meta 且无人
|
|
1912
|
+
// 清理(reaper 只扫 staging orphan),cap=4/事件 × 长会话=无主行堆积。专用 Runner 覆盖 sessionStore 为
|
|
1913
|
+
// 进程内 TTL 店(1h 短 TTL,hook 子代理无 resume 语义,行随进程/TTL 消失);其余 deps 原样共享。
|
|
1145
1914
|
const hookAgentRunner = new Runner({ ...runnerDeps, sessionStore: new TtlSessionStore({ defaultTtlDays: 1 / 24 }) });
|
|
1146
1915
|
const hookAgent = async ({ prompt, model, timeoutMs }) => {
|
|
1147
1916
|
const pick = hookModelFor(model);
|
|
@@ -1156,7 +1925,11 @@ async function main() {
|
|
|
1156
1925
|
enableFork: false,
|
|
1157
1926
|
maxCostUsd: 0.05,
|
|
1158
1927
|
limits: { maxTurns: 10, timeoutSec: Math.max(30, Math.ceil(timeoutMs / 1000)) },
|
|
1928
|
+
// spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
|
|
1929
|
+
// error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
|
|
1930
|
+
// 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
|
|
1159
1931
|
toolPolicy: createAllowDenyPolicy({}),
|
|
1932
|
+
// 🔴 可重入禁:此 spec 不带 hooks 字段(deps.hooks 是部署观测 tracer,void 观察者,无决策面)。
|
|
1160
1933
|
});
|
|
1161
1934
|
metrics.inc("hook_llm_calls_total", { type: "agent" });
|
|
1162
1935
|
if (result.status !== "completed")
|
|
@@ -1168,12 +1941,15 @@ async function main() {
|
|
|
1168
1941
|
}
|
|
1169
1942
|
};
|
|
1170
1943
|
const clipHookErr = (s) => (s ?? "").slice(0, 300);
|
|
1944
|
+
// Auto session titles — fire-and-forget at first
|
|
1945
|
+
// submit, one cheap hookLlm call (summarize role → MODEL_CHEAP_ID when configured), write-once via the session
|
|
1946
|
+
// store's setTitleIfNull (SQL twins + local sidecar carry it; the memory dev store doesn't → titler dark, honest).
|
|
1171
1947
|
const titleCapable = sessionStore;
|
|
1172
1948
|
const sessionTitler = config.sessionAutoTitle && titleCapable.setTitleIfNull && titleCapable.probeTitle
|
|
1173
1949
|
? createSessionTitler({
|
|
1174
1950
|
setTitleIfNull: titleCapable.setTitleIfNull.bind(sessionStore),
|
|
1175
1951
|
probeTitle: titleCapable.probeTitle.bind(sessionStore),
|
|
1176
|
-
redact: redactSecrets,
|
|
1952
|
+
redact: redactSecrets, // audit 2026-07-13: scrub a model-echoed secret before persist/list/log
|
|
1177
1953
|
llm: (o) => hookLlm({ prompt: o.prompt, timeoutMs: o.timeoutMs }),
|
|
1178
1954
|
logger,
|
|
1179
1955
|
metrics,
|
|
@@ -1181,8 +1957,54 @@ async function main() {
|
|
|
1181
1957
|
: undefined;
|
|
1182
1958
|
if (sessionTitler)
|
|
1183
1959
|
logger.info("session_auto_title_enabled", {});
|
|
1960
|
+
// ① workflowsCapability SINGLE-SOURCE (core 1.155 `workflowsCapability(deps)`; ruling = SPLIT two caps):
|
|
1961
|
+
// `capabilities.workflows` = core's own truth (`workflowScriptRunner.safeForUntrustedScripts` ∧ governance), boot-
|
|
1962
|
+
// computed from the SAME RunnerDeps the engine uses = the ENGINE-CAN axis ("can the engine self-orchestrate /
|
|
1963
|
+
// ultracode"), ORTHOGONAL to the durable-LIST axis (`Boolean(workflowRunStore)`, surfaced as `workflowsList`).
|
|
1964
|
+
// ⚠️ Today these COINCIDE — `workflowRunStore` is built iff self-orch (InMemory/File, ~L527), the same gate that
|
|
1965
|
+
// wires the safe runner — so this is core's FORWARD-LOOKING contract, not a live-divergence fix (the earlier
|
|
1966
|
+
// "memory backend diverges" premise was wrong; double-review caught it — see the cap site in http/server.ts).
|
|
1967
|
+
// Static per deployment (deps are boot-fixed).
|
|
1184
1968
|
const workflowsCapable = workflowsCapability(runnerDeps);
|
|
1969
|
+
// Sub-task runner (code-review council lenses/arbiter, subagent/team children). [1364]①/[1367]①(core
|
|
1970
|
+
// 1.350 fork 终修的部署要求):fork 子任务带 `requireExistingSession:true` 在这里执行而源会话在 HOST 店
|
|
1971
|
+
// ——拆店=响亮 `resume.session_not_found`(cli fork 案的 server 半场)。但整店共享被 codex 判两 high
|
|
1972
|
+
// (普通子任务铸 committed durable 会话:无 reaper 覆盖=无界增长+污染 /v1/sessions 枚举),故走
|
|
1973
|
+
// ForkRoutingSessionStore:fork/resume 形(requireExisting)→ host 店优先,普通子任务 → 私有 TTL 店
|
|
1974
|
+
// (throwaway 姿势保留)。hookAgentRunner 仍保独立 TtlSessionStore(1h)——纯瞬态 hook 车道,与 fork 无关。
|
|
1975
|
+
// CC-parity (CRITICAL — 子代理 hands, root cause is the service spawn path): the subagent delegation
|
|
1976
|
+
// tool runs child tasks on THIS subRunner. Without executionEnvFactory the child's `handsEnabled` (= ownedEnv ||
|
|
1977
|
+
// deps.executionEnv, prepare-task) is FALSE → the child gets NO hands (Bash/Read/Edit) and can't run `pwd` — the
|
|
1978
|
+
// "subagent couldn't run pwd" defect hit in real TUI use. core's createSubagentTool gives hands when the env is
|
|
1979
|
+
// wired (gate3: opts.tools:[] still yields Bash), so the fix is to give the subRunner the SAME executionEnvFactory +
|
|
1980
|
+
// lspManager as the main runner. Deliberately NOT inherited: selfOrchestration (no recursive workflow spawn),
|
|
1981
|
+
// scheduler, memory, fileSnapshot, runtimeCaps — a delegated child is a short focused task, not a full session.
|
|
1982
|
+
// codex-2 F1:promptSource 必须随子代——core 1.341 把父的 centerArtifactDigest 传给委派子任务并在子
|
|
1983
|
+
// runner 上按 digest 解析;缺席=每个 catalog 任务的 Agent 子代 fail-loud prompt_snapshot_unavailable。
|
|
1984
|
+
// backgroundAgentStore(1.248 复审 F2):core prepare-task 给每个 task 的 TaskOutput/TaskStop/
|
|
1985
|
+
// AgentTranscript 装 durable 臂用的是所在 runner 的 deps——子代任务跑在 subRunner 上,缺席=子代读
|
|
1986
|
+
// 工具无 durable 回落(第三消费点,与 RunnerDeps/scenarioDeps 同一实例)。
|
|
1987
|
+
// [1543]§三族A(core 穿透审查,历史欠账键批——主 runner 挂了、subRunner 漏挂 = 子代 lane 静默降级;
|
|
1988
|
+
// 全部直引 runnerDeps 同一实例,双点永不漂移):
|
|
1989
|
+
// - tiers(H):模型目录双 runner 分叉——tier 词 AgentDefinition 在子代 lane 抛 unknown_model、Agent
|
|
1990
|
+
// model 参数("opus")静默 modelNote 降级、S3c revive 臂 row.model 记 tier 词时复活换模型。
|
|
1991
|
+
// - rosterStore(M):具名 spawn 不落 durable 名册 → tier-3 按名复活从子代不可达。
|
|
1992
|
+
// - onBackgroundChildEvent(M):子代触发的 revive/孙代对 fleet 流全盲(同一 publisher 实例=单管道)。
|
|
1993
|
+
// - loadProjectMemory/probeInstructionSources(M):委派子代 project-blind(core types 已为子代 ctx
|
|
1994
|
+
// 设计 isSubagent/agentName;门禁条件随 runnerDeps 条件键自动一致)。
|
|
1995
|
+
// - hooks/onError/toolResultStore/sessionPolicyStore(L):子代进 tracer/计量;best-effort 故障不静默;
|
|
1996
|
+
// fork 子代跨 wake 保 offload 引用;fork 形(有 sessionId)会话规则可达。
|
|
1997
|
+
// 1877 注释的「Deliberately NOT inherited」清单(selfOrchestration/scheduler/memory/fileSnapshot/
|
|
1998
|
+
// runtimeCaps)不变——本批只补漏配,不动裁定。
|
|
1999
|
+
// checkpointStore 构造(从 runStore 区上移,只依赖 backend/config——顺序安全;DURABLE_APPROVAL 仍是
|
|
2000
|
+
// 唯一门,ALL three backends carry checkpoint(),local = core FileCheckpointStore + service half)。
|
|
2001
|
+
// 三个消费点同一实例:主 runner 的 per-task spec 面(resolveSpec durableEnabled 分支)、scenarioDeps
|
|
2002
|
+
// (subagent spawn 面 forwardDurableApproval)、subRunner deps(子代执行面 suspendAsk——[1584] 接线,
|
|
2003
|
+
// 裁定见下方 subRunner 构造内注)。
|
|
1185
2004
|
const checkpointStore = backend?.checkpoint && config.durableApproval ? backend.checkpoint(logger) : undefined;
|
|
2005
|
+
// [1593] 提名成变量:parked 子代的 ensureChildSessionDurable 晋升半场要拿它做 promoteToHost
|
|
2006
|
+
// (子会话住私有 TTL 店,park 前整树拷进 host durable 店——只查不迁的 1.263 形在拆店生产形下恒
|
|
2007
|
+
// 否决 park,cli 真机 14ms~2s expired 即此)。
|
|
1186
2008
|
const subRunnerSessions = new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 }));
|
|
1187
2009
|
const subRunner = new Runner({
|
|
1188
2010
|
brain,
|
|
@@ -1193,6 +2015,9 @@ async function main() {
|
|
|
1193
2015
|
tracer,
|
|
1194
2016
|
promptSource,
|
|
1195
2017
|
sessionStore: subRunnerSessions,
|
|
2018
|
+
// A7/S4 余量(design/158):core RunnerDeps 消费端对以下键均无在场性判定(无 "k" in deps /
|
|
2019
|
+
// Object.keys(deps) 分支——S4 已逐键核过全量 RunnerDeps 消费面),故显式可选键(undefined 与
|
|
2020
|
+
// 缺席同义)替代条件 spread,行为等价、可读性与主 runnerDeps 装配点(1687 行)同款。
|
|
1196
2021
|
executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
|
|
1197
2022
|
lspManager: lspManager ? lspManager : undefined,
|
|
1198
2023
|
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
@@ -1204,15 +2029,42 @@ async function main() {
|
|
|
1204
2029
|
hooks: deploymentHooks,
|
|
1205
2030
|
onError: runnerDeps.onError ? runnerDeps.onError : undefined,
|
|
1206
2031
|
toolResultStore,
|
|
2032
|
+
// sessionPolicyStore:挂载无害(tighten-only 空查);如实注(复审 F5)——子代 sessionId 是新铸
|
|
2033
|
+
// uuid,E6 规则按宿主 id 键存 → 子代查自己恒空;规则真到子代靠 inheritedGate.ancestorRules(本就
|
|
2034
|
+
// 通)。真实受益面 = retained child 自己 session 被显式 PUT 过规则的边角。
|
|
1207
2035
|
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
|
|
2036
|
+
// checkpointStore = 子代**执行面**的 park 设施([1582] cli TiDB 真验撞获缺口,[1584] core 定谳
|
|
2037
|
+
// 接线步):core prepare-task `spec.checkpointStore ?? deps.checkpointStore` 里 spec 侧永不被
|
|
2038
|
+
// subagent 设置,子代 suspendAsk 全靠这里——缺席 = 具名 bg 子代的 gated ask 只能 deny+continue,
|
|
2039
|
+
// checkpoint 表零行(cli T2 复现原话「denied by an approval constraint」)。必须与 scenarioDeps
|
|
2040
|
+
// (spawn 面 forwardDurableApproval 判定)**同一实例**:claim/expire/consumeParkedFlip 都作用于
|
|
2041
|
+
// 同一行。旧 [1546] HIGH-2 撤载(「core 不转发 durableApproval + SAFETY ask 会 park 出搁浅
|
|
2042
|
+
// checkpoint」)的前提被 core 1.388 件4(转发臂)消除,搁浅顾虑由件1-4+1.389 逐项闭合:watcher
|
|
2043
|
+
// park 翻行 + reap 豁免 + reconciliation 兜孤儿;unattended SAFETY park 恒带缺省 deadline;
|
|
2044
|
+
// TaskStop on parked 走 expire CAS 仲裁(1.389 F-1);/decide 可赎回。行为钉 =
|
|
2045
|
+
// test/subagent-park-wiring.test.ts(SOURCE PIN + park 全链 + 旧形反向控制)。
|
|
1208
2046
|
checkpointStore: checkpointStore ? checkpointStore : undefined,
|
|
1209
2047
|
});
|
|
2048
|
+
// S6: resolve identity + owned session from the authenticated channel (never the body).
|
|
1210
2049
|
const authorize = createAuthorizer(config, sessionStore);
|
|
2050
|
+
// S1/S2: durable async run registry + replayable event log (TiDB-backed).
|
|
1211
2051
|
const runStore = backend ? backend.run() : undefined;
|
|
2052
|
+
// E18 resume-at: the eventId→entryId anchor map. REQUIRED on every backend (works local — needs only the session
|
|
2053
|
+
// tree + this map, no cloud-only checkpoint), so the only undefined case is the env-only/no-backend deploy (which
|
|
2054
|
+
// also has no async runs — consistent). The shell sends body.resumeAt=eventId; resolveSpec resolves it here.
|
|
1212
2055
|
const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
|
|
2056
|
+
// Per-session approval exemption ("本会话不再询问") — REQUIRED on every backend like the anchor
|
|
2057
|
+
// map; undefined only on the env-only/no-backend deploy (which has no durable approvals either — consistent).
|
|
1213
2058
|
const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
|
|
1214
2059
|
const instanceId = uuidv7();
|
|
2060
|
+
// Per-task wall-clock override (seconds). 0/absent = keep the built-in 600 (1500 council/debate/team) —
|
|
2061
|
+
// the override only ever RAISES (Math.max at the use site), so a misconfigured low value cannot shrink
|
|
2062
|
+
// the council budget.
|
|
1215
2063
|
const taskTimeoutSec = Math.max(0, Math.floor(Number(process.env.TASK_TIMEOUT_SEC ?? "0")) || 0);
|
|
2064
|
+
// [854]④ per-request 配速的运营方上限旋钮(可选;缺省不设=不封顶):body.limits 每键各自被对应旋钮
|
|
2065
|
+
// Math.min 封顶(normalizeLimits)。多租部署想约束 caller 自报配速时才设;单用户 turnkey 通常留空。
|
|
2066
|
+
// ⚠️ 语义=只钳「显式请求值」,不是全队默认限额:body 缺席(或重放体里的非法值被 defensive DROP)⇒ 该键
|
|
2067
|
+
// 不设限,与升级前行为同形——要给所有任务强加限额是另一个旋钮(未建),别指望这三个(交叉评审 F1 判读)。
|
|
1216
2068
|
const capEnv = (name) => {
|
|
1217
2069
|
const n = Math.floor(Number(process.env[name] ?? "0"));
|
|
1218
2070
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
@@ -1222,12 +2074,32 @@ async function main() {
|
|
|
1222
2074
|
maxOutputTokens: capEnv("TASK_MAX_OUTPUT_TOKENS_MAX"),
|
|
1223
2075
|
maxTurns: capEnv("TASK_MAX_TURNS_MAX"),
|
|
1224
2076
|
};
|
|
1225
|
-
|
|
2077
|
+
// F4: human-in-the-loop approval gate (pending state durable so any instance can decide).
|
|
2078
|
+
const approvalStore = backend?.approval ? backend.approval() : undefined; // local omits approval() → legacy poll-gate off (durable HITL needs checkpoint anyway)
|
|
1226
2079
|
const approvalEnabled = approvalStore && config.approvalRequire.length > 0;
|
|
2080
|
+
// Finding #2 (edge case): the single-user auto-accept tool-policy BASELINE applies ONLY when the operator
|
|
2081
|
+
// expressed NO gate intent. If they SET approval flags (APPROVAL_REQUIRE/APPROVAL_DENY/DURABLE_APPROVAL) but no
|
|
2082
|
+
// backing store is wired (e.g. DB_BACKEND=local → no approvalStore/checkpointStore ⇒ approvalEnabled/durableEnabled
|
|
2083
|
+
// false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined so core's UNGATED warning
|
|
2084
|
+
// still surfaces it. So the baseline needs single-user turnkey ∧ zero expressed gate intent.
|
|
1227
2085
|
const operatorGateIntent = config.approvalRequire.length > 0 || config.approvalDeny.length > 0 || config.durableApproval;
|
|
1228
2086
|
const singleUserAutoAcceptBaseline = config.requirePrincipal !== true && !operatorGateIntent;
|
|
2087
|
+
// design/45 durable-checkpoint (core 1.67): opt-in (DURABLE_APPROVAL + a pool). When on, F4 uses durable
|
|
2088
|
+
// suspend/resume instead of the poll gate — a gated `ask` suspends the task (status:"suspended" + checkpoint)
|
|
2089
|
+
// and an operator resumes it on any replica. Default off → unchanged poll-policy.
|
|
2090
|
+
// checkpointStore 构造已上移到 subRunner 之前([1535] server 半场:子代 park 设施);本行原位注释保留。
|
|
2091
|
+
// TOC integration ask③ (INTEGRATION-DESIGN §11): reclaim runs orphaned by a previous host-lane engine
|
|
2092
|
+
// process (orphaned `running` → failed + release its EEXIST-blocking claim; exclude resume-in-progress rows). The
|
|
2093
|
+
// logic + its null-safety (runStore undefined on the env-only/no-backend lane) live in the unit-tested
|
|
2094
|
+
// `reclaimOrphansAtBoot` helper — extracted after a workflow re-review caught a boot crash there (HIGH, 1.49.0).
|
|
1229
2095
|
await reclaimOrphansAtBoot(runStore, checkpointStore, logger);
|
|
1230
|
-
|
|
2096
|
+
// Sandbox-image-pool index (IMAGE-API-DESIGN.md §2): enables /v1/images* query + select. TiDB-backed projection
|
|
2097
|
+
// of the bake manifests + publish lifecycle. Present whenever a pool is — read-only catalog until P2/P3 bakes.
|
|
2098
|
+
const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined; // local omits imageIndex() → sandbox-image-pool is cloud/fleet-only
|
|
2099
|
+
// RFC A2 (A 切片先行): the model-facing SelectEnvironment tool — mounted via spec.tools
|
|
2100
|
+
// (RFC 判定②(a)) whenever the per-task image chain is actually usable (k8s backend + image catalog), opt-out
|
|
2101
|
+
// via SELECT_ENVIRONMENT_TOOL=false. Viewer scope is rebuilt per-execute from the Runner-threaded read-only
|
|
2102
|
+
// ctx.principal (auth-channel-derived — a tool argument can never widen visibility).
|
|
1231
2103
|
const selectEnvTool = config.selectEnvironmentTool && config.remoteExec?.provider === "k8s" && imageIndex
|
|
1232
2104
|
? selectEnvironmentTool({
|
|
1233
2105
|
catalog: imageIndex,
|
|
@@ -1235,42 +2107,91 @@ async function main() {
|
|
|
1235
2107
|
viewerFor: (principal) => ({ operator: explicitOperator(principal, config.operatorPrincipals), tenantId: principal ?? null }),
|
|
1236
2108
|
})
|
|
1237
2109
|
: undefined;
|
|
1238
|
-
|
|
2110
|
+
// Sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2): enables /v1/images/bakes* when a pool exists
|
|
2111
|
+
// AND IMAGE_BAKES_ENABLED. The config boot-invariant already fail-fasts an empty OPERATOR_PRINCIPALS (a
|
|
2112
|
+
// world-writable build-host-RCE door). image-api stays the UNPRIVILEGED controller; the privileged build runs
|
|
2113
|
+
// on the sema-registry-managed bake-runner worker on build-host (it claims + ingests over the internal routes).
|
|
2114
|
+
const imageBakes = backend?.imageBake && config.imageBakes.enabled ? backend.imageBake() : undefined; // local omits imageBake() → cloud/fleet-only
|
|
1239
2115
|
if (imageBakes)
|
|
1240
2116
|
logger.info("image_bakes_enabled", { operators: config.operatorPrincipals.length, runner: config.imageBakes.runnerPrincipal });
|
|
2117
|
+
// Durable HITL surface = checkpointStore ALONE (DURABLE_APPROVAL=true). It mounts AskUserQuestion-suspend (the
|
|
2118
|
+
// agent asks the human + the portal approval center) AND the resumable checkpoint. The F4 TOOL-APPROVAL
|
|
2119
|
+
// gate is an ADDITIVE layer driven by APPROVAL_REQUIRE — empty = no tool is gated, but AskUserQuestion HITL
|
|
2120
|
+
// still works. (Decoupled 2026-06-13: previously these were coupled, forcing a placeholder APPROVAL_REQUIRE
|
|
2121
|
+
// to get HITL — clay: OA writes don't gate (OA-backend RBAC is the authority), but portal HITL must work.)
|
|
2122
|
+
// durableEnabled 已在 runnerDeps 前按同一条件求值([1.294 G1] 前置声明处)——checkpointStore 构造条件与其
|
|
2123
|
+
// 同源,恒有 durableEnabled === !!checkpointStore。
|
|
1241
2124
|
if (durableEnabled)
|
|
1242
2125
|
logger.info("durable_approval_enabled", { toolApprovalGate: config.approvalRequire.length });
|
|
2126
|
+
// 修4(三路复审 W3):the fs-write ask gate (resolveSpec, applyTaskSettings wiring below) mounts purely off the
|
|
2127
|
+
// host-semantics lane — DELIBERATELY decoupled from any approval face, so no assembly path is ever gateless:
|
|
2128
|
+
// with no live coordinator and no durable park, a permissionMode default/auto Write/Edit ask fail-closed
|
|
2129
|
+
// auto-denies via core resolveAsk (onAsk absent ⇒ headless auto-deny, loud in-transcript). What that posture
|
|
2130
|
+
// must NOT be is silent at boot: an operator who explicitly set TOOL_APPROVAL_ENABLED=false (or a multi-tenant
|
|
2131
|
+
// default) would otherwise read the resulting hand-tool denials as an outage. Warn once, at boot.
|
|
1243
2132
|
if ((config.remoteExec === undefined || config.remoteExec.provider === "host") && !toolApproval && !durableEnabled) {
|
|
1244
2133
|
logger.warn("fs_write_gate_no_responder", {
|
|
1245
2134
|
hint: "host-semantics lane with no approval face: permissionMode default/auto Write/Edit asks will auto-deny (fail-closed); wire TOOL_APPROVAL_ENABLED=true or DURABLE_APPROVAL=true to answer them",
|
|
1246
2135
|
});
|
|
1247
2136
|
}
|
|
2137
|
+
// [1245]/[1248]② the workflow ask leg attaches on EVERY lane (not fs-dependent — see createWorkflowAskPolicy),
|
|
2138
|
+
// so a sandbox-lane deployment running self-orchestration with no approval face hits the same fail-closed
|
|
2139
|
+
// auto-deny class. Same loud-at-boot posture; host lanes are already covered by the broader warn above.
|
|
1248
2140
|
if (config.selfOrchestrationEnabled && config.remoteExec !== undefined && config.remoteExec.provider !== "host" && !toolApproval && !durableEnabled) {
|
|
1249
2141
|
logger.warn("workflow_gate_no_responder", {
|
|
1250
2142
|
hint: "self-orchestration with no approval face: permissionMode default/auto/acceptEdits run_workflow asks will auto-deny (fail-closed); wire TOOL_APPROVAL_ENABLED=true or DURABLE_APPROVAL=true to answer them",
|
|
1251
2143
|
});
|
|
1252
2144
|
}
|
|
2145
|
+
// Finding #2: a single-user turnkey worker with no explicit approval/durable/commandPolicy gets an
|
|
2146
|
+
// ADJUDICATED auto-accept tool-policy BASELINE (createAllowDenyPolicy — a PRESENT effect-aware policy, so core's
|
|
2147
|
+
// write-capable hand tools are never UNGATED / unadjudicated). Log the posture so the auto-accept is OBSERVABLE,
|
|
2148
|
+
// not silent; tighten irreversible ops via APPROVAL_REQUIRE / AUTONOMY / commandPolicy (layered tighten-only).
|
|
1253
2149
|
if (singleUserAutoAcceptBaseline) {
|
|
2150
|
+
// Blanket auto-accept over ALL write/hand/MCP tool surfaces (core's per-surface UNGATED enumeration — hand band /
|
|
2151
|
+
// destructive-MCP / first-party-write — is suppressed for single-user turnkey by design). Log it so the posture is
|
|
2152
|
+
// observable; tighten (which also re-surfaces per-surface gating) via APPROVAL_REQUIRE / AUTONOMY / commandPolicy.
|
|
1254
2153
|
logger.info("tool_policy_single_user_default", {
|
|
1255
2154
|
posture: "adjudicated auto-accept — ALL write/hand/MCP tool surfaces auto-allowed (core per-surface UNGATED enumeration suppressed for single-user turnkey)",
|
|
1256
2155
|
tighten: "APPROVAL_REQUIRE / AUTONOMY / commandPolicy (re-surfaces + gates specific surfaces)",
|
|
1257
2156
|
});
|
|
1258
2157
|
}
|
|
2158
|
+
// D-G: a boot-time signal that this worker is in crypto-bound direct-door mode (trusted-header path disabled).
|
|
2159
|
+
// Pairs with the /health directDoorActive field for the DG-M2 go-live runbook (a positive go signal, not just
|
|
2160
|
+
// a behavioral 401). The boot invariant in config.ts already fail-fasts a half-configured direct door.
|
|
1259
2161
|
if (config.directDoorActive)
|
|
1260
2162
|
logger.info("direct_door_active", { iss: config.principalJwtIss, aud: config.principalJwtAud, operators: config.operatorPrincipals.length });
|
|
2163
|
+
// CC-parity (adversarial-review finding): a MULTI-TENANT deployment running self-orchestration WITHOUT worker
|
|
2164
|
+
// isolation is an availability risk — the in-process hardened VM is ESCAPE-isolated but NOT RESOURCE-isolated, so an
|
|
2165
|
+
// entitled tenant's workflow script can starve the host event loop (microtask loop / OOM), affecting every tenant
|
|
2166
|
+
// (src/orchestration/hardened-vm-runner.ts documents this). Loud boot warning so the operator sets
|
|
2167
|
+
// SELF_ORCHESTRATION_WORKER_ISOLATION=true. (Single-user is unaffected — the one principal owns its own worker.)
|
|
1261
2168
|
if (config.requirePrincipal && config.selfOrchestrationEnabled && !config.selfOrchestrationWorkerIsolation)
|
|
1262
2169
|
logger.warn("self_orchestration_no_worker_isolation", { detail: "multi-tenant self-orchestration without SELF_ORCHESTRATION_WORKER_ISOLATION=true — a tenant workflow can starve the host event loop (availability risk). Set it to enable worker_thread resource isolation." });
|
|
2170
|
+
// 🔒 (systematic-audit HIGH fix, defense-in-depth): a multi-tenant deployment (requirePrincipal) with NO service/trace
|
|
2171
|
+
// token now correctly fails CLOSED — fleet/trace/session-list are owner-gated, never fleet-wide (isFleetWide, server.ts).
|
|
2172
|
+
// Warn the operator: without SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN those endpoints only ever return the caller's own
|
|
2173
|
+
// rows (safe, but may surprise an operator expecting cross-tenant ops visibility — set a token for ops-relay).
|
|
1263
2174
|
if (config.requirePrincipal && !config.authToken && Object.keys(config.authTokens ?? {}).length === 0 && !config.traceToken)
|
|
1264
2175
|
logger.warn("multi_tenant_no_service_token", { detail: "REQUIRE_PRINCIPAL is on but no SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN — fleet/trace/session-list are owner-gated only (never fleet-wide, tenant-isolation safe). Set a token for cross-tenant ops visibility." });
|
|
2176
|
+
// Rate limiter: TiDB-backed (cross-replica SOFT limit, same write-behind counter as the cost quota)
|
|
2177
|
+
// when a pool exists, else in-memory per-replica. Both satisfy RateGate. (design/30 P0-2)
|
|
1265
2178
|
const rateLimiter = config.rateLimitPerMin > 0
|
|
1266
|
-
? backend?.rateLimiter
|
|
2179
|
+
? backend?.rateLimiter // local omits rateLimiter() → in-memory per-process RateLimiter (single-replica)
|
|
1267
2180
|
? backend.rateLimiter(config.rateLimitPerMin, counterDegradeHook).startRefresh()
|
|
1268
2181
|
: new RateLimiter(config.rateLimitPerMin)
|
|
1269
2182
|
: undefined;
|
|
2183
|
+
// Capability layer: scenario → {tools (+ subagent roster), prompt, skills}. Heavy bits (skills,
|
|
2184
|
+
// git client) are built ONCE here; per request we only select a scenario and bind coordinates.
|
|
1270
2185
|
let skills = loadSkills(config.skillsDir);
|
|
2186
|
+
// Sema-registry B1 (restart-to-apply, baked into scenarios below): overlay center skills (lazy-fetched by
|
|
2187
|
+
// hash + sha256-verified) over the image baseline, center-wins-by-name; resolve center MCP servers' env-NAME
|
|
2188
|
+
// refs to real values from this service's env. Both empty/unset → unchanged (image SKILLS_DIR, no MCP).
|
|
1271
2189
|
if (effective?.skills && config.configCenter) {
|
|
1272
2190
|
skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
|
|
1273
2191
|
}
|
|
2192
|
+
// plugins 域消费半场(契约 [1361]②/[1362]/[1366]①;2026-07-28 开工车):把 center 声明的插件引用
|
|
2193
|
+
// 物化(https-only+ALLOW_HOSTS 纵深复验/sha 钉扎/隔离 clone/LKG)并装载其 skills —— **在 center 直发
|
|
2194
|
+
// skills 之后**:撞名让位判据(收紧④,plugin 让位)以此刻的 skills 集为准。失败单条不连坐,永不拒启。
|
|
1274
2195
|
if (effective?.plugins && config.configCenter) {
|
|
1275
2196
|
const pluginOut = await applyCenterPlugins(skills, effective, {
|
|
1276
2197
|
cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
|
|
@@ -1287,21 +2208,44 @@ async function main() {
|
|
|
1287
2208
|
}
|
|
1288
2209
|
}
|
|
1289
2210
|
}
|
|
2211
|
+
// F7:boot 的 LKG 落盘点。codex R26:走同一只 persistLkgDurable(完整性门在前)——此前裸 saveLkg 会把
|
|
2212
|
+
// applyCenterSkills 静默吞掉失败的「缺正文 manifest」发布成 boot 可见 LKG(离线重启=静默缺 skill 且不再
|
|
2213
|
+
// 有信号收敛)。校验失败=不落盘(warn 已在函数内),下一个干净候选/refresh 重试重写。
|
|
1290
2214
|
if (bootLkgCandidate && lkgEnabled) {
|
|
1291
2215
|
const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
|
|
1292
2216
|
logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
|
|
1293
2217
|
return false;
|
|
1294
2218
|
});
|
|
2219
|
+
// codex R30: a failed/incomplete boot publication must enter the SAME retained-candidate retry lane as the
|
|
2220
|
+
// refresh path — ccEtag has already advanced, so the next ticks 304 and nothing else would ever retry; a
|
|
2221
|
+
// restart during a center outage would then boot stale/env config instead of the generation this process
|
|
2222
|
+
// already applied. blocked=[] — everything WAS applied at boot, this is a publication-only retry (promotion
|
|
2223
|
+
// with no withheld reasons publishes NO restart signal, it just completes the LKG and clears the state).
|
|
1295
2224
|
{
|
|
2225
|
+
// codex R32/R34: verify the boot's LIVE application UNCONDITIONALLY — applyCenterSkills skips an
|
|
2226
|
+
// unfetchable body (this process may run skill-stale / on an older same-name built-in), and the LKG
|
|
2227
|
+
// prewarm inside persistLkgDurable can succeed where the live fetch failed (bootPublished true, LKG
|
|
2228
|
+
// complete, process still stale). Any live miss ⇒ a skills restart: published immediately when the
|
|
2229
|
+
// handoff already stands, or carried in blocked for the 304-gated promotion when it doesn't.
|
|
1296
2230
|
const enabledManifest = (bootLkgCandidate.effective.skills?.skills ?? []).filter((m) => m.enabled !== false);
|
|
2231
|
+
// codex R33: the FULL runtime projection must match, not just name+body — a same-body built-in retained
|
|
2232
|
+
// after a failed center fetch can carry different scenarios/description (e.g. global exposure where the
|
|
2233
|
+
// manifest restricts to one scenario), and the scenario bundles baked right after are then stale.
|
|
1297
2234
|
const canonScenarios = (a) => [...(a ?? [])].sort().join("\u0000");
|
|
1298
2235
|
const liveSkillMiss = enabledManifest.some((m) => !skills.some((sk) => sk.spec.name === m.name &&
|
|
1299
2236
|
skillContentHash(sk.spec.content) === m.contentHash &&
|
|
1300
2237
|
sk.spec.description === m.description &&
|
|
1301
2238
|
canonScenarios(sk.scenarios) === canonScenarios(m.scenarios)));
|
|
1302
2239
|
if (liveSkillMiss)
|
|
1303
|
-
bootSkillStale = true;
|
|
2240
|
+
bootSkillStale = true; // codex R35: sticky — survives same-generation 200 recomputes
|
|
1304
2241
|
if (liveSkillMiss && (bootPublished || (!lkgSurvivesRestart && lastPrewarmBodiesVerified))) {
|
|
2242
|
+
// Signal NOW only on the arms where a restart PROVABLY repairs (codex R34/R36/R37):
|
|
2243
|
+
// · durable + published — the complete LKG (manifest + hash-verified bodies) is the handoff;
|
|
2244
|
+
// · NON-durable + body proof — the prewarm inside persistLkgDurable just hash-verified every enabled
|
|
2245
|
+
// body on disk, so the miss was a TRANSIENT live-application failure and a restarted process
|
|
2246
|
+
// re-pulls successfully. WITHOUT the proof (persistent 404/hash mismatch) publishing here would
|
|
2247
|
+
// restart-storm a stateless deployment (codex R37) — that case falls to the retained-debt arm
|
|
2248
|
+
// below: visible on /health, retried per confirming 304, promoted only once the proof holds.
|
|
1305
2249
|
pendingRestart = { restartRequired: true, reasons: ["skills"], version: bootLkgCandidate.effective.version, since: Date.now() };
|
|
1306
2250
|
logger.warn("config_boot_skill_stale_restart", { version: bootLkgCandidate.effective.version, durableHandoff: bootPublished, note: bootPublished ? "live skill application incomplete but the durable LKG carries the full candidate — skills restart published (the next boot reads the complete LKG)" : "live skill application incomplete on a non-durable deployment but every enabled body is verified in the cache — skills restart published (transient failure proven repaired; the restarted process re-pulls live)" });
|
|
1307
2251
|
}
|
|
@@ -1314,11 +2258,42 @@ async function main() {
|
|
|
1314
2258
|
if (effective?.mcp)
|
|
1315
2259
|
config.mcpServers = resolveMcpServers(effective.mcp, logger);
|
|
1316
2260
|
const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
|
|
2261
|
+
// CC-parity: deployment-injected WebSearch backend (the leg core leaves open). Absent WEB_SEARCH_PROVIDER →
|
|
2262
|
+
// undefined → the default scenario doesn't assemble the WebSearch tool. The API key stays in the backend closure.
|
|
1317
2263
|
const webSearchCfg = webSearchConfigFromEnv();
|
|
1318
2264
|
const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
|
|
1319
2265
|
if (webSearch)
|
|
1320
2266
|
logger.info(`WebSearch enabled (provider=${webSearchCfg.provider}, maxResults=${webSearch.maxResults})`);
|
|
2267
|
+
// core 1.382([1561] 提货单①②,design/153 parked 状态机)「同车必接」的 ensureChildSessionDurable 挂点:
|
|
2268
|
+
// 只在 checkpointStore 真在场时构造(与 core parkEligible 门「两者全在场才成立」的判据同源——checkpoint
|
|
2269
|
+
// 不在场时构造这个闭包毫无意义,连带把「local/无 durable backend 部署不该跑这条 acquire/release」的
|
|
2270
|
+
// 边界直接编码进构造条件,不用额外判断)。实现体拆到 `ensureSessionDurable`(session-store.ts,可独立
|
|
2271
|
+
// 测试;双复审②抓的 F-7 同族空壳缺口——host 店可能存在 `register()` 留下的零内容 session_meta 行——
|
|
2272
|
+
// 的校验也在那边)。
|
|
2273
|
+
// 🔴 [1575] F3 修(cli 复查,红先行确认真红):`checkpointStore` 在场 ⇏ `sessionStore` 就是 TiDB 真店
|
|
2274
|
+
// ——这是两个独立 config 轴(DB_BACKEND 供 checkpoint/runs 等用途 vs SESSION_BACKEND 单独控 session
|
|
2275
|
+
// 落哪),`SESSION_BACKEND` 显式留在默认值时 `sessionStore` 是纯内存 `TtlSessionStore`(无 repo 构造,
|
|
2276
|
+
// core `release()` 语义在该形态下是**整删会话**,不是"归还租约")——探针的 finally release 会销毁它
|
|
2277
|
+
// 刚证明存在的会话。追加门 `config.sessionBackend === "tidb"`(此刻已被前面装配区从 "auto" 归一,
|
|
2278
|
+
// 恒是 "tidb"|"memory" 二选一,时序安全):唯一保留可达的店形正是 release() 真安全的那种
|
|
2279
|
+
// (TiDBSessionStore.release 逐字注释「Durable history is retained; just drop any in-flight cache
|
|
2280
|
+
// entry」)。非 tidb 场景下 undefined = 子代按 pre-153 settle failed,零回归(与 checkpointStore 缺席
|
|
2281
|
+
// 同一处置)。
|
|
2282
|
+
// [1593] 只查不迁 → 主动晋升(cli TiDB 真机撞获:bg 子会话住 subRunner 私有 TTL 店、从不在 host 店,
|
|
2283
|
+
// 旧形恒 veto → core 当场 expire checkpoint(14ms~2s)+ settle failed,赎回窗口不存在)。晋升链=
|
|
2284
|
+
// 快查 host → promoteToHost(整树拷贝+私店 forget+路由记 host)→ 同一严检复验(leaf 非空);任何
|
|
2285
|
+
// 一步失败仍如实 veto(pre-153 行为,不放大)。tidb 门与 F3 裁定不变。
|
|
1321
2286
|
const ensureChildSessionDurable = checkpointStore && config.sessionBackend === "tidb" ? (sessionId) => ensureChildSessionDurableWithPromotion(sessionStore, subRunnerSessions, sessionId) : undefined;
|
|
2287
|
+
// [1900]/[1904] WebFetch 摘要器:core 工厂 + summarize 角色模型(cheap tier;resolveTaskModel 自带
|
|
2288
|
+
// 默认回落——与压缩线同族档位,不新造)。构造一次全场景共享;多租户形 WebFetch 整体被 filter,
|
|
2289
|
+
// 本注入无效化由 scenarios 现行为保证。
|
|
2290
|
+
// 🔴 复审 E-MED-2/E-MED-3 两修:
|
|
2291
|
+
// ① **惰性解析**:模型每次调用现取——`config.models`/`config.roles` 会被 center 的 effective
|
|
2292
|
+
// 热应用 mutateInPlace 整表替换,boot 期急求值会把摘要器永久 pin 在占位/旧模型上(故障形态是
|
|
2293
|
+
// 静默退回上下文放大 + 每次一发无效 LLM 调用)。
|
|
2294
|
+
// ② **fail-soft**:core resolveTaskModel 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用;
|
|
2295
|
+
// center 发一张既无 summarize 又无 default 的 roles 表就会拒启。解析失败 ⇒ 本次不摘要
|
|
2296
|
+
// (core 的 summarize 抛错是 fail-open:回退原文+note),绝不崩 boot。
|
|
1322
2297
|
const webFetchSummarize = async (content, prompt, signal) => {
|
|
1323
2298
|
const model = (() => {
|
|
1324
2299
|
try {
|
|
@@ -1333,14 +2308,29 @@ async function main() {
|
|
|
1333
2308
|
throw new Error("summarize model unresolved for this deployment");
|
|
1334
2309
|
return createWebFetchSummarizer(brain, model)(content, prompt, signal);
|
|
1335
2310
|
};
|
|
2311
|
+
// design/158 S4:标注 `: ScenarioDeps` —— 无标注的字面量连自己的键名都不校验(打错/多写一个键
|
|
2312
|
+
// 只是多一个没人读的属性,编译期无声),这是 1.304 落错家那一族的另一半土壤。
|
|
1336
2313
|
const scenarioDeps = {
|
|
1337
2314
|
runner, subRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
|
|
1338
2315
|
webSearch: webSearch ? webSearch : undefined, metrics, logger, webFetchSummarize,
|
|
2316
|
+
// core 1.364 写半场:与 RunnerDeps.backgroundAgentStore 同实例(组装区注释;半接=静默死特性)。
|
|
1339
2317
|
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
2318
|
+
// core 1.382 parked 状态机「同车必接」一对——checkpointStore 与 main runner 的 checkpointStore(subRunner
|
|
2319
|
+
// 构造区上方 const)同实例;ensureChildSessionDurable 见上方构造闭包注释。
|
|
1340
2320
|
checkpointStore: checkpointStore ? checkpointStore : undefined,
|
|
1341
2321
|
ensureChildSessionDurable: ensureChildSessionDurable ? ensureChildSessionDurable : undefined,
|
|
1342
2322
|
oaApiBaseUrl: config.oaApiBaseUrl, oaServiceToken: config.oaServiceToken, oaIssue: config.oaIssue,
|
|
1343
2323
|
brandIdentity: config.configProvider === "local",
|
|
2324
|
+
// core 1.287 extraTools seam ([802]③ server half): SendUserFile reaches the delegated-child roster. SAME
|
|
2325
|
+
// existence gate as the top-level mount (the one sendUserFileToolSpec built above — lane/tenancy/config
|
|
2326
|
+
// posture already encoded in its presence). The spec is execute-time late-bound (ctx.taskId →
|
|
2327
|
+
// TaskEnvRegistry lookup; emit via ALS to the top-level frame tail), so returning the SAME instance per
|
|
2328
|
+
// spawn is correct — per-spawn state lives in the registry, not the spec ([797] confirmed with core).
|
|
2329
|
+
// codex F1/F2 gates (subagentSendUserFileExtraTools): (F1) the subRunner carries no toolPolicy and core
|
|
2330
|
+
// does not propagate the parent's policy into child specs, so an APPROVAL_DENY/ask-gated SendUserFile was
|
|
2331
|
+
// reachable from delegated children — the factory now pre-checks the LIVE deployment approval face
|
|
2332
|
+
// (deny/require/neverAuto, canonical-space) and injects nothing when gated (conservative stopgap until a
|
|
2333
|
+
// core child-policy seam lands); (F2) read-only builtin agents (Explore/Plan) never receive the tool.
|
|
1344
2334
|
subagentExtraTools: sendUserFileToolSpec
|
|
1345
2335
|
? subagentSendUserFileExtraTools({
|
|
1346
2336
|
spec: sendUserFileToolSpec,
|
|
@@ -1349,7 +2339,16 @@ async function main() {
|
|
|
1349
2339
|
: undefined,
|
|
1350
2340
|
};
|
|
1351
2341
|
const scenarios = buildScenarios(scenarioDeps);
|
|
2342
|
+
// design/153 件3d:/decide parked 赎回腿的裸 Agent 工具——boot 期从**内建 default** 场景工厂取一份
|
|
2343
|
+
// (必须在 center overlay 盖名之前取:赎回腿要的是与 spawn 同源的生产装配,subRunner/bg opts/
|
|
2344
|
+
// checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
|
|
2345
|
+
// driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
|
|
1352
2346
|
const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
|
|
2347
|
+
// [1596]/[1597] 跨进程父约束重供席工厂:host 任务的 toolPolicy 解析槽按部署配置**同参重建**
|
|
2348
|
+
// (resolveSpec durableEnabled 分支的同款折叠:durableQuestion + durableAsk 四旋钮),单层链
|
|
2349
|
+
// count=1,durableMandate=true(host 有 durableApproval 且 durable 车道无 live onAsk 的恒形)。
|
|
2350
|
+
// exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
|
|
2351
|
+
// 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
|
|
1353
2352
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
1354
2353
|
? (row) => ({
|
|
1355
2354
|
parentConstraints: [{
|
|
@@ -1366,8 +2365,13 @@ async function main() {
|
|
|
1366
2365
|
}],
|
|
1367
2366
|
})
|
|
1368
2367
|
: undefined;
|
|
2368
|
+
// 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
|
|
2369
|
+
// center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
|
|
1369
2370
|
const scenarioDetails = builtinScenarioDetails(scenarios, scenarioDeps);
|
|
1370
|
-
const builtinScenarioNames = Object.keys(scenarioDetails);
|
|
2371
|
+
const builtinScenarioNames = Object.keys(scenarioDetails); // overlay 前捕获(六内建名)
|
|
2372
|
+
// Center-declared scenarios (composition-as-config): overlay over the built-ins, center wins — it MAY
|
|
2373
|
+
// shadow a built-in name (usability first), so shadows are logged loudly here for audit.
|
|
2374
|
+
// Restart-to-apply like skills/mcp (this runs once at boot; a refresh carrying scenarios only signals).
|
|
1371
2375
|
if (effective?.scenarios) {
|
|
1372
2376
|
const { overlay, shadows } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
|
|
1373
2377
|
Object.assign(scenarios, overlay);
|
|
@@ -1376,23 +2380,59 @@ async function main() {
|
|
|
1376
2380
|
logger.info("sema_registry_scenarios", { scenarios: Object.keys(overlay), ...(shadows.length > 0 ? { shadowsBuiltin: shadows } : {}) });
|
|
1377
2381
|
}
|
|
1378
2382
|
logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
|
|
2383
|
+
// Background reapers: recover runs whose instance died, expire never-answered approvals, sweep
|
|
2384
|
+
// rate windows. Unref'd so they never hold the process open.
|
|
2385
|
+
// design/80 D-D (SLA-timer): the deny-sweep reuses the server's resumeCheckpoint, so it is wired in AFTER
|
|
2386
|
+
// createHttpServer returns it (below) via this holder — the reaper is defined before the server.
|
|
1379
2387
|
let runDenySweep;
|
|
2388
|
+
// S7 (SILENT-FALLBACK P0-d): the sweeps' return counts were discarded — an instance death that batch-fails
|
|
2389
|
+
// N orphans was indistinguishable from organic failures. Count + log ONLY when a sweep flipped rows (>0),
|
|
2390
|
+
// so healthy ticks stay silent. Tolerant of void-returning stores (typeof guard).
|
|
1380
2391
|
const reapCount = (metric, labels) => (n) => {
|
|
1381
2392
|
if (typeof n === "number" && n > 0) {
|
|
1382
2393
|
metrics.inc(metric, labels, n);
|
|
1383
2394
|
logger.info("reaper_swept", { metric, ...labels, count: n });
|
|
1384
2395
|
}
|
|
1385
2396
|
};
|
|
1386
|
-
let wfRunReapInFlight = false;
|
|
1387
|
-
let attachmentSweepInFlight = false;
|
|
1388
|
-
let bgAgentReapInFlight = false;
|
|
2397
|
+
let wfRunReapInFlight = false; // 1.108 review (lens③): serialize the all-scopes retention sweep across ticks
|
|
2398
|
+
let attachmentSweepInFlight = false; // 复审 F1:附件孤儿 sweep 的重入守卫(同上)
|
|
2399
|
+
let bgAgentReapInFlight = false; // core 1.364: same serialization for the durable bg-agent joint reap (per-scope serial loop)
|
|
2400
|
+
// core 1.364([1503] 提货单③):retention **只走** reapDurableAgents(联合 reap:条件删赢了才 release
|
|
2401
|
+
// 转录 session;裸 store.reap 会 strand 转录)。staleRunning 翻转在 core 编排内先行(store.reap 一步)。
|
|
2402
|
+
// scope 枚举:core 1.368 `BackgroundAgentStore.listScopes?()`([1516]② 交付,SQL twins/File 双实现
|
|
2403
|
+
// 都带)——1.248 拍的「file 腿只扫 default」假设已撤;无该方法的第三方 store 仍回落 "default"(可选
|
|
2404
|
+
// 接口成员,诚实回落)。
|
|
2405
|
+
// sessions=subRunner.sessions —— [1522] 裁定件(③ 显式裁定形,双店真相考据后拍):bg 子代转录会话
|
|
2406
|
+
// 是**双店**(ForkRoutingSessionStore):①非 fork 子代铸在 transient 店(TtlSessionStore,进程内
|
|
2407
|
+
// 1h TTL)——release=内存删=**真正终结可寻址性**(转录本就不 durable;跨实例读面读的是行内
|
|
2408
|
+
// finalOutput,非转录);②fork 子代铸在 host durable 店——release=lease 释放,durable 转录行**有意
|
|
2409
|
+
// 保留**,留存归 session 留存策略/E21 purge 生命周期管(行先删可接受:行是执行记录,转录是会话资产,
|
|
2410
|
+
// 两者生命周期本就不同)。core [1522] 复审只见 durable 腿判 HIGH——按其契约句「做不到终结可寻址性
|
|
2411
|
+
// 宁可不传」,transient 类做得到、fork 类是显式裁定,维持传入。sessionsReleased 计数=「store 侧
|
|
2412
|
+
// release 被调次数」(transient=真删,durable=lease 释放),非「durable 转录处置数」——如实注。
|
|
2413
|
+
// 复审 F4 如实注:条件删的赢者若非写者副本,fork-routing 对未知 id 回落 transient=本地 no-op,写者
|
|
2414
|
+
// 进程内 pinned 转录滞留到自身 TTL/进程退出——sessionsReleased 计数偏低是诚实读数,非缺陷。
|
|
1389
2415
|
const reapBgAgents = backgroundAgentStore
|
|
1390
2416
|
? async () => {
|
|
1391
2417
|
const store = backgroundAgentStore;
|
|
1392
2418
|
const scopes = await (store.listScopes?.() ?? Promise.resolve(["default"]));
|
|
1393
2419
|
let rows = 0, sessions = 0, skipped = 0, failedScopes = 0;
|
|
1394
2420
|
for (const scope of scopes) {
|
|
2421
|
+
// [1522] LOW3:per-scope 错误隔离——一个 scope 抛(坏 record_json/权限)不饿死后续 scope;
|
|
2422
|
+
// 失败响亮(warn+计数),下 tick 重试。
|
|
1395
2423
|
try {
|
|
2424
|
+
// core 1.382([1561] 提货单④,design/153 parked 状态机对账):先对账 parked 行(checkpoint
|
|
2425
|
+
// expired/missing → 行诚实翻 failed;stale claim → 回滚 parked),再让下面既有的
|
|
2426
|
+
// reapDurableAgents 按自然节奏处置(刚翻 failed 的行本 tick 不会立刻被 maxAge 删——它是
|
|
2427
|
+
// "刚失败"的新行,不是"失败很久"的老行)。**走 registry 面**(裂脑 fence 内建于
|
|
2428
|
+
// TaskRegistry.reconcileParkedAgents 内部,不裸调 store 面——那是无活实例场景专用,core
|
|
2429
|
+
// [1561]④ 措辞)。仅在 checkpointStore 真在场时跑(同 [1561]①②「同车必接」判据——没有
|
|
2430
|
+
// checkpointStore 就不可能有真正 parked 的行,调用本身没有意义)。
|
|
2431
|
+
// 🔴 [1575] F1 修(cli 复查,红先行确认):`opts.staleClaimMaxAgeMs` 必须显式传——core 把
|
|
2432
|
+
// 整个 stale-claim 清算块(claimer 崩死回滚 + resolved-未-finalize 诚实翻 failed)门在
|
|
2433
|
+
// `staleMs !== undefined` 上,缺席不是"跳过 stale-claim 这一小步",是**整块永不运行**
|
|
2434
|
+
// (此前漏传,`bg_agents_park_reconciled_rolledback_total` 恒 0,人已做出的审批决定会被
|
|
2435
|
+
// 静默丢弃——见 config.ts backgroundAgentParkClaimStaleMs 顶注)。
|
|
1396
2436
|
if (checkpointStore) {
|
|
1397
2437
|
const pr = await defaultTaskRegistry.reconcileParkedAgents({ agentStore: store, checkpointStore }, scope, Date.now(), { staleClaimMaxAgeMs: config.backgroundAgentParkClaimStaleMs });
|
|
1398
2438
|
if (pr.failed > 0 || pr.rolledBack > 0) {
|
|
@@ -1401,6 +2441,8 @@ async function main() {
|
|
|
1401
2441
|
logger.info("reaper_swept", { metric: "bg_agents_park_reconciled", scope, failed: pr.failed, rolledBack: pr.rolledBack });
|
|
1402
2442
|
}
|
|
1403
2443
|
}
|
|
2444
|
+
// deps.mailbox(core 1.374,[1533]②):行删联动 F-13——agent 行 reap 时同 drop 其信箱
|
|
2445
|
+
// (盒生命周期随行终结;不挂=信箱行无 retention 无界涨)。
|
|
1404
2446
|
const r = await defaultTaskRegistry.reapDurableAgents(scope, { store, sessions: subRunner.sessions, ...(mailboxStore ? { mailbox: mailboxStore } : {}) }, {
|
|
1405
2447
|
maxAgeMs: config.backgroundAgentRetentionMs,
|
|
1406
2448
|
staleRunningMaxAgeMs: config.backgroundAgentStaleRunningMs,
|
|
@@ -1422,7 +2464,12 @@ async function main() {
|
|
|
1422
2464
|
}
|
|
1423
2465
|
}
|
|
1424
2466
|
: undefined;
|
|
2467
|
+
// S10 (SILENT-FALLBACK P1): the write-behind counters bucket on LOCAL floor(now/windowMs) — replica clock
|
|
2468
|
+
// skew vs the DB splits a fleet window into disjoint buckets (soft-limit leak) with zero visibility. Probe
|
|
2469
|
+
// the DB clock each reaper tick; the gauge is the fleet-wide skew fingerprint (can be negative).
|
|
1425
2470
|
const probeClockSkew = async () => {
|
|
2471
|
+
// Probe-ok gauge: a failing probe froze the skew gauge at its last value with zero signal —
|
|
2472
|
+
// "probe ran, skew=N" vs "probe failing for 30 min" were indistinguishable.
|
|
1426
2473
|
try {
|
|
1427
2474
|
const dbMs = await backend?.dbNowMs?.();
|
|
1428
2475
|
if (typeof dbMs === "number" && Number.isFinite(dbMs)) {
|
|
@@ -1435,24 +2482,54 @@ async function main() {
|
|
|
1435
2482
|
}
|
|
1436
2483
|
};
|
|
1437
2484
|
const reaper = setInterval(() => {
|
|
1438
|
-
void probeClockSkew().catch(() => undefined);
|
|
2485
|
+
void probeClockSkew().catch(() => undefined); // S10
|
|
1439
2486
|
void runStore?.reapStale(config.runStaleSec * 1000).then(reapCount("runs_reaped_total", { kind: "stale" })).catch(() => undefined);
|
|
2487
|
+
// Durable F4 (design/45) + design/80 §3 inv#3 crash-safe backstop: CAS-expire checkpoints past their
|
|
2488
|
+
// deadline OR their absolute terminal_at backstop (≈ deny), then fail the suspended run rows whose
|
|
2489
|
+
// checkpoint was thereby expired (release task_active = unlock the session). These run EVERY tick,
|
|
2490
|
+
// REGARDLESS of APPROVAL_TIMEOUT_SEC — terminal_at (stamped at put, never before an explicit deadline)
|
|
2491
|
+
// bounds even a NULL-deadline pending checkpoint, so the backstop is the always-on safety net (it was
|
|
2492
|
+
// inert when nested under the approvalTimeoutSec>0 guard — adversarial finding). The run-row half is
|
|
2493
|
+
// checkpoint-STATE-driven (not a uniform timer) so it aligns with the per-row terminal_at. Global +
|
|
2494
|
+
// idempotent across replicas, no election.
|
|
1440
2495
|
if (checkpointStore) {
|
|
2496
|
+
// 轴A #5 注释落档(1.254):core CheckpointStore.reap 契约把弃置臂的 unpin 义务派给部署 reaper——
|
|
2497
|
+
// 真 pin 只存在于 in-memory TtlSessionStore;本部署 durable checkpoint 恒配 durable session 后端
|
|
2498
|
+
// (pin=no-op),deny-sweep 走 resumeCheckpoint=core 内部 unpin ✓。SESSION_BACKEND=memory+durable
|
|
2499
|
+
// checkpoint 的 niche dev 组合下 abort-expire 臂会把被钉会话泄到进程终——显式接受,不为 dev 形加腿。
|
|
1441
2500
|
void checkpointStore.reapExpired(Date.now()).then(reapCount("checkpoints_reaped_total", {})).catch(() => undefined);
|
|
1442
2501
|
void runStore?.failSuspendedWithExpiredCheckpoint().then(reapCount("runs_reaped_total", { kind: "expired_checkpoint" })).catch(() => undefined);
|
|
2502
|
+
// D-D SLA-timer: resolve-DENY human/irreversible_ask gates past their deadline (graceful — the model
|
|
2503
|
+
// continues with the denial), vs the abort reapExpired gives the other kinds. Bounded per tick.
|
|
1443
2504
|
void runDenySweep?.(Date.now()).catch(() => undefined);
|
|
1444
2505
|
}
|
|
2506
|
+
// The finer, OPT-IN per-approval TTL sweeps (APPROVAL_TIMEOUT_SEC): the F4 poll-gate store + the
|
|
2507
|
+
// time-based suspended-run reaper. Stay gated — reapSuspended with a 0 TTL would nuke ALL suspended rows
|
|
2508
|
+
// (cutoff = now). The absolute backstop above is the safety floor; this is the operator-chosen deadline.
|
|
1445
2509
|
if (config.approvalTimeoutSec > 0) {
|
|
1446
2510
|
void approvalStore?.expireStale(config.approvalTimeoutSec * 1000).catch(() => undefined);
|
|
1447
2511
|
void runStore?.reapSuspended(config.approvalTimeoutSec * 1000).then(reapCount("runs_reaped_total", { kind: "suspended" })).catch(() => undefined);
|
|
1448
2512
|
}
|
|
2513
|
+
// GC checkpoint_ctx rows whose checkpoint is gone (bound to the checkpoint lifecycle).
|
|
1449
2514
|
void checkpointStore?.reapCtx(Date.now() - config.runStaleSec * 1000).catch(() => undefined);
|
|
1450
2515
|
rateLimiter?.sweep();
|
|
1451
2516
|
if (costQuota instanceof CostQuota)
|
|
1452
|
-
costQuota.reap();
|
|
1453
|
-
void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined);
|
|
2517
|
+
costQuota.reap(); // TiDB variant self-reaps in its flush loop
|
|
2518
|
+
void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined); // optional extra: SQL twins only (the local FileToolResultStore persists like transcripts)
|
|
2519
|
+
// 2c session-sync: GLOBALLY GC grace-window orphan blobs (standalone /sync/blobs PUTs that were never
|
|
2520
|
+
// imported are otherwise collected only on reap()/deleteBySession(), which never fire for a never-imported scope).
|
|
2521
|
+
// Bounds the standalone-PUT orphan-blob exhaustion (review finding) to the grace window. Durable twins only (local
|
|
2522
|
+
// omits the seam → optional-chained no-op).
|
|
1454
2523
|
void fileSnapshotStore?.sweepOrphanBlobs?.().catch(() => undefined);
|
|
2524
|
+
// D-1 附件 TTL:上传后从未被任何 task 引用(session_id NULL)且超过 attachmentUnboundTtlMs 的行收割
|
|
2525
|
+
// (绑定行不在此收——随会话 E21 级联删)。best-effort,与其余 reaper 腿同姿。
|
|
1455
2526
|
void taskAttachmentStore?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs).then(reapCount("attachments_reaped_total", {})).catch(() => undefined);
|
|
2527
|
+
// D-1 孤儿**对象**彻底 GC(clay 拍 2026-07-28):对象存储 × meta 行对账,grace 默认 1h(上传先行窗
|
|
2528
|
+
// 保护;`ATTACHMENT_ORPHAN_GRACE_MS=0` 关掉本腿——同 tick 邻居都有旋钮,复审 F10)。
|
|
2529
|
+
// 列举失败=本轮 warn 跳过(「列不出来」绝不当「没有孤儿」),下轮再试。
|
|
2530
|
+
// 🔴 复审 F1:必须 in-flight 守卫——本腿时长随对象总数增长(20 万对象实测秒级、百万级可达 30s),
|
|
2531
|
+
// 无守卫时超过 tick 间隔即逐 tick 叠加(实测叠 6 层),同时压 LIST/SQL/DELETE 三面。邻居
|
|
2532
|
+
// wfRunReap/bgAgentReap 都有同款守卫。
|
|
1456
2533
|
if (config.attachmentOrphanGraceMs > 0 && !attachmentSweepInFlight && taskAttachmentStore?.sweepOrphanObjects) {
|
|
1457
2534
|
attachmentSweepInFlight = true;
|
|
1458
2535
|
void taskAttachmentStore
|
|
@@ -1464,29 +2541,61 @@ async function main() {
|
|
|
1464
2541
|
.catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }))
|
|
1465
2542
|
.finally(() => { attachmentSweepInFlight = false; });
|
|
1466
2543
|
}
|
|
2544
|
+
// P1d-β 2c session-sync: GC ABANDONED staged imports — staging-id session_event rows (`%#stg-%`) with NO
|
|
2545
|
+
// session_meta whose oldest row is older than the grace window (an in-flight stream stays fresh → never reaped).
|
|
2546
|
+
// Bounds the orphan-staging-row growth (a Phase B that opened a staging then never committed). Durable session
|
|
2547
|
+
// stores only (the local backend stages in memory → no durable rows; the seam is absent → optional-chained no-op).
|
|
1467
2548
|
void backend?.session()?.sweepStagingSessions?.().catch(() => undefined);
|
|
2549
|
+
// Bake-runner backstop (§P2.7): fail any `running` bake whose lease went stale (the runner crashed mid-build),
|
|
2550
|
+
// append a synthetic terminal `done{failed}` so SSE readers settle, and force-release the single-flight lease.
|
|
2551
|
+
// staleMs (≈3× the 30s heartbeat) bounds a healthy slow build so it is never wrongly reaped.
|
|
1468
2552
|
void imageBakes?.reapStaleBakes(config.imageBakes.staleMs).then(reapCount("bakes_reaped_total", {})).catch(() => undefined);
|
|
2553
|
+
// SVC-3 worktree isolation: deregister worktrees orphaned by a process crash (the Runner never reached
|
|
2554
|
+
// destroy → `git worktree remove` never ran). `git worktree prune` cleans registrations whose dirs are
|
|
2555
|
+
// already gone. Best-effort, userland (core ships no post-kill Runner hook); never throws. Unset = no-op.
|
|
1469
2556
|
void worktreeReap?.();
|
|
2557
|
+
// SVC-1 (adversarial-review HIGH): PERIODIC notify-recovery sweep (not just at boot) — re-delivers a terminal
|
|
2558
|
+
// run whose in-process notify was lost, AND finalizes-as-abandoned a `running` run orphaned past the grace
|
|
2559
|
+
// window (core never resumes/reaps a prior `running` row, so nothing else would). Idempotent; best-effort.
|
|
1470
2560
|
void workflowNotifyGate?.recover({ orphanGraceMs: config.workflowOrphanGraceMs }).catch(() => undefined);
|
|
2561
|
+
// SVC-2 (adversarial-review HIGH): time-based GC of the workflow_journal table (the heaviest, TaskResult-bearing
|
|
2562
|
+
// one) — the per-run deleteByRun has no run-store reap hook, so this bounded sweep is what stops unbounded
|
|
2563
|
+
// growth. A resume of a journal older than the retention window re-runs live (resume is an optimization).
|
|
1471
2564
|
void workflowJournalStore?.reapExpired?.(Date.now(), config.workflowJournalRetentionMs).catch(() => undefined);
|
|
2565
|
+
// Retention (WorkflowRunStore never auto-purges, reap is explicit):
|
|
2566
|
+
// age out TERMINAL workflow_run rows across ALL scopes. SQL twins only (reapAllScopes is their DISTINCT-scope
|
|
2567
|
+
// extension; the contract itself has no cross-scope enumeration, and the File store keeps the transcripts-like
|
|
2568
|
+
// keep-everything posture). Running rows are untouched — the orphan-grace sweep above owns those.
|
|
2569
|
+
// In-flight guard (1.108 review, lens③): the sweep enumerates EVERY scope serially — under a short
|
|
2570
|
+
// REAP_INTERVAL_SEC + many scopes, overlapping sweeps would pile up on the pool. One at a time.
|
|
1472
2571
|
if (!wfRunReapInFlight) {
|
|
1473
2572
|
const sweep = sqlWorkflowRunStore?.reapAllScopes?.(Date.now(), { maxAgeMs: config.workflowRunRetentionMs });
|
|
1474
2573
|
if (sweep) {
|
|
1475
2574
|
wfRunReapInFlight = true;
|
|
1476
2575
|
void sweep.catch(() => undefined).finally(() => (wfRunReapInFlight = false));
|
|
1477
2576
|
}
|
|
2577
|
+
// 1.108: SQL notify-journal retention rides the same knob — ACKED rows are pure history (pending rows are
|
|
2578
|
+
// the recovery backlog and are NEVER reaped; the orphan-grace sweep retires a stuck pending run).
|
|
1478
2579
|
void workflowNotifyJournal
|
|
1479
2580
|
?.reapAcked?.(Date.now() - config.workflowRunRetentionMs)
|
|
1480
2581
|
.catch(() => undefined);
|
|
1481
2582
|
}
|
|
2583
|
+
// core 1.364 durable bg-agent joint reap(定义在 interval 上方,契约注释在彼)。in-flight 守卫同
|
|
2584
|
+
// wfRun sweep(per-scope 串行循环,短 tick + 多 scope 下不叠罗汉)。
|
|
1482
2585
|
if (reapBgAgents && !bgAgentReapInFlight) {
|
|
1483
2586
|
bgAgentReapInFlight = true;
|
|
1484
2587
|
void reapBgAgents().catch(() => undefined).finally(() => (bgAgentReapInFlight = false));
|
|
1485
2588
|
}
|
|
2589
|
+
// [1522] MED2:agent_roster TTL 清理(core RB-23②③ 派给部署的半场)——SQL twins 扩展面
|
|
2590
|
+
// (reapOlderThan,duck probe;File/Memory 店 core 自带 maxAgeMs,无此面=no-op)。
|
|
1486
2591
|
void rosterStore
|
|
1487
2592
|
?.reapOlderThan?.(Date.now() - config.rosterRetentionMs)
|
|
1488
2593
|
.then(reapCount("roster_rows_reaped_total", {}))
|
|
1489
2594
|
.catch(() => undefined);
|
|
2595
|
+
// 修8(三路复审 absorb-2,接线 (b)):periodic scratchpad sweep — 无 E21 purge 兜到的孤儿目录(session 从未
|
|
2596
|
+
// DELETE、purge 当次失败、local 后端无 purge coordinator)按 mtime 过期回收。SCRATCHPAD_SWEEP_TTL_MS
|
|
2597
|
+
// (default 7d,0=禁用)。activeSessionIds 不传:活跃判据的诚实边界在 env-facts.ts 的 sweep 文档——TTL 7d
|
|
2598
|
+
// 远大于任何在飞任务;一个 7 天零写入的 scratchpad 被回收是可接受的(它本就是临时区,fact 文案即如此宣示)。
|
|
1490
2599
|
if (config.scratchpadSweepTtlMs > 0) {
|
|
1491
2600
|
void sweepStaleScratchpads(config.localDataRoot ?? localRoot, { olderThanMs: config.scratchpadSweepTtlMs })
|
|
1492
2601
|
.then((removed) => {
|
|
@@ -1497,29 +2606,69 @@ async function main() {
|
|
|
1497
2606
|
}
|
|
1498
2607
|
}, config.reapIntervalSec * 1000);
|
|
1499
2608
|
reaper.unref?.();
|
|
2609
|
+
// Optional OTLP/HTTP metrics export (1.37). Periodically pushes the registry to an OTel collector;
|
|
2610
|
+
// best-effort (a collector outage is logged, never affects serving). /metrics stays available too.
|
|
1500
2611
|
const otelExporter = config.otel
|
|
1501
2612
|
? startOtlpExporter(metrics, {
|
|
1502
2613
|
...config.otel,
|
|
1503
2614
|
onError: (err) => logger.warn("otel_export_failed", { err: String(err) }),
|
|
1504
2615
|
})
|
|
1505
2616
|
: undefined;
|
|
2617
|
+
// Config refresh: re-pull with the ETag; teams hot-reload via the registry. A models/roles
|
|
2618
|
+
// change updates config but the live Runner keeps its startup set — logged so ops restart to apply.
|
|
2619
|
+
// 提示词装配协议 §9.5-1/-3: the loop goes through the ConfigProvider seam (never the raw HTTP fetch —
|
|
2620
|
+
// one resolver, no remote/local drift) and runs for the LOCAL provider too: config.d edits are picked
|
|
2621
|
+
// up by the same 60s poll (the local provider's etag = the store's raws hash, so an unchanged tree is
|
|
2622
|
+
// a cheap null and a corrupt↔fixed flip bumps the version). Local mode previously read config.d
|
|
2623
|
+
// exactly once at boot.
|
|
1506
2624
|
if (configProvider) {
|
|
1507
2625
|
const ccRef = config.configCenter;
|
|
2626
|
+
// Single-flight (codex F2): an async tick body under setInterval has no serialization — a slow read
|
|
2627
|
+
// (degraded disk/远端) outliving the 60s interval would let a LATER tick apply a newer candidate and
|
|
2628
|
+
// the stalled OLDER read then complete and roll the config (and ccEtag) back. Skip the tick while one
|
|
2629
|
+
// is in flight; the flag flips in `finally`, so a throw never wedges the loop shut.
|
|
1508
2630
|
let refreshInFlight = false;
|
|
1509
2631
|
const refreshTick = async (prefetched) => {
|
|
1510
2632
|
if (refreshInFlight)
|
|
1511
2633
|
return;
|
|
1512
2634
|
refreshInFlight = true;
|
|
1513
2635
|
try {
|
|
2636
|
+
// codex-3/4 F1:欠账的 catalog 过渡在**每个真拍**开头无条件重试——与 fetch 成败/候选好坏解耦
|
|
2637
|
+
// (304、fetch 抛错、候选被整拒的拍此前全都跳过重试=盘错恢复后过渡仍卡死)。次序安全:本拍
|
|
2638
|
+
// 若随后采用了更新候选,adopt 自会覆盖/清空 pending(先重试旧账、后采新目标,永不倒退)。
|
|
1514
2639
|
await retryPendingCatalog("refresh-retry");
|
|
2640
|
+
// codex R18: the stuck-deferred state must not vanish behind the advanced etag — re-warn + count on
|
|
2641
|
+
// EVERY tick (incl. 304s and fetch-error ticks) while it persists; /health carries the same state.
|
|
2642
|
+
// The RECOVERY (persist retry + promotion) moved BELOW the fetch (codex R29): promoting before
|
|
2643
|
+
// adjudicating the current source generation could publish a SUPERSEDED candidate N as the
|
|
2644
|
+
// boot-visible LKG while N+1 exists — a restart in that window resurrects withdrawn config.
|
|
1515
2645
|
if (planeDeferredNoHandoff) {
|
|
1516
2646
|
metrics.inc("models_tiers_plane_deferred_stuck_total");
|
|
1517
2647
|
logger.warn("models_tiers_deferred_no_handoff", { version: planeDeferredNoHandoff.version, since: planeDeferredNoHandoff.since, note: "candidate still retained without a next-boot handoff — mount restart-surviving LKG storage + CONFIG_LKG_DURABLE=true, or restart manually after ensuring the candidate is re-pulled at boot" });
|
|
1518
2648
|
}
|
|
2649
|
+
// F3(LKG 复审):LKG-boot 的 deferred 到货把 unconditional 结果直接喂进来——丢弃它再按 LKG 的
|
|
2650
|
+
// etag 条件拉,会在「center 重建且 etag 值撞旧」的场景永远 304=陈旧配置钉死;消费原始结果则
|
|
2651
|
+
// 应用后 ccEtag=真实新值,陈旧面消除(顺带省一次拉取)。
|
|
1519
2652
|
const r = prefetched !== undefined ? prefetched : await configProvider.fetchEffective(ccEtag);
|
|
2653
|
+
// codex R23/R29 recovery lane: a 304 (r === null) CONFIRMS the retained candidate's generation is
|
|
2654
|
+
// still the source's current one (its etag advanced when it was retained) — only then is it safe to
|
|
2655
|
+
// retry persistence and promote the withheld restart signal. A fetch that returned a NEWER candidate
|
|
2656
|
+
// takes the normal branches below (which replace/supersede the retained state); a fetch that THREW
|
|
2657
|
+
// never reaches here (freshness unknown ⇒ no promotion this tick). Retry only where promotion is
|
|
2658
|
+
// possible (durable-declared lane; R37 adds the non-durable skills-debt lane, whose retry doubles as
|
|
2659
|
+
// the repair proof) — elsewhere a per-tick write could never promote and just burns IO.
|
|
1520
2660
|
if (r === null && planeDeferredNoHandoff) {
|
|
2661
|
+
// Retry where promotion is possible: the durable-declared lane (full handoff), and — codex R37 —
|
|
2662
|
+
// the NON-durable skills-debt lane, where the retry's body prewarm+verify is the repair PROOF that
|
|
2663
|
+
// gates the withheld skills restart (see lastPrewarmBodiesVerified).
|
|
1521
2664
|
const skillsDebt = (planeDeferredNoHandoff.blocked ?? []).includes("skills");
|
|
1522
2665
|
const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
|
|
2666
|
+
// codex R37 proof-gated promotion (non-durable lane): every enabled body now hash-verifies on disk
|
|
2667
|
+
// ⇒ the center + body endpoint are BOTH live, a restarted process re-pulls successfully — publish
|
|
2668
|
+
// the skills restart exactly now, never on manifest-success alone (restart-storm breaker: a boot
|
|
2669
|
+
// that fails the body again lacks the proof and returns to visible-debt, not to signaling).
|
|
2670
|
+
// models-tiers never rides this arm (R16: a FORCED plane restart requires a durable handoff), so a
|
|
2671
|
+
// retained candidate that ALSO defers the plane keeps everything withheld until durability exists.
|
|
1523
2672
|
const skillsProofPromotion = !promoted && !lkgSurvivesRestart && skillsDebt && lastPrewarmBodiesVerified && planeDeferredNoHandoff.planeDeferred !== true;
|
|
1524
2673
|
if (skillsProofPromotion) {
|
|
1525
2674
|
pendingRestart = { restartRequired: true, reasons: ["skills"], version: planeDeferredNoHandoff.version, since: Date.now() };
|
|
@@ -1527,6 +2676,10 @@ async function main() {
|
|
|
1527
2676
|
planeDeferredNoHandoff = undefined;
|
|
1528
2677
|
}
|
|
1529
2678
|
else if (promoted) {
|
|
2679
|
+
// models-tiers joins only when the PLANE was deferred (codex R27: a skill-only retained candidate
|
|
2680
|
+
// must not manufacture a plane reason on promotion). codex R30: an EMPTY promotion (boot
|
|
2681
|
+
// publication retry — nothing was withheld, everything already applied) publishes NO restart
|
|
2682
|
+
// signal; it just completes the LKG and clears the state.
|
|
1530
2683
|
const promotedReasons = [...new Set([...(planeDeferredNoHandoff.blocked ?? []), ...(planeDeferredNoHandoff.planeDeferred ? ["models-tiers"] : [])])];
|
|
1531
2684
|
if (promotedReasons.length > 0) {
|
|
1532
2685
|
pendingRestart = { restartRequired: true, reasons: promotedReasons, version: planeDeferredNoHandoff.version, since: Date.now() };
|
|
@@ -1536,6 +2689,17 @@ async function main() {
|
|
|
1536
2689
|
}
|
|
1537
2690
|
}
|
|
1538
2691
|
if (r) {
|
|
2692
|
+
// §9.5-5 candidate gate: ANY invalid domain rejects the WHOLE candidate — the live config
|
|
2693
|
+
// keeps serving the LKG (`latestEffective`) instead of that domain falling to schema-default/
|
|
2694
|
+
// empty. ccEtag is deliberately NOT advanced: the bad tree stays visible (re-judged each poll),
|
|
2695
|
+
// and fixing the file bumps the version so the next poll lands. The dedupe key is the SORTED
|
|
2696
|
+
// de-duplicated domain set (codex F3: the collector's ordering isn't guaranteed, a raw join
|
|
2697
|
+
// would re-warn every poll), and the first warn carries the full per-domain error detail —
|
|
2698
|
+
// that's the actionable diagnosis, the key only mutes repeats. The prompts-domain artifact
|
|
2699
|
+
// validation (core S1) hooks into this same gate when it ships.
|
|
2700
|
+
// F6(二轮复审):prompts 面判形进候选门——坏 pack(重复 id/空数组/坏信封)若只在 boot 验,
|
|
2701
|
+
// refresh 会带着它推进 etag+发 restart 信号,重启后 boot 拒之回内置=好包 A 被坏包 B 顶掉。
|
|
2702
|
+
// 这里与 domainErrors 同门:坏形=整拒候选(etag 不进,LKG/现用包 A 继续服务,持续 warn 到修复)。
|
|
1539
2703
|
const promptsRaw = r.effective.prompts;
|
|
1540
2704
|
const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
|
|
1541
2705
|
const badDomains = [
|
|
@@ -1560,11 +2724,28 @@ async function main() {
|
|
|
1560
2724
|
return;
|
|
1561
2725
|
}
|
|
1562
2726
|
lastRejectedCandidate = undefined;
|
|
2727
|
+
// ccEtag advances at the END of each branch (codex lens-1 HIGH): advancing before apply meant
|
|
2728
|
+
// an applyEffective throw mid-way left a MIXED-generation live config AND a cached etag — the
|
|
2729
|
+
// next poll 304'd, so the failed candidate was never retried. Now a throw keeps the old etag,
|
|
2730
|
+
// the next poll re-fetches the same candidate, and the (idempotent) re-apply self-heals.
|
|
1563
2731
|
if (ccRef?.dryRun) {
|
|
1564
|
-
logEffectiveDiff(config, r.effective, logger);
|
|
2732
|
+
logEffectiveDiff(config, r.effective, logger); // compare-only: surface ongoing center changes, apply nothing
|
|
1565
2733
|
ccEtag = r.etag;
|
|
1566
2734
|
}
|
|
1567
2735
|
else {
|
|
2736
|
+
// refresh: teams + runtime governance + MODELS/ROLES/ROSTER now hot. teamsOnly skips
|
|
2737
|
+
// only the boot-only runtime GATES (rate/cost/approval). applyEffective mutates config.models/roles
|
|
2738
|
+
// IN PLACE (the Runner's live ref) — but `pricing` (separate object the Runner also reads live) and
|
|
2739
|
+
// `keyResolver` (closes over modelApiKeyEnv) are ours to refresh.
|
|
2740
|
+
// codex R10: hot-apply of the MODEL PLANE holds ONLY when the Runner shares our map (tier-less at
|
|
2741
|
+
// construction). A tier-frozen Runner keeps a private expanded copy — hot-applying a changed plane
|
|
2742
|
+
// would split admission from execution (same-key retarget silently routes/bills the stale Model
|
|
2743
|
+
// object through the restart window). Defer the plane; the models-tiers slice below signals the
|
|
2744
|
+
// orchestrator; the restarted process applies it at boot BEFORE Runner construction.
|
|
2745
|
+
// codex R12: defer also when the CANDIDATE activates tiers on a tier-less-booted Runner — it never
|
|
2746
|
+
// re-runs expandTiers, so hot-applying the arriving tier table would let tier words pass the
|
|
2747
|
+
// expanded admission gate while core throws "Unknown model ref" until restart. Hot-apply is safe
|
|
2748
|
+
// only when BOTH generations are tier-less.
|
|
1568
2749
|
const planeDeferred = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
|
|
1569
2750
|
applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferred ? { deferModelPlane: true } : {}) });
|
|
1570
2751
|
if (planeDeferred) {
|
|
@@ -1572,36 +2753,87 @@ async function main() {
|
|
|
1572
2753
|
}
|
|
1573
2754
|
else {
|
|
1574
2755
|
appliedPlaneEff = r.effective;
|
|
1575
|
-
planeDeferredNoHandoff = undefined;
|
|
2756
|
+
planeDeferredNoHandoff = undefined; // plane landed — the stuck state (if any) is resolved
|
|
1576
2757
|
}
|
|
1577
|
-
await adoptCenterPrompts(r.effective, "refresh");
|
|
2758
|
+
await adoptCenterPrompts(r.effective, "refresh"); // 热采用(core 1.316 声明轴:新任务边界自动重 pin,见 ref 注释)
|
|
2759
|
+
// codex R13: readiness may open ONLY off an APPLIED plane — a deferred candidate's roster is not
|
|
2760
|
+
// installed (config.models still the old/placeholder generation), so opening the billable gate on
|
|
2761
|
+
// it would route/bill the wrong model until restart. Deferred ⇒ gate stays as-is; the restarted
|
|
2762
|
+
// process applies the candidate pre-Runner and opens it at boot.
|
|
1578
2763
|
if (!planeDeferred)
|
|
1579
|
-
markRosterLanded(r.effective);
|
|
1580
|
-
mutateInPlace(pricing, buildPricing(config.models));
|
|
1581
|
-
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
2764
|
+
markRosterLanded(r.effective); // boot pull 失败/未发布时,refresh 落 roster 同样开门
|
|
2765
|
+
mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
|
|
2766
|
+
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
|
|
2767
|
+
// skills/mcp/scenarios/runtime-gates are baked into the live process at boot (buildScenarios / boot
|
|
2768
|
+
// middleware) — a refresh carrying a DIFFERENT value can't hot-apply, only a restart re-reads them.
|
|
2769
|
+
// We compare against the BOOT snapshot (`effective`), NOT presence: an orchestrator auto-restarts on
|
|
2770
|
+
// a non-empty `reasons` WITHOUT a restart loop (same diff every 60s is idempotent, not a fresh trigger).
|
|
2771
|
+
// Sticky `since`: keep the first-detected timestamp while `reasons` is unchanged; reset it when the set
|
|
2772
|
+
// changes; CLEAR the signal when a later pull reverts to boot (reasons empty) — restart no longer needed.
|
|
2773
|
+
// F2(LKG 复审):落盘 **await 且先于** restart 信号可见——orchestrator 见信号即杀进程,
|
|
2774
|
+
// fire-and-forget 的 rename 可能没完成 → 下一进程读旧 LKG → 同差异重现 = 本功能要消的环。
|
|
2775
|
+
// 顺序=先持久化(含 skill 正文预热,F1)再发信号;持久化失败=warn+照发(不发=配置永不生效;
|
|
2776
|
+
// 「盘坏+中心挂」双故障下环重现,接受并点名)。
|
|
1582
2777
|
const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
|
|
1583
2778
|
const reasons = restartReasons(effective, r.effective);
|
|
2779
|
+
// codex R35: the sticky boot skill debt rides EVERY tick's reasons — change-detection vs boot is
|
|
2780
|
+
// blind to it (the stale table IS the boot baseline). Under a no-handoff deferral it folds into
|
|
2781
|
+
// blocked (promoted later); otherwise it keeps pendingRestart alive until the restart happens.
|
|
2782
|
+
// codex R38: change-DETECTED skills (manifest actually changed) is distinguished from the sticky
|
|
2783
|
+
// rider — the former keeps the pre-existing documented posture (rides regardless; the restart
|
|
2784
|
+
// pulls the NEW generation live), the latter is proof-gated below on the non-durable lane.
|
|
1584
2785
|
const skillsChangeDetected = reasons.includes("skills");
|
|
1585
2786
|
if (bootSkillStale && !skillsChangeDetected)
|
|
1586
2787
|
reasons.push("skills");
|
|
2788
|
+
// codex R15/R16/R19: a DEFERRED plane is adjudicated by HANDOFF STATUS alone — independent of
|
|
2789
|
+
// whether change-detection already produced the reason (R19: an already-present models-tiers with
|
|
2790
|
+
// no handoff sailed through = the exact storm R16 closed). With a confirmed next-boot handoff the
|
|
2791
|
+
// reason is ensured present (R15: env-baseline blind spot) and the stuck state clears; without one
|
|
2792
|
+
// the reason is SUPPRESSED (a restart the next process cannot satisfy — over-budget boot pull + no
|
|
2793
|
+
// LKG — is a rolling-restart storm) and the persistent stuck state + per-tick warn carry the
|
|
2794
|
+
// observability instead. Non-plane reasons (skills/mcp/…) keep riding either way.
|
|
1587
2795
|
if (planeDeferred) {
|
|
1588
2796
|
if (lkgPersisted) {
|
|
1589
2797
|
if (!reasons.includes("models-tiers"))
|
|
1590
2798
|
reasons.push("models-tiers");
|
|
1591
|
-
planeDeferredNoHandoff = undefined;
|
|
2799
|
+
planeDeferredNoHandoff = undefined; // handoff exists — the restart signal carries observability
|
|
1592
2800
|
}
|
|
1593
2801
|
else {
|
|
2802
|
+
// codex R22: suppress the candidate's ENTIRE restart signal, not just models-tiers — a
|
|
2803
|
+
// co-occurring skills/mcp reason would still restart the worker, and with no handoff the
|
|
2804
|
+
// replacement can't land ANY of this candidate (same over-budget pull) = the same storm.
|
|
2805
|
+
// The blocked reasons stay observable (state + warn) and promote once a handoff exists.
|
|
1594
2806
|
const blocked = reasons.splice(0, reasons.length);
|
|
1595
2807
|
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), ...(blocked.length > 0 ? { blocked } : {}), planeDeferred: true, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1596
2808
|
logger.warn("models_tiers_deferred_no_handoff", { version: r.effective.version, blockedReasons: blocked, note: "plane deferred but no NEXT-BOOT handoff (LKG disabled/unwritable, or storage not declared restart-surviving — set CONFIG_LKG_DURABLE=true) — the WHOLE restart signal is withheld (it would loop); the old generation keeps serving" });
|
|
1597
2809
|
}
|
|
1598
2810
|
}
|
|
1599
2811
|
else if (lkgSurvivesRestart && !lkgPersisted) {
|
|
2812
|
+
// codex R27/R31: retention is NOT conditional on restart reasons — a hot-only candidate
|
|
2813
|
+
// (autonomy/prompts/teams/tier-less models) with a failed publication would otherwise advance
|
|
2814
|
+
// the etag and never retry (304s), and a restart during a center outage would roll back to the
|
|
2815
|
+
// older LKG (weaker governance). blocked may be [] = publication-only recovery (R30 promotion
|
|
2816
|
+
// publishes no signal for an empty set).
|
|
2817
|
+
// codex R27: a DURABLE-DECLARED deployment whose candidate publication is incomplete (skill-body
|
|
2818
|
+
// verify failed → LKG not replaced) must not signal restart off that candidate either — the next
|
|
2819
|
+
// boot would land the OLD LKG and re-signal while the center stays unavailable. Retain + withhold
|
|
2820
|
+
// + per-tick retry-promote, same machinery as the deferred plane (no models-tiers added on
|
|
2821
|
+
// promotion — the plane was NOT deferred here). Non-declared deployments keep the pre-existing
|
|
2822
|
+
// documented posture (restart converges off a live boot pull when the center is healthy).
|
|
1600
2823
|
const blocked = reasons.splice(0, reasons.length);
|
|
1601
2824
|
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1602
2825
|
logger.warn("restart_candidate_publication_incomplete", { version: r.effective.version, blockedReasons: blocked, note: "durable LKG declared but the candidate's publication is incomplete (skill bodies unverified) — restart signal withheld; retried every tick until the cache completes" });
|
|
1603
2826
|
}
|
|
1604
2827
|
else if (!planeDeferred && !lkgSurvivesRestart && bootSkillStale && !skillsChangeDetected && !lastPrewarmBodiesVerified) {
|
|
2828
|
+
// codex R38: the NON-durable lane's sticky rider must clear the SAME proof gate as the boot/304
|
|
2829
|
+
// arms (R37) — a same-generation 200 (center omits/rotates ETags or ignores conditionals) lands
|
|
2830
|
+
// here with reasons=[skills] purely from the sticky debt, and publishing it unproven re-opens
|
|
2831
|
+
// the restart storm R37 closed (every replacement fails the same body, 200s again, signals
|
|
2832
|
+
// again). persistLkgDurable just ran for THIS candidate, so the proof flag is current: unproven
|
|
2833
|
+
// ⇒ strip the rider from the publishable set and retain it as visible debt (the same
|
|
2834
|
+
// planeDeferredNoHandoff machinery the 304 recovery lane re-verifies and proof-promotes).
|
|
2835
|
+
// Proven or durable-handoff ⇒ the rider rides as before. Change-DETECTED skills is exempt
|
|
2836
|
+
// (pre-existing posture — the restart pulls the NEW generation, a different repair claim).
|
|
1605
2837
|
const i = reasons.indexOf("skills");
|
|
1606
2838
|
if (i >= 0) {
|
|
1607
2839
|
reasons.splice(i, 1);
|
|
@@ -1620,10 +2852,17 @@ async function main() {
|
|
|
1620
2852
|
}
|
|
1621
2853
|
logger.info("sema_registry_refreshed", {
|
|
1622
2854
|
version: r.effective.version,
|
|
2855
|
+
// hot-applied: teams + runtime governance (autonomy/commandPolicy) + MODELS/ROLES/ROSTER (+pricing
|
|
2856
|
+
// +per-model keys). Still restart-to-apply: skills/mcp/scenarios + runtime GATES (rate/cost/approval).
|
|
1623
2857
|
note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
|
|
2858
|
+
// Structured signal: orchestrator consumes the SAME object via GET /health.restart.
|
|
1624
2859
|
...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
|
|
1625
2860
|
});
|
|
1626
|
-
|
|
2861
|
+
// BOTH the LKG and the etag advance ONLY after the whole apply/pricing/keyResolver/restart
|
|
2862
|
+
// chain succeeded (codex lens-3): if any step above throws, latestEffective keeps pointing at
|
|
2863
|
+
// the last FULLY-applied candidate and the etag stays put, so the next poll re-fetches and
|
|
2864
|
+
// re-applies the same candidate (idempotent self-heal) — the LKG never names a half-applied one.
|
|
2865
|
+
latestEffective = r.effective; // LKG advance (§9.5-2): candidate validated + fully applied
|
|
1627
2866
|
ccEtag = r.etag;
|
|
1628
2867
|
}
|
|
1629
2868
|
}
|
|
@@ -1637,9 +2876,21 @@ async function main() {
|
|
|
1637
2876
|
};
|
|
1638
2877
|
const ccTimer = setInterval(() => void refreshTick(), 60_000);
|
|
1639
2878
|
ccTimer.unref?.();
|
|
2879
|
+
// Boot-deferred continuation(二轮复审 F5 改形):到货结果按「迟到的 boot」处理,而不是转普通 tick——
|
|
2880
|
+
// 普通 tick 的 restartReasons(undefined, r) 会把 prompts/skills 面全判为差异 → restart → 中心持续慢时
|
|
2881
|
+
// 下一进程又 deferred → 无限重启环。迟到 boot:①候选门同 F6(坏形=拒,cadence 重判);②热面
|
|
2882
|
+
// teamsOnly apply(boot-only gates/skills/mcp 已按 env 建,迟到不可应用——诚实);③prompts adopt
|
|
2883
|
+
// (进程刚起、会话面≈空,失真面⊆重启路径,且 adopt 后 prompts 不再贡献 restart 理由=prompts 无环);
|
|
2884
|
+
// ④一次性诚实 restart 信号=剩余 restart-to-apply 面(skills/mcp/scenarios/gates)非空才发;⑤基线
|
|
2885
|
+
// `effective`/LKG/etag 落位 → 后续 cadence 与到货值比较,不重复触发。⚠️ 残余环面(诚实记账):center
|
|
2886
|
+
// 发布了 skills/mcp 且中心**持续**慢于 boot 预算时,每个新进程仍会为真缺失的 skills 面发一次 restart
|
|
2887
|
+
// ——完整解=LKG 落盘(boot 直读上个进程的干净候选),已报 clay 裁量。
|
|
1640
2888
|
const deferredBootApply = async (r) => {
|
|
1641
2889
|
if (!r)
|
|
1642
2890
|
return;
|
|
2891
|
+
// F6(LKG 复审三轮):与 cadence tick 同一 single-flight 车道——deferred 的 skill 预热可能拖过 60s,
|
|
2892
|
+
// 期间 cadence 落了更新的候选 B,A 迟到完成再覆盖盘/信号=回滚。inFlight 时直接让行(丢弃 deferred
|
|
2893
|
+
// 结果无损:cadence 已经/即将拉到更新的),持有 flag 至信号发布后。
|
|
1643
2894
|
if (refreshInFlight)
|
|
1644
2895
|
return;
|
|
1645
2896
|
refreshInFlight = true;
|
|
@@ -1666,6 +2917,9 @@ async function main() {
|
|
|
1666
2917
|
ccEtag = r.etag;
|
|
1667
2918
|
return;
|
|
1668
2919
|
}
|
|
2920
|
+
// codex R10 (deferred-boot twin of the refresh-lane guard): the Runner was built from the ENV plane before
|
|
2921
|
+
// this late arrival — if it froze a tier-expanded copy, the arriving center plane must not hot-apply
|
|
2922
|
+
// (admission/Runner split). Tier-less env boot (the common deferred-boot shape) keeps true hot-apply.
|
|
1669
2923
|
const planeDeferredLate = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
|
|
1670
2924
|
applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferredLate ? { deferModelPlane: true } : {}) });
|
|
1671
2925
|
if (planeDeferredLate)
|
|
@@ -1676,14 +2930,23 @@ async function main() {
|
|
|
1676
2930
|
}
|
|
1677
2931
|
await adoptCenterPrompts(r.effective, "boot-deferred");
|
|
1678
2932
|
if (!planeDeferredLate)
|
|
1679
|
-
markRosterLanded(r.effective);
|
|
2933
|
+
markRosterLanded(r.effective); // codex R13: same guard as the refresh lane — never open readiness off an unapplied plane
|
|
1680
2934
|
mutateInPlace(pricing, buildPricing(config.models));
|
|
1681
2935
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
1682
|
-
effective = r.effective;
|
|
2936
|
+
effective = r.effective; // restart 比较基线=到货值(cadence 不再重复触发)
|
|
1683
2937
|
latestEffective = r.effective;
|
|
1684
2938
|
ccEtag = r.etag;
|
|
1685
|
-
const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
|
|
1686
|
-
const reasons = restartReasons(undefined, r.effective);
|
|
2939
|
+
const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag); // F2 parity:信号可见前落盘+skill 正文预热
|
|
2940
|
+
const reasons = restartReasons(undefined, r.effective); // (prompts 不在 restart slices——热采用已在上方 adopt)
|
|
2941
|
+
// codex R15 (late-boot twin): tiered env boot + tier-less late candidate ⇒ deferral fires but BOTH slice
|
|
2942
|
+
// fingerprints reduce to null (undefined baseline, tier-less candidate) — no models-tiers reason, plane
|
|
2943
|
+
// deferred forever. Deferral is the ground truth; force the reason so the orchestrator restarts and the
|
|
2944
|
+
// next process lands the candidate at boot. codex R16: forced ONLY with a confirmed LKG handoff — without
|
|
2945
|
+
// it the restarted process re-enters this exact lane (env plane, late candidate) = restart storm; keep
|
|
2946
|
+
// serving the old generation and warn instead.
|
|
2947
|
+
// codex R19 (late-boot twin): handoff status decides, regardless of whether change-detection already
|
|
2948
|
+
// produced the reason — an active-tier candidate over an env boot deterministically carries models-tiers
|
|
2949
|
+
// from restartReasons(undefined, candidate), and letting it through with no handoff is the R16 storm.
|
|
1687
2950
|
if (planeDeferredLate) {
|
|
1688
2951
|
if (lkgPersistedLate) {
|
|
1689
2952
|
if (!reasons.includes("models-tiers"))
|
|
@@ -1691,12 +2954,16 @@ async function main() {
|
|
|
1691
2954
|
planeDeferredNoHandoff = undefined;
|
|
1692
2955
|
}
|
|
1693
2956
|
else {
|
|
2957
|
+
// codex R22 (late-boot twin): withhold the WHOLE signal — co-occurring skills/mcp reasons would
|
|
2958
|
+
// restart into the same over-budget/no-LKG lane and loop.
|
|
1694
2959
|
const blocked = reasons.splice(0, reasons.length);
|
|
1695
2960
|
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), ...(blocked.length > 0 ? { blocked } : {}), planeDeferred: true, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1696
2961
|
logger.warn("models_tiers_deferred_no_handoff", { version: r.effective.version, blockedReasons: blocked, note: "late-boot plane deferred but no NEXT-BOOT handoff (see CONFIG_LKG_DURABLE) — the WHOLE restart signal is withheld (it would loop); old generation keeps serving" });
|
|
1697
2962
|
}
|
|
1698
2963
|
}
|
|
1699
2964
|
else if (planeDeferredLate === false && lkgSurvivesRestart && !lkgPersistedLate) {
|
|
2965
|
+
// codex R31 (late-boot twin): retain failed publications even with zero restart reasons.
|
|
2966
|
+
// codex R27 (late-boot twin): durable-declared + incomplete publication ⇒ retain + withhold + retry.
|
|
1700
2967
|
const blocked = reasons.splice(0, reasons.length);
|
|
1701
2968
|
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1702
2969
|
logger.warn("restart_candidate_publication_incomplete", { version: r.effective.version, blockedReasons: blocked, note: "late-boot candidate publication incomplete on a durable-declared deployment — restart signal withheld; retried every tick" });
|
|
@@ -1705,19 +2972,32 @@ async function main() {
|
|
|
1705
2972
|
pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
|
|
1706
2973
|
logger.info("config_loaded_deferred", { source: configProvider.kind, version: r.effective.version, models: (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length, ...(reasons.length > 0 ? { restartRequired: true, restartReasons: reasons } : {}) });
|
|
1707
2974
|
};
|
|
2975
|
+
// LKG 起服的进程有完整 boot 基线 → 到货走普通 tick(候选门+hot+对 LKG 的 restartReasons,通常
|
|
2976
|
+
// 零差异=无信号无环);env 起服(无 LKG)才走「迟到 boot」路径。
|
|
1708
2977
|
if (bootConfigPending)
|
|
1709
2978
|
void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
|
|
1710
2979
|
}
|
|
2980
|
+
// Per-model API key (sema-registry `apiKeyEnv`): resolve each model's own upstream key per brain call /
|
|
2981
|
+
// cascade rung. undefined when no per-model keys are configured → spec field stays unset (core default).
|
|
2982
|
+
// `let` (not const): rebuilt on a sema-registry refresh so per-model key ADDITIONS hot-apply too (the
|
|
2983
|
+
// resolver is undefined when no per-model keys exist, so in-place mutation alone wouldn't cover empty→non-empty).
|
|
1711
2984
|
let keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
2985
|
+
// PAIR-REVIEW F-6:Bearer-only 网关 × per-model key 组合死角的 boot 响亮告警——core 的 per-call auth 是
|
|
2986
|
+
// **整体替换**语义(1.345:per-call apiKey 在场即剥构造期 Authorization,x-api-key 成唯一凭据),而
|
|
2987
|
+
// key-resolver 只会产 { apiKey } 形;部署若以 ANTHROPIC_AUTH_TOKEN(Bearer)直连此类网关,则每个配了
|
|
2988
|
+
// per-model key 的模型必 401 且无从自诊。headers 形支持候 core/registry additive 席位(跨仓件);此前
|
|
2989
|
+
// 先把组合不可用讲清楚。只 warn 不拒:混合网关(两种头都收)是存在的,fail-open+可诊断。
|
|
1712
2990
|
if (process.env.ANTHROPIC_AUTH_TOKEN && !process.env.ANTHROPIC_API_KEY && (Object.keys(config.modelApiKeys).length > 0 || Object.keys(config.modelApiKeyEnv).length > 0)) {
|
|
1713
2991
|
logger.warn("per_model_key_bearer_gateway_combo", {
|
|
1714
2992
|
perModelKeys: Object.keys(config.modelApiKeys).length + Object.keys(config.modelApiKeyEnv).length,
|
|
1715
2993
|
note: "ANTHROPIC_AUTH_TOKEN (Bearer) deployment with per-model keys: core's per-call auth REPLACES the Authorization header with x-api-key for those models — a Bearer-only gateway will 401 them. Remove the per-model key for such models, or use a gateway accepting x-api-key.",
|
|
1716
2994
|
});
|
|
1717
2995
|
}
|
|
2996
|
+
// Audit回溯 + degenerate-output instrument are TiDB-specific raw-SQL consumers (outside the store
|
|
2997
|
+
// abstraction); they run only on the TiDB backend (the mysql2 pool), and are a no-op on PG until ported.
|
|
1718
2998
|
const mysqlPool = backend?.mysqlPool();
|
|
1719
|
-
const auditPgPool = backend?.pgPool();
|
|
1720
|
-
const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
|
|
2999
|
+
const auditPgPool = backend?.pgPool(); // the audit face now has a PG twin — web session area lights up on DB_BACKEND=pg
|
|
3000
|
+
const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined; // the File leg
|
|
1721
3001
|
const ownerAware = sessionStore;
|
|
1722
3002
|
const sessionAudit = mysqlPool || auditPgPool || auditLocalRoot
|
|
1723
3003
|
? async (sessionId) => {
|
|
@@ -1731,6 +3011,24 @@ async function main() {
|
|
|
1731
3011
|
return { ...audit, owner: (await ownerAware.ownerOf?.(sessionId)) ?? null };
|
|
1732
3012
|
}
|
|
1733
3013
|
: undefined;
|
|
3014
|
+
// §0.5 E21 — DELETE /v1/sessions/:id purge coordinator. Deletes the session abstraction's CONVERSATION HISTORY
|
|
3015
|
+
// (sessionStorage.deleteSession → session_meta/session_event) PLUS the service-owned operational rows for that
|
|
3016
|
+
// session: the runs ledger (runStore.deleteBySession → task_run/task_active/task_event, owner-guarded), the
|
|
3017
|
+
// durable checkpoints + resume-ctx (checkpointStore.deleteBySession, present only under DURABLE_APPROVAL), and
|
|
3018
|
+
// offloaded tool results (toolResultStore.deleteBySession). Wired only when the session store can actually
|
|
3019
|
+
// delete (a DB backend).
|
|
3020
|
+
//
|
|
3021
|
+
// 🔴 ORDERING (adversarial-review HIGH — privacy/right-to-delete + silent-success): the session HISTORY delete
|
|
3022
|
+
// (session_meta) is the `ownerOf` gate the DELETE route consults to decide idempotency. It MUST be the LAST
|
|
3023
|
+
// committed leg. If a privacy-relevant child leg (runs/checkpoint/tool-result) is deleted AFTER the meta row is
|
|
3024
|
+
// gone and then FAILS, the idempotent retry sees `ownerOf===undefined` (history already gone) → the route
|
|
3025
|
+
// short-circuits to 200 {deleted:false} and NEVER re-purges → those child rows are stranded while the API
|
|
3026
|
+
// reports success. So we purge the child legs FIRST and PROPAGATE any error (do NOT swallow): on a child
|
|
3027
|
+
// failure the history delete is SKIPPED, `session_meta` survives, the DELETE returns 500, and the idempotent
|
|
3028
|
+
// retry's `ownerOf` still resolves → it re-runs the FULL purge to convergence. `deleted` is computed from the
|
|
3029
|
+
// FINAL committed result (history-deleted || runsRemoved>0 — the privacy-relevant data that actually existed).
|
|
3030
|
+
// [1196] session-watch registry(SSE 订阅共享探针面)。谓词与 /head 探针同门(getLeafId+ownerOf);
|
|
3031
|
+
// env 旋钮经 posIntEnv 有界校验;getLeafId 箭头包一层=天然绑定宿主(SESSION_CACHE_TTL_SEC=0 裸类实例)。
|
|
1734
3032
|
const sessionWatchRegistry = ownerAware.getHead && ownerAware.ownerOf
|
|
1735
3033
|
? new SessionWatchRegistry(async (sid) => (await ownerAware.getHead(sid)) ?? { owner: undefined, leafId: null }, {
|
|
1736
3034
|
hotMs: posIntEnv(process.env.SESSION_EVENTS_HOT_MS, 200),
|
|
@@ -1738,29 +3036,73 @@ async function main() {
|
|
|
1738
3036
|
maxWatchedSessions: posIntEnv(process.env.SESSION_EVENTS_MAX_PROBES, 512, 100_000),
|
|
1739
3037
|
})
|
|
1740
3038
|
: undefined;
|
|
3039
|
+
// S2 fast path([1208]③ 兑现):同副本 append 落点(tidb/pg persist post-commit)经 leaf-bus 直推
|
|
3040
|
+
// notifyLocal——单副本部署零延迟;owner 不随 bus 传(写点无廉价 owner 读),探针围栏仍是租户权威
|
|
3041
|
+
// (session-leaf-bus 契约注)。local lane 无写钩,探针道照旧。
|
|
1741
3042
|
setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
|
|
1742
3043
|
const purgeSession = ownerAware.deleteSession
|
|
1743
3044
|
? async (sessionId, owner) => {
|
|
3045
|
+
// Runs-ledger FIRST: it transactionally re-asserts the no-active-run invariant (FOR UPDATE on task_active,
|
|
3046
|
+
// serializing against a concurrent createRun). If a run claimed the session in the route's check→purge
|
|
3047
|
+
// window it returns `{ active }` and deletes nothing → we bubble it up so the route 409s, BEFORE touching
|
|
3048
|
+
// any other table (no partial purge of a live session). Each leg propagates its error so a child failure
|
|
3049
|
+
// aborts the purge BEFORE the history-delete gate is removed (keeping the idempotent retry convergent).
|
|
1744
3050
|
const runs = runStore ? await runStore.deleteBySession(sessionId, owner) : { removed: 0 };
|
|
1745
3051
|
if ("active" in runs)
|
|
1746
3052
|
return { active: runs.active };
|
|
3053
|
+
// checkpoint rows carry no owner column → owner-guarded via a session_meta.owner sub-select, so they MUST
|
|
3054
|
+
// run while session_meta still exists (before the history delete below); tool_result is keyed only by
|
|
3055
|
+
// `ref` so it stays route-guarded (see its deleteBySession doc).
|
|
1747
3056
|
if (checkpointStore)
|
|
1748
3057
|
await checkpointStore.deleteBySession(sessionId, owner);
|
|
1749
3058
|
if (toolResultStore)
|
|
1750
|
-
await toolResultStore.deleteBySession?.(sessionId);
|
|
3059
|
+
await toolResultStore.deleteBySession?.(sessionId); // optional extra: SQL twins only (local file store has no per-session purge index; E21 purge is a durable-backend contract)
|
|
3060
|
+
// E18 resume-at anchors are per-session privacy-relevant metadata → purge ALL of them with the session, scoped
|
|
3061
|
+
// by session_id ALONE (the route already proved session ownership at the DELETE gate). A per-ROW owner guard
|
|
3062
|
+
// here would LEAK: an anchor's owner is the per-run submitting principal, which diverges from the canonical
|
|
3063
|
+
// session owner when an anonymous session is later attached by a principal (REQUIRE_PRINCIPAL=false) — those
|
|
3064
|
+
// mixed-owner rows would survive the delete (a right-to-delete violation reported as success).
|
|
1751
3065
|
if (resumeAnchorStore)
|
|
1752
3066
|
await resumeAnchorStore.deleteBySession(sessionId);
|
|
3067
|
+
// Approval exemptions are per-session operator decisions → purge with the session (same
|
|
3068
|
+
// session_id-alone scoping rationale as the anchors above).
|
|
1753
3069
|
if (approvalExemptionStore)
|
|
1754
3070
|
await approvalExemptionStore.deleteBySession(sessionId);
|
|
3071
|
+
// E6 session-policy rows are per-session operator rules → purge them too, scoped by session_id (the route
|
|
3072
|
+
// owner-gated the session). Since core 1.423 the seam is on the INTERFACE (optional) and core's File/InMemory
|
|
3073
|
+
// stores carry it too — so this fires on EVERY backend now. (The old note here claimed local rules were
|
|
3074
|
+
// "process-ephemeral anyway" — false for the file-backed local store, whose policy rows survive restarts;
|
|
3075
|
+
// that right-to-delete hole is what [1796]§三 → core 1.423 closed.)
|
|
1755
3076
|
if (sessionPolicyStore?.deleteBySession)
|
|
1756
3077
|
await sessionPolicyStore.deleteBySession(sessionId);
|
|
3078
|
+
// D-1 附件随会话删(E21 级联;session_id 单键 scoping,与 anchors 同理由——路由已证会话所有权)。
|
|
1757
3079
|
if (taskAttachmentStore)
|
|
1758
3080
|
await taskAttachmentStore.deleteBySession(sessionId);
|
|
3081
|
+
// E19 rewind-files snapshots are per-session working-tree state → purge them too (scoped by sessionId; the
|
|
3082
|
+
// durable twins carry deleteBySession, local InMemory omits it → optional no-op on local).
|
|
1759
3083
|
if (fileSnapshotStore?.deleteBySession)
|
|
1760
3084
|
await fileSnapshotStore.deleteBySession(sessionId);
|
|
3085
|
+
// P1 ①②: drop any pending async-workflow completions for this session (else they orphan when a later
|
|
3086
|
+
// tenant reclaims the sessionId). Best-effort + non-fatal — a leftover entry is ALSO gated by the
|
|
3087
|
+
// drain-time owner check, so a purge failure never leaks; it must not abort the session delete.
|
|
3088
|
+
// `owner` scopes the purge fence (1.80): only the DELETED session's own late completions are fenced —
|
|
3089
|
+
// a new tenant legitimately re-claiming this sessionId within the window keeps its push (review MED).
|
|
1761
3090
|
if (workflowCompletionInbox)
|
|
1762
3091
|
await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
|
|
3092
|
+
// 修8(三路复审 absorb-2,接线 (a)):the session's scratchpad dir ([820]③, ensureScratchpadDir) rides
|
|
3093
|
+
// the session lifecycle — purge it with the session. purgeScratchpadDir SELF-SWALLOWS (env-facts.ts
|
|
3094
|
+
// contract: cleanup never faults the delete path; the periodic sweep below is the convergence backstop),
|
|
3095
|
+
// so it cannot strand the E21 ordering invariant. Same root the ensure used (config.localDataRoot ?? localRoot).
|
|
1763
3096
|
await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
|
|
3097
|
+
// design/129 (core 1.239): reap this session's SESSION-scoped background children — they
|
|
3098
|
+
// deliberately outlive turns (backgroundScope:"session"), so the session's DELETE is their lifecycle
|
|
3099
|
+
// end (core's parent-teardown reap skips them by design; without this they run to their forced timeout).
|
|
3100
|
+
// Sync + replica-local (the registry is in-process); best-effort — a reap of 0 on the wrong replica is
|
|
3101
|
+
// covered by core's forced child-timeout cap.
|
|
3102
|
+
// [1892]{core} 跨仓待办:scope 轴必传——core canAccess 对缺省 scope fail-closed(拒绝非通配,
|
|
3103
|
+
// 1.441 复审①正过极性),不传 = 四类后台任务在会话删除时恒 0 回收(潜伏至今,core 复审顺手
|
|
3104
|
+
// 核出)。铸值同源 runs.ts:25 契约:core 按 `spec.principal ?? "default"` 注册 ⇒ 此处
|
|
3105
|
+
// owner(会话主)?? "default" 对齐;错 scope 不越租户由 canAccess 保证(session-reap-scope 钉)。
|
|
1764
3106
|
try {
|
|
1765
3107
|
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
|
|
1766
3108
|
if (reaped > 0)
|
|
@@ -1769,13 +3111,26 @@ async function main() {
|
|
|
1769
3111
|
catch (err) {
|
|
1770
3112
|
logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
|
|
1771
3113
|
}
|
|
3114
|
+
// History/meta LAST: once committed, the `ownerOf` gate is gone and the delete is durably complete. The
|
|
3115
|
+
// owner is threaded into the SQL guard (session_meta.owner) for data-layer defense-in-depth.
|
|
1772
3116
|
const historyDeleted = await ownerAware.deleteSession(sessionId, owner);
|
|
3117
|
+
// [1196] 三轮复审(租户轮转围栏,同副本半场):session 删除即终结其 SSE 订阅者+watch 条目——
|
|
3118
|
+
// 旧 owner 的活流不得跨越 delete/reclaim 边界收新租户的 head;跨副本半场=路由心跳期 owner 复核。
|
|
1773
3119
|
sessionWatchRegistry?.dropSession(sessionId);
|
|
1774
3120
|
return { deleted: historyDeleted || runs.removed > 0 };
|
|
1775
3121
|
}
|
|
1776
3122
|
: undefined;
|
|
3123
|
+
// Degenerate-output a/b instrument: needs the durable session log to inspect turn history (design/39
|
|
3124
|
+
// ② gating data). Only on the TiDB backend (mysql2 pool); env-only / PG deploys get no-op.
|
|
1777
3125
|
const instrumentDegenerate = mysqlPool ? makeDegenerateInstrument(mysqlPool, metrics, logger) : undefined;
|
|
3126
|
+
// Plan-cache recurrence probe (core design/42, INSTRUMENT-FIRST): pure in-memory, no backend needed —
|
|
3127
|
+
// counts per-scope task-objective recurrence so core can decide whether to build plan caching.
|
|
1778
3128
|
const planCacheProbe = new PlanCacheProbe(metrics, logger);
|
|
3129
|
+
// v2 leader endpoint (design/50 + design/68): wire when LEADER_ENABLED + an isolated remote-exec backend
|
|
3130
|
+
// (E2B or k8s/Kata — SSH/ADB are single-worker real-system backends, not fan-out targets). Default off →
|
|
3131
|
+
// zero prod impact. 🔴 a real run also needs `git` on the host PATH + a durable remote with write creds.
|
|
3132
|
+
// F2(a) factory mode (preferred) = the service's executionEnvFactory + MinIO self-upload diff-out; the k8s
|
|
3133
|
+
// lane REQUIRES it (no static k8s mode). E2B without MinIO falls back to the static compat mode.
|
|
1779
3134
|
const leaderMinio = process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
1780
3135
|
? {
|
|
1781
3136
|
s3: {
|
|
@@ -1788,6 +3143,9 @@ async function main() {
|
|
|
1788
3143
|
}
|
|
1789
3144
|
: {};
|
|
1790
3145
|
const leaderProvider = config.remoteExec?.provider;
|
|
3146
|
+
// DUAL-MODE §5: orchestration is an ENGINE capability, not fleet-only — the TOC `host` lane runs leader fan-out
|
|
3147
|
+
// bounded by ONE box (isolation=none, NON-durable: no snapshot, so the durable sub-worker suspend block below is
|
|
3148
|
+
// skipped — host workers run to completion in-process-adjacent). e2b/k8s keep their isolated/suspendable posture.
|
|
1791
3149
|
const leaderEndpoint = config.leaderEnabled &&
|
|
1792
3150
|
(leaderProvider === "e2b" ||
|
|
1793
3151
|
(leaderProvider === "k8s" && "s3" in leaderMinio && executionEnvFactory) ||
|
|
@@ -1795,21 +3153,37 @@ async function main() {
|
|
|
1795
3153
|
? createLeaderEndpoint(createLeaderRunner({
|
|
1796
3154
|
brain, models: config.models, roles: config.roles, pricing, logger,
|
|
1797
3155
|
fanoutEnabled: config.leaderFanoutEnabled,
|
|
3156
|
+
// Route the single-vs-fanout classification on the cheap model (deepseek-v4-flash): the heavy
|
|
3157
|
+
// reasoning worker model returns empty ~2/3 of the time on the route prompt → silent collapse to
|
|
3158
|
+
// single (observed 2026-06-14). MODEL_ROUTER_ID overrides; else the cheap model; else default.
|
|
1798
3159
|
...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
|
|
1799
3160
|
? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
|
|
1800
3161
|
: {}),
|
|
1801
3162
|
...(leaderProvider === "e2b" ? { e2bApiKey: config.remoteExec.apiKey } : {}),
|
|
1802
3163
|
...(executionEnvFactory ? { envFactory: executionEnvFactory } : {}),
|
|
1803
3164
|
...leaderMinio,
|
|
3165
|
+
// Durable offload store for sub-worker tool results — without it core REFUSES the durable
|
|
3166
|
+
// suspend (InMemory offload would not survive a cross-replica resume) and the gated call is
|
|
3167
|
+
// denied via the unwired onAsk fallback → the worker fails instead of suspending (drill c3).
|
|
1804
3168
|
...(toolResultStore ? { toolResultStore } : {}),
|
|
3169
|
+
// k8s lane: align the wire's workspace with the adapter's durable-snapshot root (/workspace) —
|
|
3170
|
+
// the E2B default (/home/user) is OUTSIDE the k8s suspend tar, so a suspended sub-worker's repo
|
|
3171
|
+
// would silently vanish on resume (drill c3: 105-byte empty snapshots).
|
|
1805
3172
|
...(leaderProvider === "k8s" ? { workspace: "/workspace" } : {}),
|
|
3173
|
+
// Same store as the main runner: a suspended sub-worker's session must be readable on /decide
|
|
3174
|
+
// resume (sub-runner-local in-memory sessions die with "Entry <leafId> not found" on resume).
|
|
1806
3175
|
sessionStore,
|
|
3176
|
+
// design/68 C4: sub-workers can durably suspend when the service runs durable approvals.
|
|
1807
3177
|
...(checkpointStore && config.approvalRequire.length > 0
|
|
1808
3178
|
? {
|
|
1809
3179
|
durable: {
|
|
1810
3180
|
checkpointStore,
|
|
1811
3181
|
requireApproval: config.approvalRequire,
|
|
1812
3182
|
deny: config.approvalDeny,
|
|
3183
|
+
// design/80: the leader's orchestrated sub-agents adopt the SAME supervisor loop as the
|
|
3184
|
+
// top-level agent — the D-E auto-budget (auto-approve NORMAL asks up to the budget, then
|
|
3185
|
+
// escalate) + the never-auto safety set. Without these a sub-agent's gated ask always
|
|
3186
|
+
// suspends-to-human, defeating the budget circuit-breaker for the supervised team.
|
|
1813
3187
|
autoBudget: config.approvalAutoBudget,
|
|
1814
3188
|
neverAuto: config.approvalNeverAuto,
|
|
1815
3189
|
...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
|
|
@@ -1818,12 +3192,20 @@ async function main() {
|
|
|
1818
3192
|
: {}),
|
|
1819
3193
|
}), {
|
|
1820
3194
|
logger,
|
|
3195
|
+
// SDK 全量核查(2026-07-24,黑板 GD 组)撞获:wire.ts:368 的 runLeader 实际要求这五个字段全部
|
|
3196
|
+
// 非空,此前同步门只查 objective,不完整请求会先拿到 202 再在后台必然失败——同步前移,让错误
|
|
3197
|
+
// 尽早暴露而不是靠后续 GET 才发现。
|
|
1821
3198
|
requiredFields: ["objective", "durableRemote", "testCmd", "seedCmd", "baseSha"],
|
|
1822
3199
|
})
|
|
1823
3200
|
: undefined;
|
|
1824
3201
|
if (leaderEndpoint)
|
|
1825
3202
|
logger.info("leader_endpoint_enabled", { provider: leaderProvider, mode: "s3" in leaderMinio ? "factory" : "static" });
|
|
3203
|
+
// Graceful drain: shared mutable state — SIGTERM flips `draining` (shutdown below), createHttpServer
|
|
3204
|
+
// assigns `inflight` (live leg count on this instance), /health + the submit 503 gate read it.
|
|
1826
3205
|
const drainState = { draining: false };
|
|
3206
|
+
// 鉴权桥(registry-core api/auth-bridge):SEMA_REGISTRY_URL 在位即点亮(零新配置——worker 已信
|
|
3207
|
+
// registry 的 effective config,同一信任根延伸到其 JWKS);split 部署用 AUTH_BRIDGE_ISSUER/JWKS_URL/AUDIENCE
|
|
3208
|
+
// 显式覆盖。缺 registry(纯 env 部署)=分支不存在,bearer 行为与今天字节一致。
|
|
1827
3209
|
const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
|
|
1828
3210
|
const registryJwtVerifier = authBridgeIssuer
|
|
1829
3211
|
? createRegistryJwtVerifier({
|
|
@@ -1836,108 +3218,212 @@ async function main() {
|
|
|
1836
3218
|
: undefined;
|
|
1837
3219
|
if (registryJwtVerifier)
|
|
1838
3220
|
logger.info("auth_bridge_enabled", { issuer: authBridgeIssuer });
|
|
3221
|
+
// asyncRewake wake 管道 holder:server 启动时装 deliver(session→本副本活流 steer);createTaskHooks 的
|
|
3222
|
+
// ctx.wake 闭包经它投递(main 建壳/server 填实,双向不循环依赖)。
|
|
1839
3223
|
const hookWakeBus = {};
|
|
3224
|
+
// ═══════════ design/158 A8:ServiceDeps 分组装配(七组窄接口)═══════════
|
|
3225
|
+
// 每组一个**带类型标注**的中间 const —— 标注位触发 TS 多余属性检查,键放错组当场编译红。
|
|
3226
|
+
// (平铺形做不到:69 键的全可选面里任何键名都「属于」它,于是键落错家/从未接线只能靠源码钉
|
|
3227
|
+
// 逐个盯 —— 1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族。)组形状门见
|
|
3228
|
+
// test/deps-literal-shape-gate.test.ts;消费点仍读平铺 `deps.x`(createHttpServer 入口摊平)。
|
|
3229
|
+
/** durable 持久面(键 present ⇔ 路由/能力位在场;absent ⇒ 501 诚实缺席) */
|
|
1840
3230
|
const stores = {
|
|
1841
3231
|
runStore,
|
|
1842
|
-
resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
|
|
3232
|
+
resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined, // E18 resume-at eventId→entryId map (tidb/pg/local)
|
|
1843
3233
|
approvalStore,
|
|
1844
|
-
approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined,
|
|
3234
|
+
approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined, // decide remember="session" + list/revoke
|
|
1845
3235
|
checkpointStore,
|
|
1846
|
-
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
|
|
3236
|
+
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined, // E6 operator session-rule store (PUT/GET /v1/sessions/:id/policy)
|
|
3237
|
+
// E19 cap: core's gate-split (1.134.0) REMOVED the isRemoteExecutionEnv skip — core now snapshots each
|
|
3238
|
+
// completed turn + restores on resumeAt for ANY ExecutionEnv when fileSnapshotStore is wired (captureManifest/
|
|
3239
|
+
// applyManifest run over any env's FileSystem ops; a 30s timeout bounds a slow remote walk). So rewind works for
|
|
3240
|
+
// host/e2b/k8s/ssh/adb/local-docker AND the in-process worker → advertise `rewindFiles` whenever the store is wired.
|
|
1847
3241
|
fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
|
|
1848
|
-
taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
|
|
3242
|
+
taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined, // D-1 上传/取回/删除三动词面
|
|
3243
|
+
// design/153 件3d(/decide parked 赎回腿):durable bg 行店 + boot 裸 Agent 工具,与 RunnerDeps/
|
|
3244
|
+
// scenarioDeps 同实例(claim/expire/consumeParkedFlip 作用于同一行)。任一缺席=分支不存在。
|
|
1849
3245
|
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
3246
|
+
// §0.5 session-ownership: the durable session abstraction (SessionRepo-style list/fork/delete) + the delete
|
|
3247
|
+
// coordinator. `ownerAware` is the (possibly cache-wrapped) session store; both delegate to the DB-backed
|
|
3248
|
+
// TiDB/PG store. Undefined-safe: on an in-memory dev store these carry no SessionRepo ops → the routes 501.
|
|
1850
3249
|
sessionStorage: ownerAware,
|
|
1851
3250
|
workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
|
|
1852
|
-
workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
|
|
3251
|
+
workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined, // [1402] journal 读面(GET /v1/workflows/:id/journal 数据源,与 runner 同实例)
|
|
1853
3252
|
imageIndex,
|
|
1854
3253
|
imageBakes,
|
|
1855
|
-
outcomeSink: outcomeSink ? outcomeSink : undefined,
|
|
1856
|
-
sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
|
|
3254
|
+
outcomeSink: outcomeSink ? outcomeSink : undefined, // P2: GET /v1/outcomes 读面(design/73 §7.2 仪表盘底料)
|
|
3255
|
+
sendFileLedger: sendFileLedger ? sendFileLedger : undefined, // SendUserFile 账本读面:GET /v1/sendfile-links(owner-scope 镜像 sessions list)
|
|
3256
|
+
// 2c session-sync (P1d): the WHOLE backend so the /v1/sessions/:id/sync/* routes can call exportSession/
|
|
3257
|
+
// importSession (they fan out across session entries + fileSnapshot + sessionPolicy + resumeAnchor, which must
|
|
3258
|
+
// come from ONE backend in the §8 atomic order). Undefined-safe: the env-only/no-backend worker 501s the routes.
|
|
1857
3259
|
backend: backend ? backend : undefined,
|
|
1858
3260
|
};
|
|
3261
|
+
/** 进程内活体协调面(本副本 only,重启即空:HITL 协调器/可 steer 句柄注册表/总线/收件箱) */
|
|
1859
3262
|
const coordinators = {
|
|
1860
|
-
elicitation: elicitation ? elicitation : undefined,
|
|
1861
|
-
question: question ? question : undefined,
|
|
1862
|
-
toolApproval: toolApproval ? toolApproval : undefined,
|
|
1863
|
-
workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined,
|
|
1864
|
-
subagentSteerRegistry,
|
|
1865
|
-
workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined,
|
|
3263
|
+
elicitation: elicitation ? elicitation : undefined, // E23: live-only inbound-elicitation coordinator (respond route + ALS wraps)
|
|
3264
|
+
question: question ? question : undefined, // §4④: AskUserQuestion live coordinator (respond route + ALS wraps on streaming legs)
|
|
3265
|
+
toolApproval: toolApproval ? toolApproval : undefined, // [816]/[820]②: live tool-approval coordinator (respond route + ALS wrap on the sync streaming leg)
|
|
3266
|
+
workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined, // SVC-5: steerable workflow-agent handle registry
|
|
3267
|
+
subagentSteerRegistry, // C2 (core 1.219): steerable Task-subagent handle registry (POST /v1/runs/:id/subagents/:target/steer)
|
|
3268
|
+
workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined, // P1 ①②: drain → workflow_complete frame on stream-open
|
|
1866
3269
|
fleetBus,
|
|
1867
|
-
|
|
3270
|
+
// [1196] session 级 SSE 订阅:watch registry(N 订阅共享一条探针,自适应拍距,无订阅即停)。谓词与
|
|
3271
|
+
// /head 探针同门(getLeafId+ownerOf);缺 store 面不建=路由 501+capabilities.sessionEvents=false。
|
|
3272
|
+
sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined, // [1196] 建于 purgeSession 之前(delete 即 dropSession 的同副本围栏)
|
|
1868
3273
|
hookWakeBus,
|
|
1869
|
-
sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined,
|
|
3274
|
+
sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined, // SendUserFile 切片2:file_link 帧 ALS(bg/sync 腿 wrap)
|
|
1870
3275
|
leaderEndpoint,
|
|
1871
|
-
sessionTitler: sessionTitler ? sessionTitler : undefined,
|
|
3276
|
+
sessionTitler: sessionTitler ? sessionTitler : undefined, // fire-and-forget auto-title at first submit
|
|
1872
3277
|
};
|
|
3278
|
+
/** 函数/构件缝(main 把跨子系统的动作编译成一个可调用面交给路由) */
|
|
1873
3279
|
const seams = {
|
|
1874
3280
|
sessionAudit,
|
|
1875
3281
|
purgeSession: purgeSession ? purgeSession : undefined,
|
|
3282
|
+
// [1488]③(b): bg agent-handle read face — GET /v1/runs/:id/subagents/:handle/output → the same registry
|
|
3283
|
+
// face the TaskOutput tool reads (defaultTaskRegistry is the process singleton every run leg registers
|
|
3284
|
+
// its background children into). Access tuple is derived from the run row in server.ts, never the caller.
|
|
3285
|
+
// codex R2: kind-gated BEFORE the poll (backgroundAgentOutput) — generic pollTask has side effects on
|
|
3286
|
+
// other kinds (workflow onServedTerminal ack, bash output-cursor advance).
|
|
1876
3287
|
subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
3288
|
+
// [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs over the same registry singleton
|
|
3289
|
+
// (kind gate at the seam: bash/monitor/agent pass, workflow refused — journal face owns it).
|
|
1877
3290
|
taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
1878
3291
|
taskHandleStop: (handle, access) => taskHandleStop(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
3292
|
+
// 142-S5 §1.4: GET /v1/memory/export 数据源(DB memory plane only;file 形态 route 501)。
|
|
1879
3293
|
memoryExport: memoryExportBackend ? (scope) => exportMemoryScope(memoryExportBackend, scope) : undefined,
|
|
3294
|
+
// 142-S2.5: POST /v1/memory/sync/:scope 执行位(同一 DB entry plane + sync_cursors 游标面;
|
|
3295
|
+
// plan 随数据走——core 纯函数在 performMemorySync 内直调,file 形态 route 501)。
|
|
1880
3296
|
memorySync: memoryExportBackend && memorySyncCursors
|
|
1881
3297
|
? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
|
|
1882
3298
|
: undefined,
|
|
3299
|
+
// sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
|
|
3300
|
+
// 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
|
|
1883
3301
|
sessionMirrorRuling: principalCaps
|
|
1884
3302
|
? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
|
|
1885
3303
|
: undefined,
|
|
1886
3304
|
instrumentDegenerate,
|
|
1887
|
-
sideQueryAccounting,
|
|
3305
|
+
sideQueryAccounting, // [1469] side-query 四路 sink 记账(与 tracer 同源装配)
|
|
1888
3306
|
parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
|
|
3307
|
+
// design/158 S2(F-1)修:工厂在 :2308 造好、server.ts 消费分支在等,但从未进本实参段——HIGH-1
|
|
3308
|
+
// 同族第二例(条件 spread 的键从不接线,编译期无声)。缺席后果=带 requiresParentConstraint 的
|
|
3309
|
+
// parked checkpoint 重启后恒被 core pre-CAS 门拒,**不可赎回**。接线钉:test/parked-revive-gate-wiring.test.ts。
|
|
1889
3310
|
parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
|
|
1890
3311
|
};
|
|
3312
|
+
/** 可观测面(全体 fail-open,观测失败绝不改业务面行为) */
|
|
1891
3313
|
const observability = {
|
|
1892
3314
|
logger,
|
|
1893
3315
|
metrics,
|
|
1894
|
-
modelUsage: modelUsageTracker,
|
|
1895
|
-
promptManifests: promptManifestTracker,
|
|
3316
|
+
modelUsage: modelUsageTracker, // E8: shared with createTracer above → runs drain it into the modelUsage echo
|
|
3317
|
+
promptManifests: promptManifestTracker, // [998]②: shared with createTracer → the durable legs drain prompt_assembled events
|
|
1896
3318
|
planCacheProbe,
|
|
1897
3319
|
};
|
|
3320
|
+
/** 准入/治理面(每件都是拒绝的权力;缺席 = 该道门不存在,不是「默认放行的门」) */
|
|
1898
3321
|
const governance = {
|
|
1899
3322
|
authorize,
|
|
1900
3323
|
registryJwtVerifier: registryJwtVerifier ? registryJwtVerifier : undefined,
|
|
1901
3324
|
rateLimiter,
|
|
1902
3325
|
costQuota,
|
|
1903
|
-
fleetLease: fleetLease ? fleetLease : undefined,
|
|
3326
|
+
fleetLease: fleetLease ? fleetLease : undefined, // lease admission 门(提交面,镜像 quotaExceeded)
|
|
1904
3327
|
};
|
|
3328
|
+
/** 部署自述面(/health + /v1/capabilities + 提交前置门读的部署事实;多为 live getter) */
|
|
1905
3329
|
const deployment = {
|
|
3330
|
+
// 海外 pilot 实机发现:/health 曾 verbatim 回显 sessionBackend 枚举——auto/DB_BACKEND 收编把它
|
|
3331
|
+
// coerce 成 LABEL "tidb"(泛指 durable),pg 部署上误读成 SQL-TiDB 带偏排障。回显有效 store(与 listening
|
|
3332
|
+
// 日志 session 标签同款诚实修)。
|
|
1906
3333
|
sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
|
|
1907
|
-
modelReady: () => modelReadyState.ready,
|
|
1908
|
-
scenarioDetails,
|
|
1909
|
-
workflowsCapable,
|
|
3334
|
+
modelReady: () => modelReadyState.ready, // boot ready 门:roster 未落地=计费提交 503+health ready:false
|
|
3335
|
+
scenarioDetails, // 场景详情只读面(内建+center 同判定源)
|
|
3336
|
+
workflowsCapable, // ① core split: capabilities.workflows = engine-can (workflowsCapability); workflowsList = store (below)
|
|
1910
3337
|
instanceId,
|
|
3338
|
+
// Capability discovery: the deployment facts only main knows. Store-derived booleans
|
|
3339
|
+
// (asyncRuns/artifacts/approvals/leader) are computed inside the server from the deps themselves.
|
|
3340
|
+
// pricingConfigured disambiguates costMicroUsd=0 ("free run" vs "MODEL_COST_* not configured") —
|
|
3341
|
+
// it is worker-level config, so one flag here beats decorating every result's stats.
|
|
1911
3342
|
capabilities: {
|
|
1912
3343
|
version: serviceVersion(),
|
|
1913
3344
|
scenarios: Object.keys(scenarios),
|
|
1914
3345
|
pricingConfigured: Object.values(pricing).some((p) => Object.values(p).some((v) => typeof v === "number" && v > 0)),
|
|
1915
3346
|
},
|
|
3347
|
+
// center: orchestrator polls GET /health for this — set by the sema-registry refresh loop when a
|
|
3348
|
+
// pull's restart-to-apply slices differ from boot. A live getter (not a snapshot) so /health always reads current.
|
|
1916
3349
|
restartState: () => pendingRestart,
|
|
1917
|
-
planeDeferredState: () => planeDeferredNoHandoff,
|
|
3350
|
+
planeDeferredState: () => planeDeferredNoHandoff, // codex R18: /health.modelPlaneDeferred (stuck-deferred plane, no handoff)
|
|
3351
|
+
// shared drain state — SIGTERM flips `draining`, createServer assigns `inflight`, /health mirrors it.
|
|
1918
3352
|
drainState,
|
|
1919
|
-
storeDegraded: storeBackendDegraded,
|
|
3353
|
+
storeDegraded: storeBackendDegraded, // S5: /health twin of the store_backend_degraded gauge
|
|
1920
3354
|
};
|
|
3355
|
+
/** 数值旋钮(缺省写在消费点,此处只承载覆写) */
|
|
1921
3356
|
const knobs = {
|
|
3357
|
+
// (a) SQL 默认店的 per-blob 帽 → PUT /sync/blobs 前置 413(与 SqlBlobBackend 方言默认同源:
|
|
3358
|
+
// 显式 env > tidb 的 6MiB entry-限默认;pg 默认无帽=不前置)。
|
|
3359
|
+
// 🔴 复审 2026-07-28(E-HIGH-1)修:此键是 **ServiceDeps** 字段,原先被 spread 进 `runnerDeps`
|
|
3360
|
+
// (core 的 RunnerDeps 无此字段,条件 spread 绕过 TS 多余属性检查 ⇒ 编译期无声)⇒ HTTP 面恒
|
|
3361
|
+
// undefined、413 前置是死枝、超帽仍走 502。落错家的死键病族(verify-cost 先例)——接线钉见
|
|
3362
|
+
// test/review-2026-07-28-batch1.test.ts。
|
|
1922
3363
|
snapshotBlobSqlCapBytes: backend && backend.kind !== "local" && !config.snapshotBlobStore
|
|
1923
3364
|
? (config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
|
|
1924
3365
|
: undefined,
|
|
3366
|
+
// 帽只在 watch 面在场时才有意义(缺席时路由 501,读不到它)——保持与原条件段同门,不单独接线。
|
|
1925
3367
|
sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
|
|
1926
3368
|
};
|
|
3369
|
+
// Deployment-owned mapping. Tools/prompt/skills come from the selected scenario (assembled once,
|
|
3370
|
+
// bound per request); identity/session/policy stay server-side. Never taken from the request body.
|
|
3371
|
+
// design/158 A8:从 createHttpServer 实参里提出来(原地占该字面量 1,093 行中的 967 行)——
|
|
3372
|
+
// 类型标注 `ServiceDeps["resolveSpec"]` 顶替了原来的上下文推导,签名不变。
|
|
1927
3373
|
const resolveSpec = async (body, _req, auth, opts) => {
|
|
3374
|
+
// per-principal scenario governance (assignment-as-default; an explicit
|
|
3375
|
+
// body.scenario is bounded to the allowlist — outside ⇒ typed 400 `scenario_not_allowed`). The ruling
|
|
3376
|
+
// {scenario, allowlist} is center-RESOLVED and rides the caps view (TTL-cached, same fetch as runtimeCaps);
|
|
3377
|
+
// no center / dry-run / anonymous / older center / caps blip ⇒ undefined ⇒ legacy body-or-default chain.
|
|
1928
3378
|
const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario);
|
|
3379
|
+
// per-principal execution-lane policy — this worker's lane is a
|
|
3380
|
+
// PROCESS identity (REMOTE_EXEC boot wiring, no per-task switch), so admission is the gate: lane ∉
|
|
3381
|
+
// allowedLanes ⇒ typed 403 `execution_lane_not_allowed`. Same caps car as the scenario ruling (zero extra
|
|
3382
|
+
// RTT); fail-open + audit on blip. "in-process" = fleet's REMOTE_EXEC-unset core stub lane
|
|
3383
|
+
// (named distinctly from the explicit `host` lane — same machine, different posture).
|
|
1929
3384
|
gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process");
|
|
3385
|
+
// Resolve an unknown name to "default" so the WHOLE bundle stays coherent — tools/prompt AND
|
|
3386
|
+
// skills/mcp fall back together. (selectScenario already falls back for tools/prompt; without
|
|
3387
|
+
// resolving here, skills/mcp would still be filtered by the raw unknown name → any skill/MCP tagged
|
|
3388
|
+
// ["default"] silently drops. council finding.)
|
|
1930
3389
|
const scenarioName = scenarios[requested] ? requested : "default";
|
|
3390
|
+
// [891] deprecation(一版过渡窗):"autonomous" 已改名 "code"(persona 同一,唯一差别=alias 仍
|
|
3391
|
+
// 定死 finalVerification:true 保存量行为)。warn 不 400——TB harness/存量部署平滑迁移。
|
|
1931
3392
|
if (scenarioName === "autonomous")
|
|
1932
3393
|
logger.warn("scenario_autonomous_deprecated", { alias: "code", note: "renamed by [891]; alias keeps finalVerification pinned — pass scenario:'code' (+ explicit finalVerification if wanted)" });
|
|
1933
3394
|
const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal);
|
|
3395
|
+
// codex R3/R4 (appendSystemPrompt 案): ONE center-pack snapshot per request — the append-less gate below
|
|
3396
|
+
// AND the provider pick (promptProvider field) both read THIS capture, never the mutable centerPrompts ref
|
|
3397
|
+
// again (R4 TOCTOU: a refresh between the two reads could make the final provider assembled while the gate
|
|
3398
|
+
// judged safe, restoring the in-core silent drop).
|
|
1934
3399
|
const centerDecls = centerPrompts?.declarations;
|
|
3400
|
+
// TRUE when the EFFECTIVE assembly for this request takes core's pass-through arm (no core/role.append
|
|
3401
|
+
// section ⇒ any append rider/outputStyle silently discarded in core). codex R9: PROVIDER-AWARE — mirrors
|
|
3402
|
+
// core's actual triggers (stableBlocks identity decls / stableSystem OUTPUT carrying the anchors), because
|
|
3403
|
+
// a raw body.systemPrompt anchor heuristic false-positives on default-provider scenarios (unbranded
|
|
3404
|
+
// default / scan: no provider ⇒ core defaultPromptProvider retains role.append and the rider mounts even
|
|
3405
|
+
// with an anchored systemPrompt). center-wins mirrors the provider pick below (same snapshot, same
|
|
3406
|
+
// scenario derivation — principal ruling + defaultScenario already folded into scenarioName above).
|
|
1935
3407
|
const appendLessPack = centerDecls
|
|
1936
3408
|
? centerIdentityAssembled(centerDecls, scenarioName, hasConstitutionAnchors)
|
|
1937
3409
|
: providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
|
|
3410
|
+
// Accepted rider (defensive resume mirror inside; fresh malformed/over-cap shapes already 400'd in
|
|
3411
|
+
// prepareSpec). Hoisted to a const so the R14 combined-cap check below can see the accepted length.
|
|
1938
3412
|
const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
|
|
3413
|
+
// @-model (1.24) + explicit picker: an end user may pick a CONFIGURED model via the CLI
|
|
3414
|
+
// `/model` picker / `run --model` flag (body.model) OR an inline `@name` in the objective. resolveTaskModel
|
|
3415
|
+
// gates BOTH against the catalog allow-list (config.models keys) — pick WHICH model, never inject one — with
|
|
3416
|
+
// body.model winning over @mention winning over `default`. Returns the objective with the @mention stripped.
|
|
1939
3417
|
const objective = typeof body.objective === "string" ? body.objective : "";
|
|
3418
|
+
// TaskRequest.settings (client per-request SemaSettings stamp). Parsed DEFENSIVELY off the untrusted body
|
|
3419
|
+
// (every field shape-checked; `bypassPermissions`/unknown modes dropped). `env`/`hooks` are recorded as
|
|
3420
|
+
// received-but-deferred (never silent-dropped). Folded into the spec TIGHTEN-ONLY after applyRuntimeGovernance.
|
|
1940
3421
|
const parsedSettings = parseTaskSettings(body.settings);
|
|
3422
|
+
// codex R4 (appendSystemPrompt 案) fresh-leg fail-loud: a NEW submit whose append payload (top-level rider
|
|
3423
|
+
// or settings.outputStyle) meets an append-less pack 400s HERE — inside resolveSpec, where the effective
|
|
3424
|
+
// scenario (principal ruling + defaultScenario) and the center-pack snapshot are the SAME ones the provider
|
|
3425
|
+
// pick uses (an HTTP-side gate could disagree with the actual assembly). Resume legs (opts.leg absent)
|
|
3426
|
+
// keep the drop+warn mirror below — a 4xx would brick a pre-1.243 stored task.
|
|
1941
3427
|
if (opts?.leg === "fresh" && appendLessPack) {
|
|
1942
3428
|
const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
|
|
1943
3429
|
if (riderPresent || parsedSettings.settings?.outputStyle) {
|
|
@@ -1945,8 +3431,26 @@ async function main() {
|
|
|
1945
3431
|
}
|
|
1946
3432
|
}
|
|
1947
3433
|
if (parsedSettings.deferred.length > 0) {
|
|
3434
|
+
// No-silent-drop: a caller SENT env/hooks but this version does not wire them on remote (need a core seam /
|
|
3435
|
+
// a remote hook-runner — see task-settings.ts + capabilities). Log so the gap is visible, never silent.
|
|
1948
3436
|
logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
|
|
1949
3437
|
}
|
|
3438
|
+
// [#40 / TOC cwd seam] register the caller's launch dir so the HOST factory runs the agent there (read by
|
|
3439
|
+
// ctx.sessionId). 🔒 GATED: only the single-user host lane (cwdHonored) honors it; on any other lane / multi-tenant
|
|
3440
|
+
// a sent cwd is IGNORED (logged once, never silently — the shell learns via capabilities.projectContext). isValidCwd
|
|
3441
|
+
// re-checked here too (RESUME re-runs resolveSpec WITHOUT the prepareSpec 400 validation). Re-applies on resume.
|
|
3442
|
+
// [851]P3a 信号真实化:cwdHonored 只认显式 REMOTE_EXEC=host;provider 未设的 in-process 单用户 lane 没有
|
|
3443
|
+
// shell hands(core prepare-task:无 factory/executionEnv ⇒ StubExecutionEnv,亲读 dist 坐实),但进程级
|
|
3444
|
+
// 消费者(hooks 命令、C4 projectContext 本地读取)仍看引擎自身的 cwd/env——壳把引擎 spawn 在用户目录/用户
|
|
3445
|
+
// 环境时,请求的 override 往往「已被进程状态满足」,此前每任务打 "ignored" warn 与实际状态矛盾(飞轮双平台
|
|
3446
|
+
// 观测)。改法:已满足 ⇒ debug;真失配 ⇒ 保留 warn 且带 lane 字段(值与 run_local_start exec 口径一致:
|
|
3447
|
+
// provider ?? "in-process")。真 per-session honor 之路仍是 REMOTE_EXEC=host(host factory 消费
|
|
3448
|
+
// perSessionCwd/perSessionShellEnv)。
|
|
3449
|
+
// ── D-1 附件绑定半场(②):TaskRequest.attachments = 上传句柄数组 ──
|
|
3450
|
+
// fresh 腿硬验证(未知/他人 id → 400 typed;超数 → 400)——「引用了不存在的附件」必须在提交时
|
|
3451
|
+
// 拒,不能等 env 建立时才炸。resume 腿降级(acceptedAppend 同姿):缺行 warn+跳过,余下照常
|
|
3452
|
+
// (4xx 会砖存量 task;记录不可变 ⇒ 在场行的 notice 字节恒稳)。绑定=把行钉到 auth.sessionId
|
|
3453
|
+
// (authorizer 恒解析出一个;绑定幂等,重复提交/resume 重绑同值)。
|
|
1950
3454
|
let attachmentNotice;
|
|
1951
3455
|
{
|
|
1952
3456
|
const reqAtt = body.attachmentIds;
|
|
@@ -1988,10 +3492,14 @@ async function main() {
|
|
|
1988
3492
|
else
|
|
1989
3493
|
logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
|
|
1990
3494
|
}
|
|
3495
|
+
// [R-survey / TOC shellEnv seam] register the caller's `settings.env` so the HOST factory injects it into the
|
|
3496
|
+
// agent's shell env. 🔒 GATED identically to cwd (single-user host lane only — design/107 env=capability axis);
|
|
3497
|
+
// on any cloud/multi-tenant lane a sent env is IGNORED (logged once, never silent). Re-applies on resume.
|
|
1991
3498
|
if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
|
|
1992
3499
|
if (cwdHonored(config))
|
|
1993
3500
|
setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
|
|
1994
3501
|
else if (inProcessSingleUserLane(config)) {
|
|
3502
|
+
// 只记失配键「数量」,绝不记键值(秘密类)与键名(名字本身可带敏感语义)——shellEnvMismatchCount 文档同款红线。
|
|
1995
3503
|
const requested = parsedSettings.settings.shellEnv;
|
|
1996
3504
|
const mismatched = shellEnvMismatchCount(requested, process.env);
|
|
1997
3505
|
if (mismatched === 0)
|
|
@@ -2002,6 +3510,11 @@ async function main() {
|
|
|
2002
3510
|
else
|
|
2003
3511
|
logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
|
|
2004
3512
|
}
|
|
3513
|
+
// hooks hook-runner(阶段一):settings.hooks(契约校验过)→ core TaskSpec.hooks 进程内回调。
|
|
3514
|
+
// 🔒 hook 命令跑在 WORKER HOST(CC 语义:hooks 跑在引擎所在机,哪怕工具在远端沙箱)= design/107 class ②
|
|
3515
|
+
// 能力授予 → 单用户闸 `requirePrincipal !== true`,与 scheduler/backgroundShell/lspHost/MCP 注入同一姿势;
|
|
3516
|
+
// 多租户 lane 收到只警告忽略(`task_hooks_ignored`,capabilities.taskSettings.hooks 同步 advertise),绝不执行。
|
|
3517
|
+
// Re-applies on resume(从 stored body 重解析,幂等)。
|
|
2005
3518
|
const taskHooks = (() => {
|
|
2006
3519
|
const hc = parsedSettings.settings?.hooks;
|
|
2007
3520
|
if (!hc)
|
|
@@ -2010,55 +3523,122 @@ async function main() {
|
|
|
2010
3523
|
logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
|
|
2011
3524
|
return undefined;
|
|
2012
3525
|
}
|
|
3526
|
+
// payload 的 permission_mode 用【生效】模式——body.permissionMode(显式请求)优先于
|
|
3527
|
+
// settings.permissions.defaultMode(与 spec 的 withPermissionMode 同一优先序),否则钩子读到的模式与
|
|
3528
|
+
// 实际裁决模式不一致(如 body=plan 时钩子仍看到 default)。
|
|
2013
3529
|
const effMode = coercePermissionMode(body.permissionMode) ?? parsedSettings.settings?.permissions?.defaultMode;
|
|
2014
3530
|
return createTaskHooks(hc, {
|
|
2015
3531
|
logger,
|
|
2016
3532
|
sessionId: auth?.sessionId ?? "",
|
|
2017
3533
|
cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
|
|
2018
3534
|
...(effMode ? { permissionMode: effMode } : {}),
|
|
3535
|
+
// 用户变量通道(hook 进程 env + http $NAME 插值源):同请求的 settings.env——hookEnv 绝不读
|
|
3536
|
+
// 裸 process.env 的非 allowlist 键(服务密钥不可被 allowedEnvVars 点名)。
|
|
2019
3537
|
...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
|
|
3538
|
+
// 阶段三b:prompt/agent 条目的模型载体(boot 组装,见 hookLlm/hookAgent 定义处的影响范围说明)。
|
|
2020
3539
|
hookLlm,
|
|
2021
3540
|
hookAgent,
|
|
3541
|
+
// cli [1786] 点名的观测位:一次 hook 判定**未能完成**时,往常开的 fleet 流推一帧。
|
|
3542
|
+
// ⚠️ **纯 observe**(不拦、不续跑、不注入)——`additionalContext` 那条通道会让这一轮不结束,
|
|
3543
|
+
// 所以不能拿它当"提示一下"用(见 hook-runner 折叠处旁注)。
|
|
3544
|
+
// ⚠️ 带 ownerScope/ownerSessionId 供流侧做 **fail-CLOSED** 过滤(与 bg_notification 同一姿势):
|
|
3545
|
+
// 一条关于别人会话的通知落到这个壳上,会让用户以为是自己这轮出了问题。
|
|
2022
3546
|
onHookNotice: (n) => fleetBus.publishHookNotice({
|
|
2023
3547
|
...n,
|
|
2024
3548
|
...(auth?.principal ? { ownerScope: auth.principal } : {}),
|
|
2025
3549
|
...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
|
|
2026
3550
|
}),
|
|
3551
|
+
// asyncRewake wake 管道(契约:exit 2 唤醒模型):session→本副本活流 steer(hookWakeBus,server
|
|
3552
|
+
// 启动装 deliver)。stderr 过 redactSecrets 再注入(hook 进程可能回显 env 密钥;与 trace 出口同纪律)。
|
|
3553
|
+
// 无 session(匿名一次性任务)不给 wake——没有可稳定寻址的流。
|
|
2027
3554
|
...(auth?.sessionId
|
|
2028
3555
|
? {
|
|
2029
3556
|
wake: async (text) => hookWakeBus.deliver
|
|
2030
|
-
?
|
|
3557
|
+
? // provenance 围栏:注入内容标明来源=asyncRewake hook 的运行时输出,
|
|
3558
|
+
// 模型按 hook 反馈对待而非操作者指令(trusted:false 在 server 侧同批落定)。
|
|
2031
3559
|
hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
|
|
2032
3560
|
: false,
|
|
2033
3561
|
}
|
|
2034
3562
|
: {}),
|
|
2035
3563
|
});
|
|
2036
3564
|
})();
|
|
3565
|
+
// design/119 (CC --add-dir parity, core 1.215 `TaskSpec.additionalDirectories`): extra host dirs the FILE tools
|
|
3566
|
+
// may access beyond the containment root. 🔒 GATED identically to cwd/shellEnv (single-user host lane only —
|
|
3567
|
+
// these are the caller's own-machine paths; a cloud/multi-tenant lane must not let a caller widen the file-tool
|
|
3568
|
+
// containment onto operator/other-tenant host paths). Unlike cwd/shellEnv (registered per-session for the host
|
|
3569
|
+
// FACTORY to read at env construction), additionalDirectories is a plain TaskSpec field core reads directly, so
|
|
3570
|
+
// it rides onto the spec below. Ignored (logged, never silent) off the host lane. Re-applies on resume (from body).
|
|
2037
3571
|
const rawAddDirs = body.additionalDirectories;
|
|
2038
3572
|
const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
|
|
2039
3573
|
if (rawAddDirs !== undefined && !cwdHonored(config)) {
|
|
2040
3574
|
logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
|
|
2041
3575
|
}
|
|
3576
|
+
// body.model (explicit /model picker or run --model) WINS over settings.model (the config default); both gate
|
|
3577
|
+
// against the catalog allow-list via resolveTaskModel (a settings.model not in the catalog is ignored, never injected).
|
|
3578
|
+
// 🔴 TOB 档位联跑曾 FAIL(坐标级):the gate must see the TIER-EXPANDED catalog — expandTiers
|
|
3579
|
+
// runs inside core's Runner on a PRIVATE deps copy (runtask.ts), so gating on bare config.models silently
|
|
3580
|
+
// degraded every tier word (pro/flash/@mention/CC alias) to default BEFORE the Runner could resolve it.
|
|
3581
|
+
// Expand per request (cheap: empty tiers returns the same reference; non-empty = one small spread) so the
|
|
3582
|
+
// gate's key set is exactly what a freshly-constructed Runner resolves — tier words, degrade chains, CC
|
|
3583
|
+
// aliases, catalog-SHADOW all from core's ONE implementation. Window note: tiers are restart-to-apply at
|
|
3584
|
+
// the Runner; a refresh-time tier change makes this gate briefly AHEAD of the old Runner snapshot (a new
|
|
3585
|
+
// tier word then fails loud in core instead of silently degrading — honest during the restart window).
|
|
2042
3586
|
const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
|
|
2043
3587
|
const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog);
|
|
3588
|
+
// [865]② 降级永远可见:fresh submit 的未知 body.model 已在 HTTP 门 400(不会到这);走到这的未知 ref
|
|
3589
|
+
// 只剩 RESUME 重放(模型事后被移出目录)与 settings.model(lenient 文档面)——落 default 可跑,但必须
|
|
3590
|
+
// 有声(session 中途换模型是最恶性的上下文污染路径,静默=病灶本体)。
|
|
2044
3591
|
if (picked.unknownExplicit !== undefined) {
|
|
2045
3592
|
metrics.inc("task_model_unknown_fallback_total");
|
|
2046
3593
|
logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
|
|
2047
3594
|
}
|
|
3595
|
+
// no-vision images = PLACEHOLDER, not 422(会话可继续的 CC 心智). The old
|
|
3596
|
+
// fail-loud 422 here BLOCKED core's own degradation: since core 1.233.1 a serving model whose `input`
|
|
3597
|
+
// has no "image" gets image parts degraded to a bounded text placeholder ("[image omitted: …]"), per
|
|
3598
|
+
// CURRENT model (degrade chains stay correct) and re-applied on every history replay — so a previously
|
|
3599
|
+
// poisoned session self-heals. We pin ≥1.240.1, so pass the images through and let core do exactly that.
|
|
3600
|
+
// The GET /v1/models vision flag stays advertised (the shell's composer-warning half). A model
|
|
3601
|
+
// that never declared `input` keeps sending images verbatim (deliberate default — never wrongly strip
|
|
3602
|
+
// for a vision-capable deployment that skipped the metadata; same posture).
|
|
3603
|
+
// S6 (SILENT-FALLBACK P0-c): images headed for an EXPLICITLY text-only model — core (>=1.233.1) degrades
|
|
3604
|
+
// them to bounded text placeholders per current model (the pass-through posture above). That modality drop
|
|
3605
|
+
// was invisible fleet-side; count + warn at submit. modelSupportsImages is false-safe (undeclared input →
|
|
3606
|
+
// true → no signal), matching core's "never wrongly strip" posture — no false positives for verbatim
|
|
3607
|
+
// pass-through models. ⚠️ sema-registry lane caveat (review LOW-2): the roster maps `vision?: boolean` to
|
|
3608
|
+
// an EXPLICIT input list (sema-registry.ts) — a vision-capable roster model that FORGOT the flag becomes
|
|
3609
|
+
// declared text-only, so this signal fires AND core really does degrade its images (signal matches
|
|
3610
|
+
// behavior; the fix for that footgun is the roster entry, not this counter). The per-run durable event rides core's vision.placeholder trace kind once we
|
|
3611
|
+
// consume >=1.245 (the authoritative replacement-time signal), not this submit-time predictor.
|
|
2048
3612
|
if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
|
|
2049
3613
|
metrics.inc("images_omitted_total", { model: picked.model });
|
|
2050
3614
|
logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
|
|
2051
3615
|
}
|
|
3616
|
+
// E18 resume-at: the shell sends body.resumeAt as the E2 message eventId; core's TaskSpec.resumeAt takes the
|
|
3617
|
+
// PERSISTED SessionTreeEntry.id, NOT the eventId ("a deployment that holds eventIds owns the eventId→entryId
|
|
3618
|
+
// map" — core never persists eventIds). Resolve eventId→entryId via the anchor store HERE, 4xx-ing BEFORE the
|
|
3619
|
+
// run on an UNKNOWN anchor (never hand core an unresolvable id). A KNOWN anchor whose entry was later compacted
|
|
3620
|
+
// away surfaces as a failed run RESULT (resume_at.not_found) — 4xx on the sync path, the run result on async (the
|
|
3621
|
+
// async contract is poll-the-result). RESUME re-runs resolveSpec from the stored body — the resume paths strip
|
|
3622
|
+
// resumeAt before re-resolving (else core would reject resumeAt + a durable resume), so this branch only fires on
|
|
3623
|
+
// a fresh submit. NEVER silent-drop (mirrors the body.model silent-drop bug class).
|
|
2052
3624
|
let resumeAtEntryId;
|
|
2053
3625
|
if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
|
|
2054
3626
|
if (!auth?.sessionId)
|
|
2055
3627
|
throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
|
|
3628
|
+
// 501 gated on the SAME pair the `resumeAt` capability advertises (anchor store AND getLeafId) — capture is a
|
|
3629
|
+
// no-op without getLeafId, so a getLeafId-less backend has a permanently-empty map; report "not available"
|
|
3630
|
+
// (honest 501) rather than a misleading 404. Keeps "capability says yes ⟺ the route resolves" by construction.
|
|
2056
3631
|
if (!resumeAnchorStore || !ownerAware.getLeafId)
|
|
2057
3632
|
throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
|
|
2058
3633
|
resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
|
|
2059
3634
|
if (resumeAtEntryId === undefined)
|
|
2060
3635
|
throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
|
|
2061
3636
|
}
|
|
3637
|
+
// R8 code-only rewind (CC "Restore code" mode, core 1.166 `rewindFilesTo`): restore the working tree to a prior
|
|
3638
|
+
// USER message's snapshot WITHOUT forking the conversation. Resolve the SAME handle (the prompt's taskId) → entryId
|
|
3639
|
+
// via the SAME anchor store. Honored ONLY when resumeAt is ABSENT (core's contract: the two are mutually exclusive —
|
|
3640
|
+
// with resumeAt set, that branch's own rewindFiles governs); so we only resolve it on the code-only path. Same
|
|
3641
|
+
// fail-loud discipline as resumeAt (501 no-store / 404 unknown anchor — never silent-drop).
|
|
2062
3642
|
let rewindFilesToEntryId;
|
|
2063
3643
|
if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
|
|
2064
3644
|
if (!auth?.sessionId)
|
|
@@ -2069,51 +3649,204 @@ async function main() {
|
|
|
2069
3649
|
if (rewindFilesToEntryId === undefined)
|
|
2070
3650
|
throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
|
|
2071
3651
|
}
|
|
3652
|
+
// 142-S4 defaultScopes 种子:projectId → center 项目登记簿(config.projects,registry 喂)→ 该项目
|
|
3653
|
+
// 声明的默认记忆 scope 词表(memory.scopes 额外 READ 层;写路由仍钉在派生 scope — memorySpecForRequest)。
|
|
3654
|
+
// projectId 优先取 auth.resolvedProjectId(authorizer 的 PROJECT_ID_REGEX 422 门通过后回传);RESUME 腿
|
|
3655
|
+
// 的 auth 从 checkpoint 重建、无此字段 → 回落持久化 body.projectId(原始提交已过形状门,与 memoryWrite
|
|
3656
|
+
// 的 rides-the-persisted-body 姿势一致),让种子在 resume 腿等价重放。查表 miss(未登记/已下架)= 无种子,
|
|
3657
|
+
// 不 fail(登记簿是加性配置面,不是准入门)。
|
|
2072
3658
|
const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
|
|
2073
3659
|
const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
|
|
2074
3660
|
const spec = {
|
|
3661
|
+
// D-1:附件告知随 objective 进 durable 流(只有名字/mime/尺寸——**内容永不进流**,这正是
|
|
3662
|
+
// 「拼进 objective 是伪方案」的账要划清的线;文件名已消毒为安全字符集,无注入面)。
|
|
2075
3663
|
objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
|
|
3664
|
+
// Decoupling seam: a trusted client (token-holder) injects its OWN business/UX system prompt
|
|
3665
|
+
// (persona, UI markers like ::ISSUE_FORM::, workflow rules). The service stays business-agnostic
|
|
3666
|
+
// — integrators evolve their UX here, not by changing scenario prompts in this repo. core's
|
|
3667
|
+
// stableSystem appends it after the scenario base, before <user_memory>.
|
|
2076
3668
|
systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
|
|
3669
|
+
// [1476] R1 / [1478] R2: top-level append rider → core TaskSpec.appendSystemPrompt (stable block after the
|
|
3670
|
+
// scenario base + systemPrompt, before the volatile tail). settings.outputStyle composes AFTER this via
|
|
3671
|
+
// applyTaskSettings (`append\n\nstyle`) — byte-stable order: knowledge block always first, style always
|
|
3672
|
+
// second (pinned by test; order flapping would shred the prefix cache). Resume re-enters here off the
|
|
3673
|
+
// persisted body, so the rider re-applies on resumed legs like systemPrompt does — DEFENSIVELY (codex F2):
|
|
3674
|
+
// the resume families skip prepareSpec's 400 gate and a pre-1.243 store kept unknown keys verbatim, so an
|
|
3675
|
+
// over-cap legacy value is dropped+warned here instead of becoming active prompt content.
|
|
2077
3676
|
appendSystemPrompt: acceptedAppend,
|
|
3677
|
+
// sessionId comes from `auth` (ownership-checked / principal-derived), NEVER the body — the
|
|
3678
|
+
// authorizer always resolves one. (No `?? body.sessionId` fallback: that would re-open the
|
|
3679
|
+
// body-as-capability hole S6 closed if a deployment ran without an authorizer.)
|
|
2078
3680
|
sessionId: auth?.sessionId,
|
|
3681
|
+
// The authenticated end-user principal (design/62, core 1.78). Read-only on the TaskSpec — core injects
|
|
3682
|
+
// it into a consumer MCP's `principalHeader` per-task so the MCP can enforce per-user RBAC (oa-mcp). It
|
|
3683
|
+
// comes from the auth channel (never the body/model), so the model can't substitute another identity.
|
|
2079
3684
|
principal: auth?.principal,
|
|
2080
3685
|
images: body.images,
|
|
3686
|
+
// design/112 C1: pass the client/gateway-supplied user context through to core (localizes the env block's date
|
|
3687
|
+
// to the user's zone + surfaces who the agent acts for). HTTP-validated + capped at submit (prepareSpec); on a
|
|
3688
|
+
// resume the body was already validated at its original submit. core re-validates timeZone (invalid → UTC) and
|
|
3689
|
+
// SANITIZES userEmail (inlineUntrusted: folds newlines, neutralizes <system-reminder>, caps 160cp — no injection;
|
|
3690
|
+
// double-review-confirmed). PICK only the two fields core reads — don't passthrough stray keys (a caller could
|
|
3691
|
+
// otherwise bloat the persisted run body with an uncapped junk field; core ignores them but the body stays tight).
|
|
2081
3692
|
...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
|
|
3693
|
+
// Structured output (CC --json-schema): a JSON Schema constraining the model's FINAL
|
|
3694
|
+
// answer. core injects a built-in `submit_output` tool from it + surfaces the validated object as
|
|
3695
|
+
// TaskResult.structuredOutput. HTTP layer shape+size-validated it (prepareSpec); core does deep validation.
|
|
3696
|
+
// RESUME re-runs resolveSpec ⇒ the structured-output constraint re-applies on resumed legs too.
|
|
3697
|
+
// 历史复审轴B #5(1.254):compactionModel 已接([1479]①)而 design/145 配套旋钮 clampTolerance
|
|
3698
|
+
// 够不着=不对称。窄收单键(0..1 数值;其余 compaction 键仍是操作方轴,有意不开)。
|
|
2082
3699
|
...(() => {
|
|
2083
3700
|
const ct = body.compaction?.clampTolerance;
|
|
2084
3701
|
return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
|
|
2085
3702
|
})(),
|
|
2086
3703
|
outputSchema: body.outputSchema,
|
|
3704
|
+
// 历史复审轴A #2(1.254):outputRetries 与 outputSchema 是成对的 caller 面旋钮(retry-on-invalid
|
|
3705
|
+
// 轮数),此前只接了 schema 半边。窄校验:1..10 整数,schema 在场才有意义(core 无 schema 时忽略)。
|
|
2087
3706
|
...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
|
|
2088
3707
|
? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
|
|
2089
3708
|
: {}),
|
|
3709
|
+
// E18 resume-at: the resolved persistent SessionTreeEntry.id (from the eventId the shell sent). core branches
|
|
3710
|
+
// the session at this prior entry before running the new objective ("rewind to this message, ask differently").
|
|
2090
3711
|
resumeAt: resumeAtEntryId,
|
|
3712
|
+
// [833] rewind exclusive mode (core 1.292): "before" branches at the target's PARENT (excludes the target —
|
|
3713
|
+
// "remove this prompt and everything after it"); absent/"at" = core default, zero regression. Defensive
|
|
3714
|
+
// normalize (RESUME re-enters resolveSpec without the HTTP 400 gate): only the two enum values ride, and only
|
|
3715
|
+
// WITH a resolved resumeAt (core ignores the field otherwise; keep the persisted spec tight). core's "before"
|
|
3716
|
+
// edge rejections (resume_at.before_target_not_user / resume_at.before_root_unsupported /
|
|
3717
|
+
// rewind_snapshot.unresolvable) surface as failed-result errorCodes mapped to 4xx by resumeAtHttpStatus,
|
|
3718
|
+
// errorCode passed through UNCHANGED for the shell to render.
|
|
2091
3719
|
resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
|
|
3720
|
+
// design/114 Phase3 (reuse-path warm-resume): require the session to already exist — core's acquire fails loud
|
|
3721
|
+
// (`resume.session_not_found`) on a genuinely-missing/purged session instead of silently starting a fresh one.
|
|
3722
|
+
// The service authorizer ALSO stops pre-registering a missing id under this flag (security.ts), so the store's
|
|
3723
|
+
// fail-loud actually fires (else the authorizer's claim-create would materialize the session first). Rides onto
|
|
3724
|
+
// resume legs (resolveSpec re-runs); a resumed session exists, so it's satisfied there.
|
|
2092
3725
|
requireExistingSession: body.requireExistingSession === true ? true : undefined,
|
|
3726
|
+
// §4 (Fork): CC `/fork` — core 1.257 (design/136 BREAKING) retired the standalone Fork tool; a fork is now
|
|
3727
|
+
// Agent(subagent_type:"fork") gated by `enableFork`. 🔴 core FLIPPED enableFork to opt-OUT (undefined = available),
|
|
3728
|
+
// so enableForkFromBody now returns an EXPLICIT boolean (never undefined) — false where the service denies, else
|
|
3729
|
+
// fail-OPEN. DEFAULT ON single-user turnkey (clay 2026-07-01); per-task opt-OUT via body.enableFork:false;
|
|
3730
|
+
// multi-tenant honored ONLY with a per-principal entitlement resolver wired (core enforces `allowFork`), else
|
|
3731
|
+
// explicit false (fail-closed). core still requires a fork-capable store (`hasSessionFork`; TOC file backend
|
|
3732
|
+
// satisfies it via LocalSessionStore.fork), else inert.
|
|
3733
|
+
// NB: `centerRuntimeCapsResolver`(非合成后的 runtimeCapsResolver)—— env observer 基线不是
|
|
3734
|
+
// center 背书的 entitlement 源,多租户 fail-close 判别只认 center caps client。
|
|
2093
3735
|
enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
|
|
3736
|
+
// E12 (shell-host contract): opt-in post-completion prompt suggestions. core runs ONE extra fire-and-forget LLM pass
|
|
3737
|
+
// after the task completes; the service surfaces the strings (redacted, UNTRUSTED UI-only) as a `suggestions`
|
|
3738
|
+
// event. Defensive normalize (RESUME re-runs resolveSpec without the HTTP 400 validation). OFF ⇒ zero extra LLM.
|
|
2094
3739
|
suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
|
|
3740
|
+
// E19 (shell-host contract): opt-in working-tree rewind. core snapshots each completed turn + restores the files on a
|
|
3741
|
+
// resumeAt branch, for any env when fileSnapshotStore is wired (gate-split 1.134.0). OFF ⇒ no snapshot.
|
|
2095
3742
|
rewindFiles: body.rewindFiles === true ? true : undefined,
|
|
3743
|
+
// R8 code-only (core 1.166): the resolved target entry id for "Restore code" — core restores the working tree to
|
|
3744
|
+
// its snapshot WITHOUT branching the session. Mutually exclusive with resumeAt (only set on the code-only path).
|
|
2096
3745
|
rewindFilesTo: rewindFilesToEntryId,
|
|
3746
|
+
// design/119 (CC --add-dir): extra host dirs the FILE tools may access (core canonicalizes each into the
|
|
3747
|
+
// containment allowlist + lists them in the `# Environment` block). Single-user host lane only (gated above).
|
|
2097
3748
|
additionalDirectories,
|
|
3749
|
+
// EnterPlanMode (core 1.167): MODEL-DRIVEN plan — `enablePlanMode:true` alone (writable
|
|
3750
|
+
// start, NO handsReadOnly) auto-mounts `enter_plan_mode`/`present_plan` so the model can self-enter read-only
|
|
3751
|
+
// plan AT ANY TIME it judges a task needs planning. 🆕 DEFAULT ON (clay: resident; PLAN_MODE_ENABLED=false opts
|
|
3752
|
+
// out). USER-driven plan (permissionMode=plan) ADDS handsReadOnly on top via applyTaskSettings (which also sets
|
|
3753
|
+
// enablePlanMode, idempotent). Additive — widens nothing; the model self-selects when to use it.
|
|
2098
3754
|
enablePlanMode: config.planModeEnabled ? true : undefined,
|
|
3755
|
+
// Workflow super-set unlock (same shape as the rank-1 roster gap): per-task activation of the
|
|
3756
|
+
// LLM-authored workflow engine (core `run_workflow` + the workflow.ts agent/parallel/pipeline orchestration).
|
|
3757
|
+
// The engine is BUILT but the HTTP API never mapped a body field to `spec.selfOrchestration`, so run_workflow
|
|
3758
|
+
// was per-task unreachable (capabilities advertised `workflows:true` but the LLM could never call it). Gated on
|
|
3759
|
+
// the DEPLOYMENT enabling it (`selfOrchestrationEnabled` → workflowScriptRunner/stores wired; else a set flag is
|
|
3760
|
+
// a harmless no-op core fail-closes). 🔒 Multi-tenant entitlement is NOT blanket-closed here (unlike mcp, which
|
|
3761
|
+
// has no engine entitlement) — core's `runtimeCapsResolver` enforces `allowWorkflows` (the third-stage
|
|
3762
|
+
// per-principal cap) ON THE ENGINE, so a multi-tenant principal WITHOUT the entitlement gets run_workflow
|
|
3763
|
+
// fail-closed by core (no resolver ⇒ fail-closed). Single-user honors directly. Three-gate: engine-can ∧
|
|
3764
|
+
// center-may(allowWorkflows) ∧ shell-show. (Logic + gates documented in `selfOrchestrationFromBody`.) 🔒 The
|
|
3765
|
+
// multi-tenant floor: pass whether an entitlement RESOLVER is wired — core's allowWorkflows is TIGHTEN-ONLY
|
|
3766
|
+
// (no resolver ⇒ fail-OPEN per-principal), so the helper fail-closes multi-tenant when none is present.
|
|
3767
|
+
// L2 ultracode (design/111): OR the preset's selfOrchestration:true into the body intent, passed THROUGH the
|
|
3768
|
+
// existing gate — it grants nothing a raw body.selfOrchestration:true couldn't (multi-tenant still fail-closes
|
|
3769
|
+
// without an entitlement resolver). The preset never bypasses the闸.
|
|
2099
3770
|
selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
|
|
3771
|
+
// C1 (core 1.219, subagent viewing pane): opt-in widening of the forward sink from
|
|
3772
|
+
// task_progress-only to a delegated child's live CONTENT events (text_delta/reasoning_delta/tool_start/
|
|
3773
|
+
// tool_end, each carrying `parentToolCallId` attribution). Purely a RENDER channel — core never merges the
|
|
3774
|
+
// child stream into the parent's model context. The service's forward-sink consumers redact per §E1 (shared
|
|
3775
|
+
// builders); default OFF = prior progress-only behavior. Strict `=== true` (never a truthy coercion).
|
|
2100
3776
|
forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
|
|
3777
|
+
// design/122 (core 1.225): opt-in retention of SETTLED sub-agent sessions so the operator/
|
|
3778
|
+
// shell can REVIVE them (POST /v1/runs/:id/subagents/:target/resume). Defensive normalize + clamp
|
|
3779
|
+
// (RESUME re-runs resolveSpec without HTTP validation): boolean passes; an object's ttlMs/max are clamped
|
|
3780
|
+
// to sane ceilings (retention pins live sessions in memory — a caller must not turn the knob into a
|
|
3781
|
+
// resource hold; core's own defaults are 30min/16, the run-scoped ledger releases everything at parent
|
|
3782
|
+
// end either way). Malformed values fall to undefined = OFF (core default), never a throw.
|
|
2101
3783
|
retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
|
|
3784
|
+
// [876] per-task custom subagents (core 1.295 TaskSpec.agents — CC `.claude/agents/` parity): the shell's
|
|
3785
|
+
// resolved AgentDefinition[] rides the spec verbatim (model already a real name/Model object — no alias
|
|
3786
|
+
// translation here). 🔒 TOB tenant gate + defensive per-item normalize live in taskAgentsFromBody
|
|
3787
|
+
// (spec-fields.ts): multi-tenant is fail-closed (warn task_agents_ignored — no per-tenant caps face yet,
|
|
3788
|
+
// capabilities.taskAgents advertises false); RESUME replays skip the HTTP whitelist 400 → illegal items
|
|
3789
|
+
// DROP+warn per entry (task_agent_dropped), never a bricked resume. Absent/empty ⇒ no key (byte-compat).
|
|
3790
|
+
// [1.211 codex L] 内联闭包换 spec-fields 真出口(taskAgentsSpecFragment)——测试直接 import 调真
|
|
3791
|
+
// fold,SOURCE PIN 收敛为「resolveSpec 内恰一次调用点」单锚(文本重写同构表达式的假绿面就此关)。
|
|
2102
3792
|
...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
|
|
3793
|
+
// [854]② (core 1.295): opt-out of task-end session-background reaping ("keep my dev server running").
|
|
3794
|
+
// Resource-residency grant on the host lane → single-user gate like backgroundShell/hooks (multi-tenant
|
|
3795
|
+
// warns task_retain_bg_ignored + ignores; capability advertises false). Only literal `true` rides (false =
|
|
3796
|
+
// core default, key omitted); non-boolean is 400 at submit, defensively dropped on a resume replay.
|
|
2103
3797
|
...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
|
|
2104
3798
|
? { retainBackgroundProcesses: true }
|
|
2105
3799
|
: {}),
|
|
3800
|
+
// [1052]② (core 1.314): per-run 工具面收窄两位——excludeTools=roster 真卸载,deferTools=延迟披露
|
|
3801
|
+
// (cli「Workflow 默认开不暴露」承载位)。tighten-only(core 继承 union 不变量),无租户门;defensive
|
|
3802
|
+
// normalize(resume 重放),提交面 400 在 HTTP 门。
|
|
2106
3803
|
...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
|
|
2107
3804
|
...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
|
|
3805
|
+
// [1144]/[1146] (core 1.328 R2): promptProfile 双形轴直通——纯呈现轴无租户门(deferTools 姿势);
|
|
3806
|
+
// 缺省不挂键(引擎缺省 simple);非法值由 HTTP 门 400,resume 重放 defensive DROP。
|
|
2108
3807
|
...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
|
|
3808
|
+
// [922]①/[915]② (core 1.296): interactiveTools 三态旋钮直通——boolean 上 spec(壳 -p 恒 stamp false=
|
|
3809
|
+
// A3 类「headless 提问 park」根治腿;plan 显式 true 恒赢),undefined=core 自动判据(送达面探测)。
|
|
3810
|
+
// per-run 工具面选择无跨租户面 ⇒ 无租户门;非 boolean 由 HTTP 门 400,resume 重放 typeof 检查天然 DROP。
|
|
2109
3811
|
...(typeof body.interactiveTools === "boolean"
|
|
2110
3812
|
? { interactiveTools: body.interactiveTools }
|
|
2111
3813
|
: {}),
|
|
3814
|
+
// design/131 (core 1.246): per-task resilience INTENT flags. allowDegrade/allowFailover are
|
|
3815
|
+
// caller-facing (a bench/eval run wants true failure shapes); bypassBreaker is operator-only (normalizer
|
|
3816
|
+
// drops it for non-operators — it punches through a SHARED breaker). All-absent = byte-compat.
|
|
2112
3817
|
resilience: normalizeResilience(body.resilience, explicitOperator(auth?.principal, config.operatorPrincipals)),
|
|
3818
|
+
// design/132 (core 1.249): one-shot end-game verification nudge ("re-run the final artifact through
|
|
3819
|
+
// its real entrypoint before finishing"). OPT-IN by core's own judgment (default ON lost the evidence case:
|
|
3820
|
+
// +1 turn on every interactive write task) — the AUTONOMY caller declares it (harness/scheduler lanes).
|
|
3821
|
+
// [849] scenario-declared leg: the `autonomous` scenario pins it in its bundle (cap.finalVerification —
|
|
3822
|
+
// picking that scenario IS the autonomy declaration, so this is not "inferring from scenario"); OR-folded
|
|
3823
|
+
// with the caller's explicit flag — a scenario can only ADD the closing verification turn, never strip a
|
|
3824
|
+
// caller's request. Children do not inherit (core semantics).
|
|
2113
3825
|
finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
|
|
3826
|
+
// design/133 (core 1.251): turn-boundary attachment reminders (todo/changed-files/plan-mode),
|
|
3827
|
+
// literal-true unions — the normalizer drops `false`/garbage keys instead of forwarding them (core contract:
|
|
3828
|
+
// "off = delete the key, never send false"). Resume-safe by construction: the persisted body is full JSON
|
|
3829
|
+
// and this is the single translation point, so this line IS the whitelist inclusion.
|
|
2114
3830
|
attachments: normalizeAttachments(body.attachments),
|
|
3831
|
+
// hooks(阶段一,gate 见上面 taskHooks 装配):core 是整槽覆盖(`spec.hooks ?? deps.hooks`,
|
|
3832
|
+
// runtask gateBaseline 注释点名),task hooks 一挂会 shadow deps.hooks 上的 TOOL_TRACE 观测 → composeHooks
|
|
3833
|
+
// 把部署基线折进来(部署槽先跑=观测看到真实执行)。无 task hooks 时不挂字段,deps 路径原样。
|
|
2115
3834
|
...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
|
|
2116
3835
|
model: picked.model,
|
|
3836
|
+
// [1479]① compactionModel (design/145 cheap compaction gear): catalog-gated to the SAME expanded view as
|
|
3837
|
+
// body.model (canonical name form). resolveSpec is the AUTHORITY (codex R5 — the prepareSpec 400 is only
|
|
3838
|
+
// a fast-fail UX layer; the catalog hot-refreshes in place, so a ref valid at the HTTP gate can be gone
|
|
3839
|
+
// after the authorize/resolve awaits): unknown on a FRESH leg → 400 fail-loud (the documented guarantee —
|
|
3840
|
+
// never a silent price change); unknown on a RESUME replay → drop + warn (core's resolveModel THROWS on
|
|
3841
|
+
// an unknown compactionModel, which would brick the resume; the fallback is the summarize role / main
|
|
3842
|
+
// model, the pre-field behavior).
|
|
3843
|
+
// Window note (codex R6, requalified): the resolveSpec→Runner handoff shares spec.model's DOCUMENTED
|
|
3844
|
+
// restart-window class ([865] / the wireCatalog note above) — the Runner resolves the canonical string
|
|
3845
|
+
// against ITS OWN constructor-time expanded snapshot, so a hot refresh between here and prepare can
|
|
3846
|
+
// desync the two views. Inside that window the failure mode is core's resolveModel TYPED THROW
|
|
3847
|
+
// ("Unknown model ref", roles.js dist-read) = a fail-loud task error, never a silent price change —
|
|
3848
|
+
// the same honest posture the sibling field ships with. Binding a Model OBJECT here instead would
|
|
3849
|
+
// diverge from spec.model's string-intent contract and freeze catalog bytes into the persisted body.
|
|
2117
3850
|
...((() => {
|
|
2118
3851
|
if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
|
|
2119
3852
|
return {};
|
|
@@ -2128,10 +3861,28 @@ async function main() {
|
|
|
2128
3861
|
}
|
|
2129
3862
|
return { compactionModel: cm };
|
|
2130
3863
|
})()),
|
|
3864
|
+
// E7 (shell-host contract): reasoning-effort selection threaded to core's ThinkingLevel. Defensive
|
|
3865
|
+
// isThinkingLevel guard — RESUME re-runs resolveSpec WITHOUT the HTTP-layer 400 validation, so an invalid/
|
|
3866
|
+
// absent value falls through to the resolved role's default thinking (core's RoleSpec.thinking), never throws.
|
|
3867
|
+
// L2 ultracode (design/111): the preset FLOORS thinking at the ultra tier (= max, via core resolveReasoningProfile);
|
|
3868
|
+
// awareness rides for free (core composes it from thinking∈{xhigh,max}). Else the explicit reasoningEffort.
|
|
2131
3869
|
thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
|
|
3870
|
+
// Per-model auth (sema-registry apiKeyEnv): core calls this per brain call / cascade rung so each
|
|
3871
|
+
// model uses its own upstream key; a model without one falls back to the gateway key. undefined
|
|
3872
|
+
// when no per-model keys are configured → unchanged single-key behavior.
|
|
2132
3873
|
getApiKeyAndHeaders: keyResolver,
|
|
3874
|
+
// [854]④ per-request 配速:body.limits.{timeoutSec,maxOutputTokens,maxTurns} 现在被收下(核对上游
|
|
3875
|
+
// TB2.0 实测诉求;旧姿势「body limits 一律忽略」作废)。合成规则在 resolveTaskLimits(spec-fields.ts):
|
|
3876
|
+
// - body.timeoutSec 给了就用 body(caller 显式配速,可低于内建墙;已被可选 TASK_TIMEOUT_MAX_SEC 封顶);
|
|
3877
|
+
// - body 缺席保持既有姿势 = tenancy 墙钟(单用户 turnkey 无墙 / 多租 2400s、大任务 3600s,
|
|
3878
|
+
// clay 2026-07-04 make-real 教训①)+ env TASK_TIMEOUT_SEC 只抬不降(taskWallClockSec 内 Math.max);
|
|
3879
|
+
// - maxOutputTokens/maxTurns 直透传(可选 TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX 同款封顶)。
|
|
3880
|
+
// resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw)。
|
|
2133
3881
|
...(() => {
|
|
2134
3882
|
const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
|
|
3883
|
+
// [1301]③ config catalog server 半场:env 封顶不再是「五层五值互不知情」的暗手——每个真在场
|
|
3884
|
+
// 的运营方旋钮以 configOverrides 声明进 spec(advisory,core 折进 config.assembled 的
|
|
3885
|
+
// overrideReasons;「谁设的顶」变成读帧不考古)。只声明 SET 了的键(缺省不设=不污染帧)。
|
|
2135
3886
|
const declarations = [];
|
|
2136
3887
|
if (taskLimitCaps.timeoutSec !== undefined)
|
|
2137
3888
|
declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
|
|
@@ -2139,39 +3890,101 @@ async function main() {
|
|
|
2139
3890
|
declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
|
|
2140
3891
|
if (taskLimitCaps.maxTurns !== undefined)
|
|
2141
3892
|
declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
|
|
3893
|
+
// PAIR-REVIEW F-4:budget 族与墙钟基值的声明补齐——[1301]③ 首批只做了 limits 三键,而
|
|
3894
|
+
// MAX_TASK_COST_USD/MAX_TASK_TOKENS 在 caller 缺席时**直接成为** spec 值(cappedCeiling),core
|
|
3895
|
+
// 便记 provenance:"spec"=归因谎报(「谁设的 5 刀」重回考古)。同姿势:只声明 SET 了的键,
|
|
3896
|
+
// server 命名空间,绝不重复 core 目录键。
|
|
2142
3897
|
if (config.maxTaskCostUsd > 0)
|
|
2143
3898
|
declarations.push({ key: "server.budget.maxCostUsdCap", value: String(config.maxTaskCostUsd), reason: "env MAX_TASK_COST_USD (operator ceiling; becomes the effective value when the caller sends none)" });
|
|
2144
3899
|
if (config.maxTaskTokens > 0)
|
|
2145
3900
|
declarations.push({ key: "server.budget.maxTokensCap", value: String(config.maxTaskTokens), reason: "env MAX_TASK_TOKENS_MAX (operator ceiling; becomes the effective value when the caller sends none)" });
|
|
3901
|
+
// F-4 附:tenancy 墙钟基值(taskWallClockSec 在 multi-tenant 时注入 2400/3600 基值——同为
|
|
3902
|
+
// 「server 设的顶」,声明其来源;single-user 无墙=不声明)。
|
|
2146
3903
|
if (config.requirePrincipal === true)
|
|
2147
3904
|
declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
|
|
3905
|
+
// codex F1:core 目录自有键(limits.timeoutSec 等)**绝不重复声明**——core 已按真 provenance 发
|
|
3906
|
+
// 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
|
|
3907
|
+
// 声明自己命名空间的键;来源语义(caller vs 墙钟)由 timeoutSec 的 spec provenance + 上面的
|
|
3908
|
+
// Cap 声明组合可读。
|
|
2148
3909
|
return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
|
|
2149
3910
|
})(),
|
|
3911
|
+
// design/129: the TOC/interactive posture (single-user turnkey — the shell's
|
|
3912
|
+
// session lane) defaults background children to SESSION scope = CC Backgrounded semantics (a bg Agent/
|
|
3913
|
+
// Fork outlives the turn; completion arrives via the durable-inbox push). Multi-tenant / CI / workflow
|
|
3914
|
+
// deployments keep core's "task" default (no orphans burning tokens). Caller-trusted spec field
|
|
3915
|
+
// (systemPrompt tier), NEVER read from the request body. Same tenancy predicate as the wall clock above.
|
|
2150
3916
|
...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
|
|
3917
|
+
// ⑤ Per-task budget gate (1.37): honor a caller's requested ceiling but CAP it to the operator
|
|
3918
|
+
// ceiling (a request can ask for less, never more). core fails the task with errorCode budget.*
|
|
3919
|
+
// when crossed. budgetStreamCancel defaults true when maxCostUsd is set.
|
|
2151
3920
|
maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
|
|
2152
3921
|
maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
|
|
3922
|
+
// 1.40 near-budget degradation: only meaningful with a cost ceiling (the fraction is of it).
|
|
3923
|
+
// When this task has one, switch to the cheaper model at atCostFraction instead of hard-failing.
|
|
3924
|
+
// vision precheck (adversarial-review finding): DROP degrade for an image-carrying task when the
|
|
3925
|
+
// degrade TARGET can't read images (toSupportsImages=false). The precheck only sees the picked model, not the
|
|
3926
|
+
// external degrade target — so without this a runtime degrade would send the images to a text-only gateway (the
|
|
3927
|
+
// opaque 400 the precheck prevents). The task keeps its vision-capable main model; near-budget it hard-fails on
|
|
3928
|
+
// cost instead of image-failing. MODEL_DEGRADE_TO_VISION=true opts back in.
|
|
2153
3929
|
degrade: (() => {
|
|
2154
3930
|
if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
|
|
2155
3931
|
return undefined;
|
|
2156
3932
|
if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
|
|
3933
|
+
// S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
|
|
3934
|
+
// model and near-budget it hard-fails on cost instead of degrading (rationale above). Surface it.
|
|
2157
3935
|
metrics.inc("degrade_dropped_total", { reason: "vision_target" });
|
|
2158
3936
|
logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
|
|
2159
3937
|
return undefined;
|
|
2160
3938
|
}
|
|
2161
3939
|
return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
|
|
2162
3940
|
})(),
|
|
3941
|
+
// Long-term memory (design/138 S1): enabled when the memory ENGINE is wired AND a scope was derived
|
|
3942
|
+
// (single-user only — multi-tenant derives none, memory dark). MF-30 PAUSE (option B, per-request —
|
|
3943
|
+
// clay 2026-06-27 confirmed with core): `body.memoryWrite:false` makes THIS run read-only over memory
|
|
3944
|
+
// (`writeScope:null` — the engine materializes/reads but harvest commits nothing). Per-request (not a
|
|
3945
|
+
// stored flag) → no new per-session state; the shell carries the toggle. RESUME re-runs resolveSpec
|
|
3946
|
+
// from the persisted body, so a paused run stays paused across resume legs.
|
|
2163
3947
|
memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
|
|
3948
|
+
// Scenario-provided capabilities (e.g. code-review = repo tools + reviewer subagents + prompt).
|
|
3949
|
+
// RFC A2: the SelectEnvironment tool rides after the scenario's tools (spec.tools is ADDITIVE to core's
|
|
3950
|
+
// built-in roster — prepare-task mounts first-party tools separately). Only when the image chain is live.
|
|
2164
3951
|
tools: ((base) => {
|
|
2165
3952
|
const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
|
|
2166
3953
|
const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
|
|
3954
|
+
// [803]④ defer face (EXPERIMENTAL, default OFF): long-tail first-party tools get per-tool defer:true
|
|
3955
|
+
// (assessment + why NOT deferMode:"auto"/SendUserFile: src/capabilities/tool-defer.ts header).
|
|
2167
3956
|
return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
|
|
2168
3957
|
})(cap.tools),
|
|
3958
|
+
// Personalization: the caller's own skills ride after the scenario's; scenario
|
|
3959
|
+
// wins on a name clash (mergeUserSkills — the security baseline can't be shadowed). HTTP layer
|
|
3960
|
+
// already validated shape + caps (validateUserSkills).
|
|
2169
3961
|
skills: mergeUserSkills(cap.skills, body.skills, logger),
|
|
3962
|
+
// Sema-registry MCP servers (B1) filtered to this scenario (env-NAME refs already resolved at startup),
|
|
3963
|
+
// PLUS the caller's gated per-request MCP (R3 — TOC local `.mcp.json`). resolveRequestMcp honors body.mcpServers
|
|
3964
|
+
// on any SINGLE-USER deployment (`requirePrincipal!==true` — the requester is the super-admin of their own
|
|
3965
|
+
// worker; the stdio MCP runs on the worker, not the exec env, so the lane is irrelevant → seamless TOC↔cloud);
|
|
3966
|
+
// a multi-tenant deployment returns the baseline unchanged (gate closed → ignored, advertised via
|
|
3967
|
+
// capabilities.mcpInjection). Baseline wins on a name clash (a caller can ADD a server, never SHADOW a configured one).
|
|
2170
3968
|
mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
|
|
3969
|
+
// center prompts 投影([1057]①f):center 下发的场景终形(overrides[s] ?? 基线)赢过内置 provider
|
|
3970
|
+
// (center-wins,scenario overlay 同先例);无 pack/该场景空声明 ⇒ 内置照旧。声明数组逐字喂 typed
|
|
3971
|
+
// 钩子(contentHash 透传,core 1.315 校验入 manifest);packId 归因在采用日志(center_prompts_adopted)。
|
|
3972
|
+
// codex R4:读请求级快照 centerDecls(与 append-less 门同一份),不再回读活 ref——刷新竞态下门与
|
|
3973
|
+
// provider 才不会各看一版(TOCTOU:门判安全、pick 到已组装包=core 侧静默丢 rider 复活)。
|
|
2171
3974
|
promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
|
|
3975
|
+
// F4: high-risk write approval gate. DURABLE (design/45, opt-in): the gated `ask` SUSPENDS the task
|
|
3976
|
+
// (durable checkpoint, resumable on any replica). Else the legacy POLL gate: the call waits on a
|
|
3977
|
+
// durable pending row any instance can decide via /v1/approvals, releasing at the task deadline.
|
|
3978
|
+
// Durable ask (TC-5.4, core 1.95): in durable mode an AskUserQuestion call suspends like an F4 gate —
|
|
3979
|
+
// the question policy adjudicates it `ask`, the operator answers out-of-band, and the resume carries
|
|
3980
|
+
// the QuestionAnswer (server.ts `body.answer` → onQuestion closure). Suspend side mounts the tool with
|
|
3981
|
+
// QUESTION_AWAITS_RESUME (it must never run pre-suspend; reaching it = wiring bug, typed throw).
|
|
2172
3982
|
toolPolicy: durableEnabled
|
|
2173
3983
|
? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
|
|
2174
3984
|
requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
|
|
3985
|
+
// The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
|
|
3986
|
+
// durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
|
|
3987
|
+
// construction; fail-closed inside the policy on a store error.
|
|
2175
3988
|
...(approvalExemptionStore && auth?.sessionId
|
|
2176
3989
|
? {
|
|
2177
3990
|
exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
|
|
@@ -2186,7 +3999,8 @@ async function main() {
|
|
|
2186
3999
|
deny: config.approvalDeny,
|
|
2187
4000
|
pollMs: config.approvalPollMs,
|
|
2188
4001
|
context: () => ({ sessionId: auth?.sessionId ?? null, owner: auth?.principal ?? null }),
|
|
2189
|
-
neverAuto: config.approvalNeverAuto,
|
|
4002
|
+
neverAuto: config.approvalNeverAuto, // workflow audit 2026-07-13: the ordering lock must hold on the OA leg too
|
|
4003
|
+
// Same probe on the legacy poll gate (sessionId threaded via ctx here).
|
|
2190
4004
|
...(approvalExemptionStore
|
|
2191
4005
|
? {
|
|
2192
4006
|
exempt: (sessionId, toolName) => approvalExemptionStore.has(sessionId, toolName),
|
|
@@ -2195,8 +4009,21 @@ async function main() {
|
|
|
2195
4009
|
: {}),
|
|
2196
4010
|
})
|
|
2197
4011
|
: singleUserAutoAcceptBaseline
|
|
4012
|
+
// 🔴 (live finding: single-user host-lane Bash ran UNGATED, no adjudication): a
|
|
4013
|
+
// single-user turnkey worker with NO expressed gate intent previously left `toolPolicy` UNDEFINED →
|
|
4014
|
+
// core's write-capable hand tools mount UNADJUDICATED (core error-logs it every task; host lane has NO
|
|
4015
|
+
// sandbox to fall back on). CC's trust model is auto-accept, but the GATE MECHANISM must be PRESENT
|
|
4016
|
+
// (core 原则: "机制留、默认可更宽 auto-accept"). So wire an adjudicated auto-accept BASELINE
|
|
4017
|
+
// (`createAllowDenyPolicy({})` = a present, effect-aware policy that allows all): satisfies core's
|
|
4018
|
+
// `hasEffectAwareGate`, restores observability + a hook/tighten point, and the operator STILL tightens
|
|
4019
|
+
// irreversible ops via `AUTONOMY` / `commandPolicy` (layered TIGHTEN-ONLY by applyRuntimeGovernance).
|
|
4020
|
+
// Interactive approval routed to the shell HITL = follow-on. 🔴 `singleUserAutoAcceptBaseline` requires
|
|
4021
|
+
// ZERO gate intent — a single-user op who SET approval flags but wired no store falls to `undefined`
|
|
4022
|
+
// (core warns = real misconfig, not masked); multi-tenant likewise stays `undefined` (approval required).
|
|
2198
4023
|
? createAllowDenyPolicy({})
|
|
2199
4024
|
: undefined,
|
|
4025
|
+
// Durable suspend needs both the store (here, per-task) and the opt-in scope key (multi-tenant =
|
|
4026
|
+
// principal; "_" when auth is off). core suspends on a policy `ask` only when these are present.
|
|
2200
4027
|
...(durableEnabled
|
|
2201
4028
|
? {
|
|
2202
4029
|
checkpointStore,
|
|
@@ -2204,6 +4031,11 @@ async function main() {
|
|
|
2204
4031
|
scope: auth?.principal ?? "_",
|
|
2205
4032
|
...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
|
|
2206
4033
|
},
|
|
4034
|
+
// design/80 seam #2: opt this task into resource/preempt durable-suspend (design/74 third state)
|
|
4035
|
+
// when RESOURCE_SUSPEND=true — a budget/turns/walltime limit OR a scheduler preempt durably SUSPENDS
|
|
4036
|
+
// (resumable) instead of failing. SAME scope key as durableApproval (the principal). The decision
|
|
4037
|
+
// (incl. the 🔴 verify/cascade EXCLUSION that keeps eligibility aligned with the preemptSignal wiring,
|
|
4038
|
+
// so an inner suspend can't orphan a VM+checkpoint) lives in the pure, unit-tested `resourceSuspendOptIn`.
|
|
2207
4039
|
...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
|
|
2208
4040
|
enabled: config.resourceSuspend,
|
|
2209
4041
|
ttlSec: config.resourceSuspendTtlSec,
|
|
@@ -2215,21 +4047,58 @@ async function main() {
|
|
|
2215
4047
|
}
|
|
2216
4048
|
: {}),
|
|
2217
4049
|
};
|
|
4050
|
+
// Runtime governance "second baton" (center §10): compile the operator's autonomy + commandPolicy onto the
|
|
4051
|
+
// spec TIGHTEN-ONLY. commandPolicy layers via combinePolicies onto the approval/durable baseline above (NOT a
|
|
4052
|
+
// bare overwrite — TRAP #1), autonomy expands to handsReadOnly/shellGate (TRAP #2). Applied here so it also
|
|
4053
|
+
// re-applies identically on the resume paths (which rebuild via resolveSpec) — core requires shellGate/
|
|
4054
|
+
// toolPolicy be re-supplied on resume (omitting shellGate would leave a resumed run's bash UNGATED).
|
|
4055
|
+
// tightenTaskSpec THROWS on a loosening misconfig (fail-loud).
|
|
4056
|
+
//
|
|
4057
|
+
// 🔴 RESUME uses LIVE config (not a per-task frozen snapshot), IDENTICAL to how the approval baseline above
|
|
4058
|
+
// reads live `config.approvalRequire`. So if an operator LOOSENS the fleet governance while a task is
|
|
4059
|
+
// suspended, the resumed task picks up the looser policy (governance stays per-call tighten-only, so a
|
|
4060
|
+
// mid-suspend TIGHTENING is honored; a loosening is an explicit operator action). Freezing deployment policy
|
|
4061
|
+
// per-task across suspend is a larger, separate decision that must cover the approval baseline too — tracked
|
|
4062
|
+
// for center (adversarial-review HIGH-1), deliberately NOT a governance-only partial freeze here (that would
|
|
4063
|
+
// be an inconsistent false-safety: approval would still be live).
|
|
2218
4064
|
const governedBase = applyRuntimeGovernance(spec, { autonomy: config.autonomy, commandPolicy: config.commandPolicy });
|
|
4065
|
+
// Fold the client's per-request settings stamp onto the governed spec, TIGHTEN-ONLY (deny-wins) — AFTER
|
|
4066
|
+
// the approval baseline + operator governance, so the order is deployment ⊇ operator ⊇ client (each can only
|
|
4067
|
+
// narrow). tightenTaskSpec THROWS if the client settings would LOOSEN a stricter baseline → a 422 client error
|
|
4068
|
+
// (a settings stamp is tighten-only by contract; never a silent weakening). Re-applies idempotently on resume.
|
|
2219
4069
|
let governed = governedBase;
|
|
4070
|
+
// R4: fold a LIGHT top-level `body.permissionMode` onto the parsed settings (the explicit
|
|
4071
|
+
// per-turn intent WINS over a bundle `defaultMode`) → the SAME tighten-only governance path. Post-[816] all
|
|
4072
|
+
// five CC modes are honored as gate-SHAPE choices (see coercePermissionMode/deriveSettingsPolicy): `plan` ⇒
|
|
4073
|
+
// read-only hands + present_plan; `default`/`auto` ⇒ the fs-write ask gate; `acceptEdits` ⇒ the cwd-domain
|
|
4074
|
+
// variant; `bypassPermissions` ⇒ no mode gate (the deployment baseline is composed above and untouchable).
|
|
2220
4075
|
const bodyMode = coercePermissionMode(body.permissionMode);
|
|
2221
4076
|
let effectiveSettings = bodyMode ? withPermissionMode(parsedSettings.settings, bodyMode) : parsedSettings.settings;
|
|
4077
|
+
// codex R3 second bypass: settings.outputStyle ALSO lands in spec.appendSystemPrompt (applyTaskSettings
|
|
4078
|
+
// fold) — on an append-less pack core would discard it silently, exactly like the top-level rider. Same
|
|
4079
|
+
// disposition: fresh submits 400 at the HTTP gate; here (the resume mirror + any leg the gate didn't see)
|
|
4080
|
+
// strip + warn. Spread-copy, never mutate parsedSettings (it may be consulted elsewhere).
|
|
2222
4081
|
if (appendLessPack && effectiveSettings?.outputStyle) {
|
|
2223
4082
|
logger.warn("output_style_dropped", { detail: "the effective prompt pack cannot mount the append slot (already-assembled systemPrompt or center assembled-identity declaration)", sessionId: auth?.sessionId ?? null });
|
|
2224
4083
|
const { outputStyle: _dropped, ...rest } = effectiveSettings;
|
|
2225
4084
|
effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
|
|
2226
4085
|
}
|
|
4086
|
+
// codex R14 resume mirror of the aggregate cap: both carriers fold into ONE spec field — a pre-cap stored
|
|
4087
|
+
// body can be individually valid but combined over the bound (fresh submits 400'd this in prepareSpec).
|
|
4088
|
+
// Drop the STYLE (it composes second; the knowledge rider keeps priority) + warn, never silently oversize.
|
|
2227
4089
|
if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
|
|
2228
4090
|
logger.warn("output_style_dropped", { detail: `combined append carriers exceed the ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} cap (rider ${acceptedAppend.length} + style ${effectiveSettings.outputStyle.length}) — style dropped (pre-cap stored body on a resume leg?)`, sessionId: auth?.sessionId ?? null });
|
|
2229
4091
|
const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
|
|
2230
4092
|
effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
|
|
2231
4093
|
}
|
|
4094
|
+
// [820]③ scratchpadDir, hoisted from the envFacts block below so the SAME per-session dir feeds BOTH the
|
|
4095
|
+
// `# Environment` fact and the fs-write gate's exemptDirs (single-source path via scratchpadDirFor inside
|
|
4096
|
+
// ensureScratchpadDir). HOST-semantics lanes only + session required — rationale at the envFacts consumer.
|
|
2232
4097
|
const hostSemanticsLane = config.remoteExec === undefined || config.remoteExec.provider === "host";
|
|
4098
|
+
// G3([816]③/[820]③,[1840]§四):壳供 body.scratchpadDir 优先——壳(CC 形)有自己的 per-session
|
|
4099
|
+
// scratchpad 约定路径,接受后事实/豁免/根围栏三处同源指向壳的目录(壳侧提示词与 server 写门不再
|
|
4100
|
+
// 两个 scratchpad)。验收门 fail-closed(多租户/远程 lane/浅路径全拒收,warn 后回落自算)——
|
|
4101
|
+
// 规则与理由集中在 acceptShellScratchpadDir。
|
|
2233
4102
|
const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
|
|
2234
4103
|
requirePrincipal: config.requirePrincipal,
|
|
2235
4104
|
hostSemanticsLane,
|
|
@@ -2238,19 +4107,47 @@ async function main() {
|
|
|
2238
4107
|
const scratchpadDir = shellScratchpad ??
|
|
2239
4108
|
(hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
|
|
2240
4109
|
if (effectiveSettings) {
|
|
4110
|
+
// [816]/[820]① fs-write gate wiring — HOST-semantics lanes only: core's createFsWriteGatePolicy
|
|
4111
|
+
// canonicalizes每一个 target/dir 走给定 env 的真实 fs(exists/canonicalPath/readLink,dist 亲读),所以
|
|
4112
|
+
// env 必须就是 hand 工具真正写的那块盘。e2b/k8s/ssh/adb/local-docker 的沙箱 env 由 core 在 spec 之后
|
|
4113
|
+
// 才铸(executionEnvFactory),这里给 worker 本机 env 会拿错误的 fs 裁决(symlink/exists 全答错)——
|
|
4114
|
+
// 沙箱 lane 诚实不挂(gate=undefined ⇒ derive 回落 base 规则,pre-[816] 行为)。
|
|
4115
|
+
// NodeExecutionEnv 构造是纯字段赋值(不 spawn)。
|
|
4116
|
+
//
|
|
4117
|
+
// 修5(三路复审 W4,cwd 基准漂移):gate 的 cwd 基准=与 hands 真跑处同源,分三形:
|
|
4118
|
+
// · session 注册过 cwd(cwdHonored 已在注册处把关)⇒ 用它——与 host factory 的 effectiveHostWorkspace
|
|
4119
|
+
// 第一优先级同源(顶层 HTTP 任务无 parentCwd,resolver 即 session cwd)。
|
|
4120
|
+
// · `host` factory lane 且未注册 ⇒ hands 落在 factory 才铸的 EPHEMERAL per-task 目录(spec 期不可知)。
|
|
4121
|
+
// 此前回退 process.cwd()(服务自身启动目录)——acceptEdits 会把服务目录当 auto-allow 域、多任务共享
|
|
4122
|
+
// 进程时判定漂移(W4)。改为 fail-safe 哨兵:一个从不创建的目录 ⇒ acceptDirs/相对路径 canonicalize
|
|
4123
|
+
// 失败即 ask(fs-write-gate-policy dist 亲读:dir.ok=false 跳过、target 解析失败=ask);绝对路径裁决
|
|
4124
|
+
// (hand 工具的书面契约形)与 scratchpad exempt 均不受影响。
|
|
4125
|
+
// · provider 未设(in-process host / run-local)⇒ hands 就在本进程 cwd 跑(无 factory),process.cwd()
|
|
4126
|
+
// 正是真工作目录,保留。
|
|
2241
4127
|
const fsWriteGate = hostSemanticsLane
|
|
2242
4128
|
? (() => {
|
|
2243
4129
|
const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
|
|
2244
4130
|
const gateCwd = sessionCwd ??
|
|
2245
4131
|
(config.remoteExec === undefined
|
|
2246
4132
|
? process.cwd()
|
|
2247
|
-
: join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
|
|
4133
|
+
: join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted")); // never created — see above
|
|
2248
4134
|
return {
|
|
2249
4135
|
env: new NodeExecutionEnv({ cwd: gateCwd }),
|
|
2250
4136
|
cwd: gateCwd,
|
|
2251
4137
|
...(scratchpadDir ? { scratchpadDir } : {}),
|
|
4138
|
+
// ③ (core 1.295) sensitive-path write deny set:组合进 gate 腿的同一 combinePolicies 折叠
|
|
4139
|
+
// (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
|
|
4140
|
+
// 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)。
|
|
2252
4141
|
...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
|
|
4142
|
+
// [1557]§四 opt-in(cli[1555]②「Bash echo > file 绕写门」缺口的部署侧补丁):MANUAL_MODE_SHELL_GATE
|
|
4143
|
+
// 未设置时 config.manualModeShellGate 缺席,本行不传字段——deriveSettingsPolicy 的 default/
|
|
4144
|
+
// auto/acceptEdits 分支照旧不碰 TaskSpec.shellGate,零行为变化。
|
|
2253
4145
|
...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
|
|
4146
|
+
// [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
|
|
4147
|
+
// remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
|
|
4148
|
+
// 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
|
|
4149
|
+
// 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
|
|
4150
|
+
// approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。
|
|
2254
4151
|
...(approvalExemptionStore && auth?.sessionId
|
|
2255
4152
|
? {
|
|
2256
4153
|
isExempt: async (toolName) => {
|
|
@@ -2264,6 +4161,10 @@ async function main() {
|
|
|
2264
4161
|
};
|
|
2265
4162
|
})()
|
|
2266
4163
|
: undefined;
|
|
4164
|
+
// [1248]②/codex F2 — the workflow ask leg's session-exemption probe, on EVERY lane (unlike fsWriteGate:
|
|
4165
|
+
// the workflow gate is name-keyed, not fs-adjudicated). Same store + canonical toolName key space as the
|
|
4166
|
+
// fs-gate probe — one remember="session" grant serves both; without it the durable lane re-parks every
|
|
4167
|
+
// later run_workflow call after an operator already granted "don't ask again this session".
|
|
2267
4168
|
const workflowGate = approvalExemptionStore && auth?.sessionId
|
|
2268
4169
|
? {
|
|
2269
4170
|
isExempt: async (toolName) => {
|
|
@@ -2281,12 +4182,36 @@ async function main() {
|
|
|
2281
4182
|
throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
|
|
2282
4183
|
}
|
|
2283
4184
|
}
|
|
4185
|
+
// subagent-hands follow-on (adversarial-review finding): core does NOT propagate
|
|
4186
|
+
// handsReadOnly/shellGate to a delegated subagent (SubagentToolOptions has NO read-only knob; subagent.js's child
|
|
4187
|
+
// runTask omits it). So a read-only (permissionMode=plan) parent that mounts the Task tool would let the CHILD get
|
|
4188
|
+
// WRITABLE hands — now reachable because subRunner has an executionEnvFactory (the hands fix). That defeats plan
|
|
4189
|
+
// mode's read-only contract (a within-tenant gap, not a cross-tenant breach). Until core propagates it (relayed),
|
|
4190
|
+
// strip the delegation tools when this task is hands-read-only — a read-only run delegates nothing writable.
|
|
4191
|
+
// `stripDelegationTools` (runtime-governance.ts, tested there) matches the subagent tool ("Agent", core 1.202
|
|
4192
|
+
// canonical; legacy alias "Task") + Fork in CANONICAL space — the pre-1.202 raw `!== "Task"` filter here
|
|
4193
|
+
// silently stopped matching when core renamed the tool (fail-open: a plan-mode parent could delegate a
|
|
4194
|
+
// WRITABLE-hands child).
|
|
2284
4195
|
if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
|
|
2285
4196
|
governed = { ...governed, tools: stripDelegationTools(governed.tools) };
|
|
2286
4197
|
}
|
|
4198
|
+
// §7 P0.5 per-task sandbox image(用户按需选像): the caller sends a PROFILE
|
|
4199
|
+
// (intent) — NEVER a digest (a sha256 is enumerable + caller-unbound → trusting a caller digest is fail-OPEN;
|
|
4200
|
+
// a user could pass another tenant's digest). The TRUSTED control plane resolves profile→digest with the
|
|
4201
|
+
// caller's principal, FAIL-CLOSED (same visibility the /v1/images/select admission uses), and registers it by
|
|
4202
|
+
// sessionId for the k8s factory to apply per-pod. RESUME re-runs resolveSpec ⇒ re-resolves LIVE (no stale
|
|
4203
|
+
// frozen digest), consistent with the governance live-config discipline above.
|
|
4204
|
+
// RFC A2: a session-level SelectEnvironment binding (PROFILE intent, tool-written) is the fallback when the
|
|
4205
|
+
// request body carries no explicit profile. Body wins (an explicit caller choice for THIS task); either way
|
|
4206
|
+
// the profile is re-resolved FAIL-CLOSED right here — a session binding never skips re-admission, and the
|
|
4207
|
+
// F3 boundary contract holds structurally (binding is only read at task boundaries, children inherit
|
|
4208
|
+
// the registered ref at spawn).
|
|
2287
4209
|
const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
|
|
4210
|
+
// cascade/verify mirror the HTTP-layer v1 reject (server.ts): those flows strip/replace the session, so a
|
|
4211
|
+
// session-level binding would not reach the rungs/verifier sub-run — silently mixed environments. A body
|
|
4212
|
+
// profile on cascade/verify is already 400-rejected up front; the session fallback simply does not apply.
|
|
2288
4213
|
const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
|
|
2289
|
-
let resolvedImageCaps;
|
|
4214
|
+
let resolvedImageCaps; // RFC A1: rides into envFacts
|
|
2290
4215
|
if (taskImageProfile !== undefined) {
|
|
2291
4216
|
if (config.remoteExec?.provider !== "k8s") {
|
|
2292
4217
|
throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
|
|
@@ -2303,16 +4228,54 @@ async function main() {
|
|
|
2303
4228
|
const resolved = await resolveSandboxImageRef({
|
|
2304
4229
|
profile: taskImageProfile,
|
|
2305
4230
|
...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
|
|
4231
|
+
// principal comes from the auth channel (trusted header / verified JWT), NEVER the body — so a caller
|
|
4232
|
+
// cannot widen its own visibility. explicitOperator (NOT isOperator): an empty OPERATOR_PRINCIPALS must
|
|
4233
|
+
// yield operator=false here — isOperator([],p)=true-for-all would let any caller resolve tenant-scoped
|
|
4234
|
+
// images on an operator-less deployment (adversarial-review HIGH-1; the bake/direct-door boot guards do
|
|
4235
|
+
// NOT cover per-task image selection).
|
|
2306
4236
|
viewer: { operator: explicitOperator(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
|
|
2307
4237
|
index: imageIndex,
|
|
2308
4238
|
});
|
|
2309
4239
|
if (!resolved.ok)
|
|
2310
4240
|
throw new HttpError(resolved.status, resolved.message);
|
|
4241
|
+
// Register by sessionId — the factory reads it by ctx.sessionId (stable on every path; see the factory
|
|
4242
|
+
// wrapper). RESUME re-runs resolveSpec ⇒ re-resolves LIVE + re-registers (no stale frozen digest).
|
|
4243
|
+
// ⚠️ KNOWN narrow edge (NOT a security breach): two CONCURRENT requests for the SAME session (same owner —
|
|
4244
|
+
// session ownership is enforced) race this set(); a request the run-claim later rejects (409) can leave its
|
|
4245
|
+
// image bound for the active run. Both images are the SAME principal's own admitted images, so the worst
|
|
4246
|
+
// case is the user's run using the user's other selected image (a correctness glitch, not cross-tenant).
|
|
4247
|
+
// Hardening (register-after-claim, keyed by the durable run id) needs the registry threaded into the server
|
|
4248
|
+
// claim path — tracked as a follow-up; cascade/verify are rejected with a profile (see prepareSpec) because
|
|
4249
|
+
// they strip/replace the session and would silently fall back to the default image.
|
|
2311
4250
|
perTaskImage.set(auth.sessionId, resolved.ref);
|
|
2312
4251
|
resolvedImageCaps = resolved.capabilities;
|
|
2313
4252
|
}
|
|
4253
|
+
// RFC A1 (core 1.240.0 `TaskSpec.envFacts`): compose the deployment-trusted sandbox facts
|
|
4254
|
+
// (profile/capabilities from the fail-closed resolution above, region pkgSource, exec-lane egress posture)
|
|
4255
|
+
// and ride them on the spec — core renders the `# Environment` block (sanitized+bounded, egress=none gets
|
|
4256
|
+
// the "downloads will fail" caveat). Facts only ride when known; nothing known ⇒ no field ⇒ block unchanged.
|
|
2314
4257
|
if (config.envFactsEnabled) {
|
|
4258
|
+
// pkgSource is a FACT only where the derivePkgSourceEnv injection actually lands (the e2b/k8s sandbox
|
|
4259
|
+
// lanes — main.ts wiring above). host/ssh/adb/run-local get NO injection, so with the global default
|
|
4260
|
+
// (1.180.0) an unconditional read would tell the model "official sources" about an environment we never
|
|
4261
|
+
// touched (workflow audit 2026-07-13: honest-facts posture, same as egress's "don't declare what you
|
|
4262
|
+
// don't know").
|
|
2315
4263
|
const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
|
|
4264
|
+
// [820]③ scratchpadDir (core field + prompt chain in since 1.257.3; this fill was the missing half).
|
|
4265
|
+
// HOST-semantics lanes only (provider unset = in-process host, or explicit "host"): the advertised path
|
|
4266
|
+
// must be REAL to the executing hands — on e2b/k8s/ssh/adb/local-docker the tools run off this box, so a
|
|
4267
|
+
// worker-local path would be a lie (same honest-facts axis as pkgSource above; the remote lanes get their
|
|
4268
|
+
// IN-SANDBOX path via the [848] branch below instead). Session-scoped dir under
|
|
4269
|
+
// localDataRoot, mkdir'd at the HOISTED compute above the settings fold (fail → fact omitted) — the SAME
|
|
4270
|
+
// dir now feeds createFsWriteGatePolicy's exemptDirs ([820]① landed at core 1.290, wired in the
|
|
4271
|
+
// applyTaskSettings gate above). No sessionId (adhoc no-session task) ⇒ no per-session home ⇒ fact
|
|
4272
|
+
// omitted. Lifecycle/reaping: see ensureScratchpadDir docs (deferred).
|
|
4273
|
+
// [848] remote lanes' counterpart: on e2b/k8s/local-docker/ssh the withRemoteScratchpad-decorated
|
|
4274
|
+
// factory `mkdir -p`s exactly this path on the env's first exec, so advertising it is honest — with
|
|
4275
|
+
// the one best-effort caveat (a failed mkdir leaves the advertised path absent; see the decorator
|
|
4276
|
+
// header). Mutually exclusive with the host-lane fact above by construction (hostSemanticsLane owns
|
|
4277
|
+
// scratchpadDir; this branch is remote lanes only), and it applies the decorator's OWN validity rule
|
|
4278
|
+
// (remoteScratchpadDirFor: absent/unsafe sessionId ⇒ no decoration ⇒ no fact). adb stays factless.
|
|
2316
4279
|
const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
|
|
2317
4280
|
? remoteScratchpadDirFor(auth?.sessionId)
|
|
2318
4281
|
: undefined;
|
|
@@ -2323,17 +4286,36 @@ async function main() {
|
|
|
2323
4286
|
...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
|
|
2324
4287
|
egress: egressForRemoteExec(config.remoteExec),
|
|
2325
4288
|
...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
|
|
4289
|
+
// [1467]②(core 1.360)durable resume 铸句真值:按车道能力面,单真源 resumeFactsForLane
|
|
4290
|
+
// (e2b=双 preserved/k8s=按 s3Snapshot 有效能力分叉/ssh·adb·host=scratch preserved/
|
|
4291
|
+
// local-docker=不声明)。
|
|
2326
4292
|
resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
|
|
2327
4293
|
});
|
|
2328
4294
|
if (facts)
|
|
2329
4295
|
governed = { ...governed, envFacts: facts };
|
|
2330
4296
|
}
|
|
4297
|
+
// [1339] scratchpad 写门失效真因:提示词(envFacts.scratchpadDir)和 policy 层豁免(exemptDirs)都
|
|
4298
|
+
// 接了,但 core fs 工具的**根围栏**(resolveKey:taskRootPath+additionalDirectories)在 policy 之前
|
|
4299
|
+
// 短路——Write/Edit 到 scratchpad 直接 path_not_in_root,exemptDirs 根本没被问到(Bash 无根围栏,
|
|
4300
|
+
// 反而能写=口径矛盾还教模型绕门)。修:host 车道把同一 scratchpadDir 折进 spec.additionalDirectories
|
|
4301
|
+
// (server 自算的 per-session 路径,非租户输入,不走 cwdHonored 门限;compute 时已 mkdir,core 的
|
|
4302
|
+
// canonicalPath 必过)。remote 车道暂不折:in-sandbox 路径由 [848] 装饰器首 exec 才 mkdir,prepare 期
|
|
4303
|
+
// canonicalize 会失败被 core 静默跳过——那半场随 core 排序案另行。
|
|
4304
|
+
// codex R3(M2):**独立于 envFactsEnabled 门**——SANDBOX_ENV_FACTS=false 只关事实展示,写门围栏
|
|
4305
|
+
// 豁免是行为契约,关展示不得顺带关围栏(否则该配置下 [1339] 原病复发)。
|
|
2331
4306
|
if (scratchpadDir !== undefined) {
|
|
2332
4307
|
const dirs = governed.additionalDirectories ?? [];
|
|
2333
4308
|
if (!dirs.includes(scratchpadDir))
|
|
2334
4309
|
governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
|
|
2335
4310
|
}
|
|
4311
|
+
// A value router (S1 verdict §6.3 cash-out, src/router/route-orchestration.ts): auto-decide this task's
|
|
4312
|
+
// orchestration. CONSERVATIVE + DETERMINISTIC — default single; escalate to the SUP prevention posture only
|
|
4313
|
+
// on a capability-danger signal (write/exec on a NON-isolated env — an isolated sandbox CONTAINS the harm,
|
|
4314
|
+
// so isolated tasks stay single, matching the verdict's default). caller-explicit team (council/debate/team)
|
|
4315
|
+
// is honored, never auto-produced. Flag-gated (ROUTER_ENABLED) — ships dark, enable after live validation.
|
|
2336
4316
|
if (config.routerEnabled) {
|
|
4317
|
+
// Resolve isolation from provider AND runtimeClass — a k8s worker on plain runc is NOT VM-isolated
|
|
4318
|
+
// (review HIGH); fail-closed for runc/empty/unknown.
|
|
2337
4319
|
const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
|
|
2338
4320
|
const decision = routeServiceTask({
|
|
2339
4321
|
isolatedExecEnv,
|
|
@@ -2343,13 +4325,20 @@ async function main() {
|
|
|
2343
4325
|
});
|
|
2344
4326
|
logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
|
|
2345
4327
|
if (decision.mode === "supervisor") {
|
|
4328
|
+
// SUP prevention posture: gate every bash (shellGate:"always") AND the direct write hand tools
|
|
4329
|
+
// (edit_file/write_file via toolPolicy) — the verdict's gate-deny on capability-danger. tighten-only
|
|
4330
|
+
// (composes with the governance/approval baseline; never loosens). The worker's existing approval
|
|
4331
|
+
// machinery (durable/poll) enforces the suspend — same precondition as autonomy "ask".
|
|
2346
4332
|
return tightenTaskSpec(governed, supPostureOverrides());
|
|
2347
4333
|
}
|
|
2348
4334
|
}
|
|
2349
4335
|
return governed;
|
|
2350
4336
|
};
|
|
2351
4337
|
const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
|
|
4338
|
+
// D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
|
|
4339
|
+
// before the server, so it calls through this late-bound reference).
|
|
2352
4340
|
runDenySweep = server.denyExpiredApprovals;
|
|
4341
|
+
// [1934]:绑址旋钮 + 无鉴权自收窄(理由见 resolveBindHost 顶注)。undefined = Node 默认全接口。
|
|
2353
4342
|
const bindHost = resolveBindHost(config);
|
|
2354
4343
|
await new Promise((resolve) => (bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve)));
|
|
2355
4344
|
logger.info("listening", {
|
|
@@ -2359,6 +4348,9 @@ async function main() {
|
|
|
2359
4348
|
? { note: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
|
|
2360
4349
|
: {}),
|
|
2361
4350
|
});
|
|
4351
|
+
// fleet worker 接线(announce/heartbeat + usage 批报;lease 消费后一拍):boot 完成后
|
|
4352
|
+
// 注册(=首次心跳)。门 = center lane 配置 ∧ FLEET_ADVERTISE_ADDRESS 显式声明(不自猜可达地址);任一缺 → undefined
|
|
4353
|
+
// = 非 fleet 部署零行为。draining 翻转的即刻 announce 在 drainThenShutdown;退出 DELETE 在 hardShutdown。
|
|
2362
4354
|
const fleetClient = startFleetClientFromEnv(config, {
|
|
2363
4355
|
instanceId,
|
|
2364
4356
|
version: serviceVersion(),
|
|
@@ -2372,27 +4364,39 @@ async function main() {
|
|
|
2372
4364
|
port: config.port,
|
|
2373
4365
|
model: config.model.id,
|
|
2374
4366
|
brain: brainSummary(config),
|
|
2375
|
-
|
|
4367
|
+
// shared = cross-replica TiDB breaker state; in-process = core's per-replica Map; off = breaker disabled
|
|
4368
|
+
breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off", // 1.108: was hard-coded "shared(tidb)" — lied under DB_BACKEND=pg
|
|
4369
|
+
// On DB_BACKEND=local the wiring coerces sessionBackend to the LABEL "tidb" (meaning "the
|
|
4370
|
+
// backend's durable store, generically") — printing it verbatim read as a SQL deployment on a file-everything
|
|
4371
|
+
// box. Print the effective store instead (same honesty fix as breakerState above).
|
|
2376
4372
|
session: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
|
|
4373
|
+
// design/138 S1: the file-based memory engine (single-user only); "off" = MEMORY_ENGINE=off or multi-tenant.
|
|
2377
4374
|
memory: memoryEngine ? `engine(${memoryEngine.root})` : "off",
|
|
2378
4375
|
instanceId,
|
|
2379
4376
|
rateLimitPerMin: config.rateLimitPerMin,
|
|
2380
4377
|
rateLimit: config.rateLimitPerMin > 0 ? (backend ? `shared()` : "in-process") : "off",
|
|
2381
4378
|
maxPrincipalCostUsd: config.maxPrincipalCostUsd || null,
|
|
2382
4379
|
costQuota: config.maxPrincipalCostUsd > 0 ? (backend ? `shared()` : "in-process") : "off",
|
|
4380
|
+
// durable offload store for large tool results (cross-replica wake); else core's in-process default
|
|
2383
4381
|
toolResultStore: toolResultStore ? (backend?.kind === "local" ? "file(local)" : "shared(sql)") : "in-process",
|
|
2384
4382
|
otel: config.otel ? config.otel.endpoint : null,
|
|
2385
|
-
perModelKeys: Object.keys(config.modelApiKeyEnv).length,
|
|
4383
|
+
perModelKeys: Object.keys(config.modelApiKeyEnv).length, // models with their own upstream key (else gateway)
|
|
2386
4384
|
sealedModelKeys: Object.values(config.modelApiKeys).filter((v) => typeof v === "string").length,
|
|
2387
|
-
sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length,
|
|
4385
|
+
sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length, // sealed-box custody keys unsealed (count only — values never log)
|
|
4386
|
+
// SVC-1: durable workflow record + at-least-once notify backend (file = crash-safe ledger; memory = ephemeral)
|
|
2388
4387
|
workflowRunStore: config.selfOrchestrationEnabled
|
|
2389
4388
|
? config.workflowRunStoreBackend === "memory"
|
|
2390
4389
|
? "in-process"
|
|
2391
4390
|
: sqlWorkflowRunStore
|
|
2392
|
-
? `db(${backend.kind})`
|
|
4391
|
+
? `db(${backend.kind})` // P1: cross-replica twin active
|
|
2393
4392
|
: "file"
|
|
2394
4393
|
: "off",
|
|
2395
4394
|
});
|
|
4395
|
+
// SVC-1 at-least-once notify — CRASH-RECOVERY sweep: once at boot, re-derive every un-acked (started-but-not-
|
|
4396
|
+
// delivered) workflow's terminal state from the durable run store and re-deliver the completion notify that a
|
|
4397
|
+
// prior replica crash dropped (idempotent on the receiver). Fire-and-forget AFTER listen so startup isn't
|
|
4398
|
+
// blocked on it (recovery only re-fires completion notifications — independent of serving new requests). A
|
|
4399
|
+
// sweep throw is logged, never fatal.
|
|
2396
4400
|
if (workflowNotifyGate) {
|
|
2397
4401
|
void workflowNotifyGate
|
|
2398
4402
|
.recover({ orphanGraceMs: config.workflowOrphanGraceMs })
|
|
@@ -2408,13 +4412,20 @@ async function main() {
|
|
|
2408
4412
|
otelExporter?.stop();
|
|
2409
4413
|
breakerState?.stop();
|
|
2410
4414
|
if (costQuota && "stop" in costQuota)
|
|
2411
|
-
costQuota.stop();
|
|
4415
|
+
costQuota.stop(); // the DB-backed quota/limiter (TiDB|PG) have a refresh loop to stop; the in-memory ones don't
|
|
2412
4416
|
if (rateLimiter && "stop" in rateLimiter)
|
|
2413
4417
|
rateLimiter.stop();
|
|
2414
4418
|
runner.sessions.dispose();
|
|
2415
4419
|
subRunner.sessions.dispose();
|
|
4420
|
+
// TOC local LSP: NodeLspManager holds LOCAL child-process language servers — kill them on shutdown so a
|
|
4421
|
+
// restart doesn't orphan a fleet of stdio servers. dispose() isn't on the LspServerManager interface (only the
|
|
4422
|
+
// concrete managers have it), so probe it; the e2b/k8s bridge manager is a harmless no-op if it lacks one.
|
|
2416
4423
|
void lspManager?.dispose?.();
|
|
4424
|
+
// SVC-1: release the File journal's append handle; the SQL twins hold no fd (probe — close isn't on the seam).
|
|
2417
4425
|
void workflowNotifyJournal?.close?.();
|
|
4426
|
+
// core 1.270.1:hardShutdown 终点(drain 已完,in-flight 不再被破坏)全量收割 session 驻留
|
|
4427
|
+
// bg bash(detached 进程组,引擎死后 reparent PID 1 残留=实测;retain-declared 例外在原语内)。
|
|
4428
|
+
// TOB 容器形态同样调=无害且显式;SIGKILL 路径无钩=已知不可救记档。
|
|
2418
4429
|
try {
|
|
2419
4430
|
const reaped = defaultTaskRegistry.reapAllSessionBackground();
|
|
2420
4431
|
if (reaped > 0)
|
|
@@ -2423,16 +4434,30 @@ async function main() {
|
|
|
2423
4434
|
catch (err) {
|
|
2424
4435
|
logger.warn("shutdown_background_reap_failed", { err: String(err) });
|
|
2425
4436
|
}
|
|
2426
|
-
void fleetClient?.stop();
|
|
4437
|
+
void fleetClient?.stop(); // stop heartbeat + flush final usage window + best-effort DELETE deregistration
|
|
2427
4438
|
server.close(() => {
|
|
2428
4439
|
void (backend ? backend.close() : Promise.resolve()).finally(() => process.exit(0));
|
|
2429
4440
|
});
|
|
4441
|
+
// 对抗复查 B-4(HIGH):the exit above only fires after backend.close() SETTLES — a wedged pool teardown
|
|
4442
|
+
// (dead DB, hung socket) kept the "force-terminated" process alive indefinitely (the 10s closeAllConnections
|
|
4443
|
+
// below unblocks server.close(), but nothing bounded backend.close). FINAL deadline: whatever is still
|
|
4444
|
+
// holding after 15s, exit anyway (durable state is already flushed by then — checkpoint/terminal writes
|
|
4445
|
+
// happen before hardShutdown; the pool teardown is best-effort cleanup, not correctness).
|
|
2430
4446
|
const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
|
|
2431
4447
|
finalExit.unref?.();
|
|
4448
|
+
// `server.close()` waits for every connection to end — an open SSE stream (/v1/tasks/stream, /events) would
|
|
4449
|
+
// otherwise block it forever, so pool.end()/exit never run and the process hangs until SIGKILL. Drop idle
|
|
4450
|
+
// keep-alives now, then force-terminate any lingering connections after a grace period so `close()` can
|
|
4451
|
+
// resolve and shutdown completes. The timer is unref'd but still fires while the server holds connections.
|
|
2432
4452
|
server.closeIdleConnections?.();
|
|
2433
4453
|
const forceClose = setTimeout(() => server.closeAllConnections?.(), 10_000);
|
|
2434
4454
|
forceClose.unref?.();
|
|
2435
4455
|
};
|
|
4456
|
+
// SIGTERM = graceful DRAIN — flip `draining` (new billable submits 503+Retry-After, /health carries
|
|
4457
|
+
// draining:true for readiness摘流), then wait for this instance's in-flight legs (live streams + bg/resume) to
|
|
4458
|
+
// finish before the hard shutdown. Bounded by DRAIN_GRACE_MS (default 10min — an interactive turn is minutes;
|
|
4459
|
+
// the previous behavior was a 10s hard-cut that killed long turns). A SECOND SIGTERM or SIGINT (dev Ctrl-C)
|
|
4460
|
+
// skips the wait — k8s sends SIGKILL after terminationGracePeriodSeconds regardless, so the escape hatch is free.
|
|
2436
4461
|
let draining = false;
|
|
2437
4462
|
const drainThenShutdown = () => {
|
|
2438
4463
|
if (closing)
|
|
@@ -2445,14 +4470,17 @@ async function main() {
|
|
|
2445
4470
|
draining = true;
|
|
2446
4471
|
drainState.draining = true;
|
|
2447
4472
|
drainState.since = Date.now();
|
|
2448
|
-
void fleetClient?.announceNow();
|
|
4473
|
+
void fleetClient?.announceNow(); // draining 翻转即刻再 announce — 第一时间自摘流,比任何观测都快
|
|
2449
4474
|
const inflight = drainState.inflight?.() ?? 0;
|
|
2450
4475
|
logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
|
|
2451
4476
|
if (inflight === 0) {
|
|
2452
4477
|
hardShutdown();
|
|
2453
4478
|
return;
|
|
2454
4479
|
}
|
|
4480
|
+
// 修3(三路复审 B3-2 时钟):drain elapsed/截止计算走 performance.now() 单调域(挂钟跳变不得吞掉/拉长
|
|
4481
|
+
// drainGraceMs 窗);对外报告的 drainState.since(/health drainingSince)保留 epoch(Date.now)。
|
|
2455
4482
|
const t0 = performance.now();
|
|
4483
|
+
// NOT unref'd on purpose — this timer IS the shutdown driver (the open connections keep the loop alive anyway).
|
|
2456
4484
|
const tick = setInterval(() => {
|
|
2457
4485
|
const n = drainState.inflight?.() ?? 0;
|
|
2458
4486
|
const elapsed = Math.round(performance.now() - t0);
|
|
@@ -2465,8 +4493,16 @@ async function main() {
|
|
|
2465
4493
|
};
|
|
2466
4494
|
process.on("SIGTERM", drainThenShutdown);
|
|
2467
4495
|
process.on("SIGINT", hardShutdown);
|
|
4496
|
+
// SIGHUP(壳窗口关闭把信号打到整个进程组)≠「没人在用这只引擎」:共享引擎形态(同 config 多壳会话,
|
|
4497
|
+
// 引擎按 T11 防孤儿拍骑在首壳进程组里)下 peer 会话还活着——无条件 drain 会把 peer mid-turn 斩掉(499)
|
|
4498
|
+
// 且窗内新提交 503(B3,壳侧 [771]② 取证)。改 hup-pending 空闲窗:不翻 draining(peer 零感知),
|
|
4499
|
+
// inflight()(bg/resume legs + live streams)连续 sighupIdleGraceMs 为 0 才 hardShutdown,任何在飞 leg
|
|
4500
|
+
// 重置窗口。真孤儿(最后一窗关掉)有界自灭=T11 保留;第二个 SIGHUP 升级走既有 drain 路径(硬梯子)。
|
|
4501
|
+
// session 驻留 bash 的全量收割仍挂 hardShutdown 终点(reapAllSessionBackground)。
|
|
2468
4502
|
process.on("SIGHUP", createSighupIdleHandler({
|
|
2469
4503
|
inflight: () => drainState.inflight?.() ?? 0,
|
|
4504
|
+
// ⚠️ 时基契约(三路复审修3):lastActivityAt 由 server.ts 以 performance.now() 单调域打点,与
|
|
4505
|
+
// sighup-idle.ts 内部的 `now` seam(缺省 performance.now)同域——不得混入 Date.now 值。
|
|
2470
4506
|
lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
|
|
2471
4507
|
isStopped: () => closing || draining,
|
|
2472
4508
|
escalate: drainThenShutdown,
|