@sema-agent/server 1.323.0 → 2.0.1
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/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +995 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -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 +585 -1
- package/dist/config.d.ts +78 -2
- package/dist/config.js +874 -301
- 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 +530 -2129
- 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 +58 -2
- 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
|
@@ -1,111 +1,54 @@
|
|
|
1
|
-
import { readFileSync
|
|
2
|
-
import { setLeafAdvanceListener } from "./session-leaf-bus.js";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
4
2
|
import { homedir } from "node:os";
|
|
5
3
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import { skillContentHash } from "@sema-agent/registry-core";
|
|
8
|
-
import { Runner, TtlSessionStore, uuidv7, FileRosterStore, FileBackgroundAgentStore, FileMailboxStore, defaultTaskRegistry, combinePolicies, createDurableQuestionPolicy, createAllowDenyPolicy, tightenTaskSpec, isThinkingLevel, QUESTION_AWAITS_RESUME, FileWorkflowRunStore, InMemoryWorkflowRunStore, workflowsCapability, NodeLspManager, createFileWorkflowScriptStore, expandTiers, NodeExecutionEnv, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, verifyPromptArtifact, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
|
|
9
|
-
import { PgMemoryEngineBackend, ensurePgMemoryEngineSchema } from "./plugins/memory-engine-pg.js";
|
|
10
|
-
import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "./plugins/memory-engine-tidb.js";
|
|
11
|
-
import { PgMemoryHistoryStore, ensurePgMemoryHistorySchema, PgMemorySyncStore, ensurePgMemorySyncSchema } from "./plugins/memory-sync-store-pg.js";
|
|
12
|
-
import { TiDBMemoryHistoryStore, ensureTiDBMemoryHistorySchema, TiDBMemorySyncStore, ensureTiDBMemorySyncSchema } from "./plugins/memory-sync-store-tidb.js";
|
|
13
|
-
import { resolveTaskModel, modelSupportsImages, matchCatalogModel } from "./model-select.js";
|
|
14
|
-
import { derivePkgSourceEnv, customPkgSourceFromEnv } from "./sandbox-pkg-source.js";
|
|
4
|
+
import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createDurableQuestionPolicy, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
|
|
15
5
|
import { createSessionTitler } from "./session-titler.js";
|
|
16
|
-
import {
|
|
17
|
-
import { selectEnvironmentTool
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { basename, resolve } from "node:path";
|
|
23
|
-
import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
|
|
24
|
-
import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, purgeScratchpadDir, sweepStaleScratchpads, resumeFactsForLane } from "./env-facts.js";
|
|
25
|
-
import { normalizeSuggestNextPrompts, normalizeResilience, normalizeAttachments, normalizeResumeAtMode, resolveTaskLimits, taskAgentsSpecFragment, retainBackgroundProcessesFromBody, toolNameListFromBody, promptProfileFromBody } from "./spec-fields.js";
|
|
26
|
-
import { createBrain, brainSummary } from "./brain.js";
|
|
27
|
-
import { configLkgEnabled, loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
|
|
6
|
+
import { posIntEnv } from "./session-watch.js";
|
|
7
|
+
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
8
|
+
import { subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { brainSummary } from "./brain.js";
|
|
11
|
+
import { loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
|
|
28
12
|
import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
|
|
29
|
-
import {
|
|
30
|
-
import { createSessionStore, ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
|
|
13
|
+
import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
|
|
31
14
|
import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
|
|
32
|
-
import { createStoreBackend, openStoreBackendWithFallback, assertCloudSnapshotBlobPosture } from "./plugins/store-backend.js";
|
|
33
|
-
import { e2bExecutionEnvFactory } from "./plugins/remote-env-e2b.js";
|
|
34
|
-
import { k8sExecutionEnvFactory } from "./plugins/remote-env-k8s.js";
|
|
35
|
-
import { sshExecutionEnvFactory } from "./plugins/remote-env-ssh.js";
|
|
36
|
-
import { adbExecutionEnvFactory } from "./plugins/remote-env-adb.js";
|
|
37
|
-
import { hostExecutionEnvFactory, RemoteHostExecutionEnv } from "./plugins/remote-env-host.js";
|
|
38
15
|
import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver } from "./plugins/web-search.js";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { withWorktreeIsolation, reapOrphanWorktrees } from "./plugins/worktree-isolation.js";
|
|
42
|
-
import { isRemoteScratchpadLane, remoteScratchpadDirFor, withRemoteScratchpad } from "./plugins/remote-scratchpad.js";
|
|
43
|
-
import { createE2bLspManager } from "./lsp/e2b-manager.js";
|
|
44
|
-
import { createAuthorizer, memoryEngineBackendFor, memoryEngineRemoteLanePosture, memorySpecForRequest, HttpError } from "./security.js";
|
|
45
|
-
import { createOaApprovalPolicy, createDurableAskPolicy } from "./approval.js";
|
|
46
|
-
import { buildSessionAuditPg, buildSessionAuditLocal, buildSessionAudit } from "./audit.js";
|
|
47
|
-
import { makeDegenerateInstrument } from "./degenerate-instrument.js";
|
|
48
|
-
import { PlanCacheProbe } from "./plan-cache-probe.js";
|
|
16
|
+
import { createAuthorizer } from "./security.js";
|
|
17
|
+
import { createDurableAskPolicy } from "./approval.js";
|
|
49
18
|
import { loadSkills } from "./capabilities/skills.js";
|
|
50
|
-
import { applyCenterPlugins } from "./capabilities/center-plugins.js";
|
|
51
19
|
import { GiteaClient } from "./capabilities/repo-tools.js";
|
|
52
|
-
import { buildScenarios,
|
|
53
|
-
import { applyLongtailDefer } from "./capabilities/tool-defer.js";
|
|
54
|
-
import { resolveCollabWorkflow, listCollabWorkflows } from "./capabilities/collab-workflows.js";
|
|
20
|
+
import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
|
|
55
21
|
import { createLogger } from "./observability/logger.js";
|
|
56
|
-
import { createSighupIdleHandler } from "./sighup-idle.js";
|
|
57
22
|
import { createToolTracer, createPermissionDeniedMeter } from "./observability/tool-trace.js";
|
|
58
23
|
import { createRegistryJwtVerifier } from "./auth-bridge.js";
|
|
59
24
|
import { createMetrics } from "./observability/metrics.js";
|
|
60
25
|
import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
|
|
61
26
|
import { RateLimiter } from "./observability/rate-limit.js";
|
|
62
|
-
import { createHttpServer, explicitOperator
|
|
27
|
+
import { createHttpServer, explicitOperator } from "./http/server.js";
|
|
63
28
|
import { exportMemoryScope } from "./memory-export.js";
|
|
64
29
|
import { performMemorySync } from "./memory-sync.js";
|
|
65
|
-
import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
|
|
66
|
-
import { createLeaderEndpoint } from "./leader/endpoint.js";
|
|
67
|
-
import { createLeaderRunner } from "./leader/wire.js";
|
|
68
|
-
import { WorkflowNotifyGate, JournalingWorkflowRunStore, FileWorkflowNotifyJournalStore, } from "./orchestration/workflow-notify-journal.js";
|
|
69
|
-
import { FileWorkflowCompletionInbox, InMemoryWorkflowCompletionInbox, resolveCompletionRoute, resolveServedSession, } from "./orchestration/workflow-completion-inbox.js";
|
|
70
|
-
import { buildPricing, createTracer, createSideQueryAccountant, cappedCeiling, ModelUsageTracker, PromptManifestTracker } from "./budget.js";
|
|
71
|
-
import { CostQuota } from "./observability/cost-quota.js";
|
|
72
30
|
import { startOtlpExporter } from "./observability/otel-exporter.js";
|
|
73
31
|
import { HEARTBEAT_MS, backgroundAgentOutput, taskHandleOutput, taskHandleStop } from "./runs.js";
|
|
74
|
-
import {
|
|
75
|
-
import {
|
|
76
|
-
import { ToolApprovalCoordinator } from "./tool-approval.js";
|
|
77
|
-
import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, mcpForScenario, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "./sema-registry.js";
|
|
78
|
-
import { ensureSealedKeyStore, reportExecutionPublicKey } from "./sealed-key.js";
|
|
79
|
-
import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "./config-provider.js";
|
|
80
|
-
import { validatePromptsDomain, centerPromptProvider, centerIdentityAssembled, applyCatalogToSource, CORE_ENGINE_VERSION, withPromptArtifactBackfill } from "./capabilities/center-prompts.js";
|
|
81
|
-
import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "./config-lkg.js";
|
|
82
|
-
import { TiDBRosterStore, PgRosterStore, ensureTiDBRosterSchema, ensurePgRosterSchema } from "./plugins/roster-store-sql.js";
|
|
83
|
-
import { TiDBTaskAttachmentStore, PgTaskAttachmentStore, ensureTiDBTaskAttachmentSchema, ensurePgTaskAttachmentSchema, bindAttachmentsForTask, materializeAttachmentsInto } from "./plugins/task-attachment-store.js";
|
|
84
|
-
import { LocalTaskAttachmentStore } from "./plugins/local-task-attachment-store.js";
|
|
85
|
-
import { MinioBlobBackend, SQL_BLOB_DEFAULT_MAX_BYTES } from "./plugins/blob-backend.js";
|
|
86
|
-
import { TiDBBackgroundAgentStore, PgBackgroundAgentStore, ensureTiDBBackgroundAgentSchema, ensurePgBackgroundAgentSchema } from "./plugins/background-agent-store-sql.js";
|
|
87
|
-
import { TiDBMailboxStore, PgMailboxStore, ensureTiDBMailboxSchema, ensurePgMailboxSchema } from "./plugins/mailbox-store-sql.js";
|
|
88
|
-
import { createPrincipalEntitlementsClient, gateExecutionLane, scopedTokenNeedsWorker, applyObserverEnvOptIn } from "./runtime-caps-resolver.js";
|
|
89
|
-
import { applyRuntimeGovernance, stripDelegationTools } from "./runtime-governance.js";
|
|
90
|
-
import { parseTaskSettings, applyTaskSettings, coercePermissionMode, withPermissionMode, effectiveThinking, acceptAppendSystemPrompt, hasConstitutionAnchors, providerDropsAppend, MAX_SETTINGS_OUTPUT_STYLE_CHARS } from "./task-settings.js";
|
|
91
|
-
import { createTaskHooks, composeHooks } from "./hooks/hook-runner.js";
|
|
32
|
+
import { SQL_BLOB_DEFAULT_MAX_BYTES } from "./plugins/blob-backend.js";
|
|
33
|
+
import { composeHooks } from "./hooks/hook-runner.js";
|
|
92
34
|
import { createHookLlm } from "./hooks/hook-llm.js";
|
|
93
|
-
import {
|
|
94
|
-
import { FleetUsageAccumulator, startFleetClientFromEnv } from "./fleet-client.js";
|
|
95
|
-
import { createFleetLeaseFromEnv } from "./fleet-lease.js";
|
|
35
|
+
import { startFleetClientFromEnv } from "./fleet-client.js";
|
|
96
36
|
import { reclaimOrphansAtBoot } from "./boot-reclaim.js";
|
|
97
|
-
import {
|
|
98
|
-
import {
|
|
99
|
-
import {
|
|
100
|
-
import {
|
|
101
|
-
import {
|
|
102
|
-
import {
|
|
103
|
-
import {
|
|
104
|
-
import {
|
|
105
|
-
import {
|
|
106
|
-
import {
|
|
107
|
-
import {
|
|
108
|
-
import {
|
|
37
|
+
import { createConfigCenterRuntime } from "./boot/config-center.js";
|
|
38
|
+
import { createResolveSpec } from "./boot/resolve-spec.js";
|
|
39
|
+
import { startReapers } from "./boot/reapers.js";
|
|
40
|
+
import { openStores } from "./boot/stores.js";
|
|
41
|
+
import { createBudgetAndTracing } from "./boot/budget-tracing.js";
|
|
42
|
+
import { createExecutionEnv } from "./boot/execution-env.js";
|
|
43
|
+
import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
|
|
44
|
+
import { createLiveCoordinators } from "./boot/coordinators.js";
|
|
45
|
+
import { createRuntimeCaps } from "./boot/runtime-caps.js";
|
|
46
|
+
import { createRunnerDeps, createSharedRunnerDeps } from "./boot/runner-deps.js";
|
|
47
|
+
import { createSessionFaces } from "./boot/session-faces.js";
|
|
48
|
+
import { createLeaderFace } from "./boot/leader.js";
|
|
49
|
+
import { installShutdownHandlers } from "./boot/shutdown.js";
|
|
50
|
+
/** Service version for GET /v1/capabilities — read from the package.json shipped next to src/ in the
|
|
51
|
+
* image (Dockerfile COPYs it). Best-effort: an unreadable file reports "unknown", never crashes boot. */
|
|
109
52
|
function serviceVersion() {
|
|
110
53
|
try {
|
|
111
54
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -115,1033 +58,146 @@ function serviceVersion() {
|
|
|
115
58
|
return "unknown";
|
|
116
59
|
}
|
|
117
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Service entrypoint. **装配次序表**(design/158 A10)。
|
|
63
|
+
*
|
|
64
|
+
* 各 boot 段已按内聚下沉到 `src/boot/*.ts` 叶子(纯搬运,逐字);本函数保留的是**顺序本身**——
|
|
65
|
+
* 谁先谁后是行为(env 读取时序、诊断排空点、信号注册时点、工厂装饰顺序),每个叶子的文件头
|
|
66
|
+
* 都写明了它那条「位置即契约」。当前次序:
|
|
67
|
+
*
|
|
68
|
+
* loadConfig → logger/metrics → 诊断排空(logConfigDiagnostics + drainNumEnvWarnings)
|
|
69
|
+
* → boot/config-center(sealed-key/provider/prompt-epoch 店/effective 拉取/LKG 兜底/tolerant apply)
|
|
70
|
+
* → boot/config-center.applyLocalRemoteExec(local 形 config.d/remote-exec.json 覆写)
|
|
71
|
+
* → boot/stores(StoreBackend + memory + 四个 durable 店 + sessionStore + 拒启断言)
|
|
72
|
+
* → boot/budget-tracing(brain/pricing/quota/tracer)
|
|
73
|
+
* → boot/execution-env(remote-exec 工厂链 + LSP)
|
|
74
|
+
* → toolTracer/deploymentHooks
|
|
75
|
+
* → boot/workflow-orchestration → boot/coordinators → boot/runtime-caps
|
|
76
|
+
* → boot/runner-deps → new Runner → hook LLM/agent → subRunner
|
|
77
|
+
* → runStore/anchors/approval 面 → skills → boot/config-center.applyCenterCapabilities
|
|
78
|
+
* (center skills/plugins/boot-LKG/mcp)→ buildScenarios → .applyCenterScenarios
|
|
79
|
+
* → boot/reapers → OTLP → boot/config-center.startRefreshLoop → .initKeyResolver
|
|
80
|
+
* → boot/session-faces → boot/leader → ServiceDeps 七组 → boot/resolve-spec
|
|
81
|
+
* → createHttpServer → listen → fleet client → boot/shutdown(必须最后:信号注册时点即契约)
|
|
82
|
+
*
|
|
83
|
+
* A10 尾刀(config-center):`ccEtag/effective/latestEffective/appliedPlaneEff/planeDeferredNoHandoff/
|
|
84
|
+
* pendingRestart/bootSkillStale/keyResolver/centerPrompts/…` 这组跨段可变状态,已整体搬进
|
|
85
|
+
* `boot/config-center.ts` 那只工厂的**闭包**(仍是 `let`,每一处读写的文本与次序逐字未改 ——
|
|
86
|
+
* codex R10–R38 的重启风暴判据全部按原样保留);main 只经窄口读:`getKeyResolver/getCenterPrompts/
|
|
87
|
+
* modelReady/restartState/planeDeferredState/promptSource`,与本目录既有的晚绑 getter 惯例同款。
|
|
88
|
+
*
|
|
89
|
+
* Wires the three planes:
|
|
90
|
+
* Brain = external LLM gateway (createBrain)
|
|
91
|
+
* Session = pluggable store (TiDB durable event-log center, or in-memory)
|
|
92
|
+
* Hands = tools/MCP injected per request in resolveSpec (deployment-owned)
|
|
93
|
+
* Persistence (L1 sessions + L2 memory) lives in the external TiDB center; this container is stateless.
|
|
94
|
+
*/
|
|
118
95
|
async function main() {
|
|
119
96
|
const config = loadConfig();
|
|
97
|
+
// Liveness invariant: the running instance refreshes updated_at every HEARTBEAT_MS; the reaper fails
|
|
98
|
+
// a run whose updated_at is older than runStaleSec. If the stale window were ≤ the heartbeat, the
|
|
99
|
+
// reaper would race a perfectly alive run. Fail fast on a misconfiguration instead of reaping live work.
|
|
120
100
|
if (config.runStaleSec * 1000 <= HEARTBEAT_MS) {
|
|
121
101
|
throw new Error(`REAP_RUN_STALE_SEC (${config.runStaleSec}s) must exceed the ${HEARTBEAT_MS / 1000}s liveness heartbeat`);
|
|
122
102
|
}
|
|
123
103
|
const logger = createLogger(config.logLevel);
|
|
124
104
|
const metrics = createMetrics();
|
|
105
|
+
// S20 (SILENT-FALLBACK P1) + [792]④ boot notices: config parsing runs before the logger exists — drain both
|
|
106
|
+
// pre-logger collectors via the shared helper (F10: run-local uses the same one, so entrypoints can't drift).
|
|
125
107
|
logConfigDiagnostics(logger);
|
|
108
|
+
// design/158 B4 交接小件:plugins 数值旋钮(HOST_BG_*/K8S_BG_*/E2B_*,模块级 import 时读)的非法
|
|
109
|
+
// 字面量告警——通道在 remote-shell.ts 自带(不造 plugins→config 依赖边),与上面同一 boot 诊断点排空。
|
|
126
110
|
for (const w of drainNumEnvWarnings())
|
|
127
111
|
logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
|
|
112
|
+
// M1(接入审计 2026-07-08):core verify 契约=verifier 必须异于 implementer(去相关性是 🔴 部署契约)。
|
|
113
|
+
// verifier 角色落 "default" 时对抗验证者=实现者自评自批——比无验证更糟(假安全感)。曝光而非静默:
|
|
114
|
+
// 单模型部署配 MODEL_VERIFIER 指异构模型即消警(typo/不在 catalog 同样落到这里)。
|
|
128
115
|
if ((config.roles?.verifier ?? "default") === "default") {
|
|
129
116
|
logger.warn("verify_decorrelation_unavailable", {
|
|
130
117
|
hint: "verifier role = default (= implementer) — adversarial verification grades its own work; set MODEL_VERIFIER=<catalog model id> to a heterogeneous model",
|
|
131
118
|
...(process.env.MODEL_VERIFIER ? { rejected: process.env.MODEL_VERIFIER, reason: "not in catalog" } : {}),
|
|
132
119
|
});
|
|
133
120
|
}
|
|
121
|
+
// S24: redaction rewrites by pattern; LOW: web-search non-array payloads by provider.
|
|
134
122
|
setRedactionObserver((pattern, n) => metrics.inc("redactions_applied_total", { pattern }, n));
|
|
135
123
|
setWebSearchBadPayloadObserver((provider) => {
|
|
136
124
|
metrics.inc("web_search_bad_payload_total", { provider });
|
|
137
125
|
logger.warn("web_search_non_array_payload", { provider });
|
|
138
126
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
let appliedPlaneEff;
|
|
144
|
-
let planeDeferredNoHandoff;
|
|
145
|
-
let bootSkillStale = false;
|
|
146
|
-
let pendingRestart;
|
|
147
|
-
const cc = config.configCenter;
|
|
127
|
+
// 🔴 ONE local root for BOTH the config provider (models/skills/mcp) AND the remoteExec loader (avoids a
|
|
128
|
+
// split-brain — createConfigProvider defaulted to process.cwd() while loadRemoteExec used
|
|
129
|
+
// AGENT_DATA_DIR, so config.d and remote-exec.json could resolve to DIFFERENT directories). run-local already
|
|
130
|
+
// computes this once; main.ts now does too. Shared with the config-center runtime and every boot leaf below.
|
|
148
131
|
const localRoot = config.configLocalDir ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
: undefined;
|
|
170
|
-
const modelReadyState = { ready: !(cc && !cc.dryRun) || Boolean(process.env.MODEL_ID) };
|
|
171
|
-
const markRosterLanded = (eff) => {
|
|
172
|
-
if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
|
|
173
|
-
modelReadyState.ready = true;
|
|
174
|
-
logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
let centerPrompts;
|
|
178
|
-
let pendingCatalogFaces;
|
|
179
|
-
const retryPendingCatalog = async (phase) => {
|
|
180
|
-
if (!pendingCatalogFaces)
|
|
181
|
-
return;
|
|
182
|
-
const { target } = pendingCatalogFaces;
|
|
183
|
-
if (await applyCatalogToSource(promptSource, target, centerPrompts, logger, phase)) {
|
|
184
|
-
centerPrompts = target;
|
|
185
|
-
pendingCatalogFaces = undefined;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
|
|
189
|
-
const promptStateDir = join(promptEpochDir, "state", cc?.worker === undefined ? "global" : `worker-${createHash("sha256").update(cc.worker).digest("hex").slice(0, 16)}`);
|
|
190
|
-
const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc.baseUrl, cc.token, d), logger) : inner;
|
|
191
|
-
const promptSource = (() => {
|
|
192
|
-
try {
|
|
193
|
-
return new CenterPromptSource({
|
|
194
|
-
store: withByDigestBackfill(new FilePromptArtifactStore(promptEpochDir)),
|
|
195
|
-
stateStore: new FilePromptSourceStateStore(promptStateDir),
|
|
196
|
-
engineVersion: CORE_ENGINE_VERSION,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
catch (e) {
|
|
200
|
-
logger.warn("center_prompts_store_unwritable", { dir: promptEpochDir, err: String(e), note: "file-backed prompt-epoch store unavailable — memory fallback (catalog candidate not restart-durable); built-ins/declaration axis unaffected" });
|
|
201
|
-
return new CenterPromptSource({ store: withByDigestBackfill(new MemoryPromptArtifactStore()), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
|
|
202
|
-
}
|
|
203
|
-
})();
|
|
204
|
-
if (configProvider && !cc?.dryRun) {
|
|
205
|
-
try {
|
|
206
|
-
await promptSource.restore();
|
|
207
|
-
}
|
|
208
|
-
catch (e) {
|
|
209
|
-
logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "disk LKG unavailable — catalog candidate absent until the next adopt; declaration axis and built-ins unaffected" });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const adoptCenterPrompts = async (eff, phase) => {
|
|
213
|
-
const raw = eff?.prompts;
|
|
214
|
-
if (raw === undefined) {
|
|
215
|
-
if (centerPrompts)
|
|
216
|
-
logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
|
|
217
|
-
const cleared = await applyCatalogToSource(promptSource, undefined, centerPrompts, logger, phase);
|
|
218
|
-
if (cleared) {
|
|
219
|
-
centerPrompts = undefined;
|
|
220
|
-
pendingCatalogFaces = undefined;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
pendingCatalogFaces = { target: undefined };
|
|
224
|
-
}
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const v = validatePromptsDomain(raw);
|
|
228
|
-
if (!v.ok) {
|
|
229
|
-
logger.warn("center_prompts_invalid", { phase, error: v.error, kept: centerPrompts?.declarations?.packId ?? (centerPrompts?.catalog ? "(catalog)" : "(builtin)"), note: "malformed prompts face rejected (core assemble would THROW per task) — keeping the previous pack/built-ins; fix the center payload" });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (centerPrompts?.identity !== v.value.identity) {
|
|
233
|
-
logger.info("center_prompts_adopted", {
|
|
234
|
-
phase,
|
|
235
|
-
axes: v.value.axes,
|
|
236
|
-
...(v.value.declarations
|
|
237
|
-
? { packId: v.value.declarations.packId, contentDigest: v.value.declarations.contentDigest, sections: v.value.declarations.sections.length, scenarioOverrides: Object.keys(v.value.declarations.scenarioOverrides ?? {}).length }
|
|
238
|
-
: {}),
|
|
239
|
-
...(v.value.catalog ? { artifactDigest: v.value.catalog.artifact.artifactDigest, catalogDigest: v.value.catalog.artifact.payload.catalogDigest, sourceRevision: v.value.catalog.sourceRevision } : {}),
|
|
240
|
-
note: "new tasks assemble the new pack (epoch re-pins at the next task boundary); in-flight sessions stay pinned",
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
const transitioned = await applyCatalogToSource(promptSource, v.value, centerPrompts, logger, phase);
|
|
244
|
-
if (transitioned) {
|
|
245
|
-
centerPrompts = v.value;
|
|
246
|
-
pendingCatalogFaces = undefined;
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
pendingCatalogFaces = { target: v.value };
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
let bootConfigPending;
|
|
253
|
-
const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
|
|
254
|
-
const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
|
|
255
|
-
const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
|
|
256
|
-
let lkgBooted = false;
|
|
257
|
-
let bootLkgCandidate;
|
|
258
|
-
let lastPrewarmBodiesVerified = false;
|
|
259
|
-
const persistLkgDurable = async (effectiveObj, etag) => {
|
|
260
|
-
lastPrewarmBodiesVerified = false;
|
|
261
|
-
if (configProvider?.kind === "local")
|
|
262
|
-
return true;
|
|
263
|
-
if (!lkgEnabled)
|
|
264
|
-
return false;
|
|
265
|
-
try {
|
|
266
|
-
if (effectiveObj.skills && config.configCenter) {
|
|
267
|
-
await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
|
|
268
|
-
}
|
|
269
|
-
const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
|
|
270
|
-
const cacheDir = defaultSkillCacheDir();
|
|
271
|
-
for (const m of enabledSkills) {
|
|
272
|
-
const hex = m.contentHash.replace(/^sha256:/, "").toLowerCase();
|
|
273
|
-
if (!/^[0-9a-f]{64}$/.test(hex))
|
|
274
|
-
return false;
|
|
275
|
-
try {
|
|
276
|
-
const text = await fsReadFile(join(cacheDir, hex), "utf8");
|
|
277
|
-
if (`sha256:${createHash("sha256").update(text, "utf8").digest("hex")}` !== `sha256:${hex}`) {
|
|
278
|
-
logger.warn("config_lkg_skill_cache_incomplete", { skill: m.name, hash: m.contentHash, note: "cached body fails its hash — LKG NOT replaced, handoff NOT confirmed (the previous good LKG keeps serving offline boots)" });
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
logger.warn("config_lkg_skill_cache_incomplete", { skill: m.name, hash: m.contentHash, note: "enabled skill body not in the cache — LKG NOT replaced, handoff NOT confirmed" });
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
lastPrewarmBodiesVerified = true;
|
|
288
|
-
await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
|
|
289
|
-
return lkgSurvivesRestart;
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "durable persist failed — restart signal proceeds anyway (a dead disk + dead center double-fault can re-loop; fix the disk)" });
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
if (configProvider) {
|
|
297
|
-
const dryRun = cc?.dryRun ?? false;
|
|
298
|
-
const bootFromLkg = async (why) => {
|
|
299
|
-
if (!lkgEnabled || dryRun)
|
|
300
|
-
return null;
|
|
301
|
-
const read = await loadLkg(lkgPath, cc?.worker);
|
|
302
|
-
if (!read.lkg) {
|
|
303
|
-
if (read.reason !== "absent")
|
|
304
|
-
logger.warn("config_lkg_unusable", { path: lkgPath, reason: read.reason, note: "ignoring the on-disk LKG — env fallback serves; the cache rewrites on the next clean candidate" });
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
logger.info("config_loaded_lkg", { path: lkgPath, savedAt: read.lkg.savedAt, why, note: "booting from the persisted last-known-good (all restart-to-apply faces included) — the live pull continues in the background and reconciles on arrival" });
|
|
308
|
-
return { effective: read.lkg.effective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
|
|
309
|
-
};
|
|
310
|
-
const bootApplyLkgInline = async (lkgR) => {
|
|
311
|
-
applyEffective(config, lkgR.effective, logger, { sealedKeys });
|
|
312
|
-
await adoptCenterPrompts(lkgR.effective, "boot-lkg");
|
|
313
|
-
markRosterLanded(lkgR.effective);
|
|
314
|
-
effective = lkgR.effective;
|
|
315
|
-
appliedPlaneEff = lkgR.effective;
|
|
316
|
-
latestEffective = lkgR.effective;
|
|
317
|
-
if (lkgR.etag !== undefined)
|
|
318
|
-
ccEtag = lkgR.etag;
|
|
319
|
-
const cfgModels = (lkgR.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
|
|
320
|
-
logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
|
|
321
|
-
};
|
|
322
|
-
try {
|
|
323
|
-
const bootFetch = configProvider.fetchEffective(undefined);
|
|
324
|
-
const raced = await raceBootFetch(bootFetch, config.configBootFetchBudgetMs);
|
|
325
|
-
let lkgFallback = null;
|
|
326
|
-
if (raced === BOOT_FETCH_DEFERRED) {
|
|
327
|
-
bootConfigPending = bootFetch;
|
|
328
|
-
bootFetch.catch(() => { });
|
|
329
|
-
lkgFallback = await bootFromLkg("boot fetch deferred (budget exceeded)");
|
|
330
|
-
lkgBooted = lkgFallback !== null;
|
|
331
|
-
logger.warn("config_boot_fetch_deferred", {
|
|
332
|
-
source: configProvider.kind,
|
|
333
|
-
budgetMs: config.configBootFetchBudgetMs,
|
|
334
|
-
lkg: lkgBooted,
|
|
335
|
-
note: lkgBooted
|
|
336
|
-
? "center slow — booting from the persisted LKG; the pull continues in the background and reconciles via the refresh path on arrival"
|
|
337
|
-
: "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
|
-
});
|
|
339
|
-
}
|
|
340
|
-
if (raced === BOOT_FETCH_DEFERRED && lkgFallback) {
|
|
341
|
-
try {
|
|
342
|
-
await bootApplyLkgInline(lkgFallback);
|
|
343
|
-
}
|
|
344
|
-
catch (applyErr) {
|
|
345
|
-
lkgBooted = false;
|
|
346
|
-
logger.warn("config_lkg_apply_failed", { err: String(applyErr), note: "persisted LKG failed to apply — env fallback serves; the cache rewrites on the next clean candidate" });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
|
|
350
|
-
if (r) {
|
|
351
|
-
const bootPromptsRaw = r.effective.prompts;
|
|
352
|
-
const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
|
|
353
|
-
const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
|
|
354
|
-
if (bootClean)
|
|
355
|
-
ccEtag = r.etag;
|
|
356
|
-
for (const de of r.domainErrors ?? [])
|
|
357
|
-
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
|
-
if (dryRun) {
|
|
359
|
-
logEffectiveDiff(config, r.effective, logger);
|
|
360
|
-
}
|
|
361
|
-
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;
|
|
367
|
-
if (bootClean)
|
|
368
|
-
latestEffective = r.effective;
|
|
369
|
-
if (bootClean && !lkgBooted)
|
|
370
|
-
bootLkgCandidate = { effective: r.effective, etag: r.etag };
|
|
371
|
-
const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
|
|
372
|
-
const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
|
|
373
|
-
logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
|
|
374
|
-
if (configProvider.kind === "local" && cfgModels === 0)
|
|
375
|
-
logger.warn("config_local_no_models", { ...cfgDir, hint: "config.d/models.json 空或 CONFIG_LOCAL_DIR 指错目录(选目录旋钮是 CONFIG_LOCAL_DIR,不是 LOCAL_DATA_ROOT)— 模型继续走 env 兜底" });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
catch (err) {
|
|
380
|
-
logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
|
|
381
|
-
const lkgFallback = await bootFromLkg(`boot fetch failed: ${String(err).slice(0, 120)}`);
|
|
382
|
-
if (lkgFallback) {
|
|
383
|
-
lkgBooted = true;
|
|
384
|
-
try {
|
|
385
|
-
await bootApplyLkgInline(lkgFallback);
|
|
386
|
-
}
|
|
387
|
-
catch (applyErr) {
|
|
388
|
-
lkgBooted = false;
|
|
389
|
-
logger.warn("config_lkg_apply_failed", { err: String(applyErr), note: "persisted LKG failed to apply — env fallback serves" });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
if (!modelReadyState.ready)
|
|
395
|
-
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" });
|
|
396
|
-
if (configProvider?.kind === "local") {
|
|
397
|
-
try {
|
|
398
|
-
const fileRx = await loadRemoteExec(localRoot);
|
|
399
|
-
if (fileRx) {
|
|
400
|
-
if (fileRx.provider === "host") {
|
|
401
|
-
config.remoteExec = {
|
|
402
|
-
provider: "host",
|
|
403
|
-
...(fileRx.workdir ? { workspaceBase: fileRx.workdir } : {}),
|
|
404
|
-
...(fileRx.commandTimeoutMs != null ? { commandTimeoutMs: fileRx.commandTimeoutMs } : {}),
|
|
405
|
-
};
|
|
406
|
-
logger.info("remote_exec_from_file", { provider: "host", root: localRoot });
|
|
407
|
-
}
|
|
408
|
-
else if (fileRx.provider === "local-docker") {
|
|
409
|
-
config.remoteExec = {
|
|
410
|
-
provider: "local-docker",
|
|
411
|
-
image: fileRx.image,
|
|
412
|
-
...(fileRx.mountPath ? { mountPath: fileRx.mountPath } : fileRx.workdir ? { mountPath: fileRx.workdir } : {}),
|
|
413
|
-
...(fileRx.memory ? { memory: fileRx.memory } : {}),
|
|
414
|
-
...(fileRx.cpus != null ? { cpus: fileRx.cpus } : {}),
|
|
415
|
-
...(fileRx.network ? { network: fileRx.network } : {}),
|
|
416
|
-
};
|
|
417
|
-
logger.info("remote_exec_from_file", { provider: "local-docker", root: localRoot, image: fileRx.image });
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
logger.warn("remote_exec_file_isolated_lane_unwired", { provider: fileRx.provider, note: "e2b/k8s/ssh/adb are sourced from REMOTE_EXEC env until file-source NAME→value resolution lands (center field map)" });
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
logger.error("remote_exec_file_invalid", { root: localRoot, err: String(err) });
|
|
426
|
-
throw err;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
let backend;
|
|
430
|
-
let storeBackendDegraded = false;
|
|
431
|
-
const wantDb = config.sessionBackend === "tidb" ||
|
|
432
|
-
config.dbBackend === "local" ||
|
|
433
|
-
(config.sessionBackend === "auto" && !!(config.tidb || config.pg));
|
|
434
|
-
if (wantDb) {
|
|
435
|
-
const opened = await openStoreBackendWithFallback(config, logger);
|
|
436
|
-
backend = opened.backend;
|
|
437
|
-
storeBackendDegraded = opened.degraded;
|
|
438
|
-
}
|
|
439
|
-
metrics.setGauge("store_backend_degraded", storeBackendDegraded ? 1 : 0);
|
|
440
|
-
{
|
|
441
|
-
const minioReq = ["MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"];
|
|
442
|
-
const present = minioReq.filter((v) => (process.env[v] ?? "") !== "");
|
|
443
|
-
if (present.length > 0 && !config.snapshotBlobStore) {
|
|
444
|
-
logger.warn("snapshot_blob_store_partial_config_fallback_sql", { present, missing: minioReq.filter((v) => !present.includes(v)) });
|
|
445
|
-
}
|
|
446
|
-
metrics.setGauge("snapshot_blob_backend", 1, { backend: config.snapshotBlobStore ? "minio" : "sql" });
|
|
447
|
-
}
|
|
448
|
-
if (config.sessionBackend === "auto")
|
|
449
|
-
config.sessionBackend = backend ? "tidb" : "memory";
|
|
450
|
-
if (config.dbBackend === "local" && backend)
|
|
451
|
-
config.sessionBackend = "tidb";
|
|
452
|
-
let memoryEngine;
|
|
453
|
-
let memorySyncCursors;
|
|
454
|
-
if (config.memoryEngineBackend !== "file" && config.memoryEngineEnabled) {
|
|
455
|
-
const dialect = config.memoryEngineBackend;
|
|
456
|
-
const countedHistorySink = (store) => ({
|
|
457
|
-
appendHistory: async (rows) => {
|
|
458
|
-
try {
|
|
459
|
-
await store.appendHistory(rows);
|
|
460
|
-
}
|
|
461
|
-
catch (err) {
|
|
462
|
-
metrics.inc("memory_history_append_failed_total", { backend: dialect }, rows.length);
|
|
463
|
-
throw err;
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
if (dialect === "pg") {
|
|
468
|
-
const pool = backend?.pgPool();
|
|
469
|
-
if (!pool)
|
|
470
|
-
throw new Error("MEMORY_ENGINE_BACKEND=pg requires DB_BACKEND=pg (the memory engine binds the same PG pool) — refusing to start half-configured");
|
|
471
|
-
const q = async (text, params) => { const r = await pool.query(text, params); return { rows: r.rows }; };
|
|
472
|
-
await ensurePgMemoryEngineSchema(q);
|
|
473
|
-
await ensurePgMemoryHistorySchema(q);
|
|
474
|
-
await ensurePgMemorySyncSchema(q);
|
|
475
|
-
memorySyncCursors = new PgMemorySyncStore(q);
|
|
476
|
-
const pgMem = new PgMemoryEngineBackend(q, { history: countedHistorySink(new PgMemoryHistoryStore(q)) });
|
|
477
|
-
memoryEngine = { backend: pgMem, root: join(config.localDataRoot ?? localRoot, "memory-work") };
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
const pool = backend?.mysqlPool();
|
|
481
|
-
if (!pool)
|
|
482
|
-
throw new Error("MEMORY_ENGINE_BACKEND=tidb requires DB_BACKEND=mysql/tidb — refusing to start half-configured");
|
|
483
|
-
await ensureTiDBMemoryEngineSchema(pool);
|
|
484
|
-
await ensureTiDBMemoryHistorySchema(pool);
|
|
485
|
-
await ensureTiDBMemorySyncSchema(pool);
|
|
486
|
-
memorySyncCursors = new TiDBMemorySyncStore(pool);
|
|
487
|
-
memoryEngine = { backend: new TiDBMemoryEngineBackend(pool, { history: countedHistorySink(new TiDBMemoryHistoryStore(pool)) }), root: join(config.localDataRoot ?? localRoot, "memory-work") };
|
|
488
|
-
}
|
|
489
|
-
logger.info("memory_engine_enabled", { enabled: true, backend: dialect, multiTenant: config.requirePrincipal === true, workRoot: memoryEngine.root });
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
memoryEngine = memoryEngineBackendFor(config);
|
|
493
|
-
if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
|
|
494
|
-
logger.warn("memory_engine_dir_defaulted", {
|
|
495
|
-
root: memoryEngine.root,
|
|
496
|
-
hint: "LOCAL_DATA_ROOT is set but MEMORY_ENGINE_DIR is not — the memory engine is sharing the machine-wide default; set MEMORY_ENGINE_DIR to isolate it under your data root",
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
let rosterStore;
|
|
501
|
-
{
|
|
502
|
-
const mysqlPool = backend?.mysqlPool?.();
|
|
503
|
-
const pgPool = backend?.pgPool?.();
|
|
504
|
-
if (pgPool) {
|
|
505
|
-
const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
|
|
506
|
-
await ensurePgRosterSchema(q);
|
|
507
|
-
rosterStore = new PgRosterStore(q);
|
|
508
|
-
}
|
|
509
|
-
else if (mysqlPool) {
|
|
510
|
-
await ensureTiDBRosterSchema(mysqlPool);
|
|
511
|
-
rosterStore = new TiDBRosterStore(mysqlPool);
|
|
512
|
-
}
|
|
513
|
-
else if (backend?.kind === "local") {
|
|
514
|
-
rosterStore = new FileRosterStore(join(config.localDataRoot ?? localRoot, "roster.json"));
|
|
515
|
-
}
|
|
516
|
-
if (rosterStore)
|
|
517
|
-
logger.info("roster_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
518
|
-
}
|
|
519
|
-
let backgroundAgentStore;
|
|
520
|
-
{
|
|
521
|
-
const mysqlPool = backend?.mysqlPool?.();
|
|
522
|
-
const pgPool = backend?.pgPool?.();
|
|
523
|
-
if (pgPool) {
|
|
524
|
-
const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
|
|
525
|
-
await ensurePgBackgroundAgentSchema(q);
|
|
526
|
-
backgroundAgentStore = new PgBackgroundAgentStore(q);
|
|
527
|
-
}
|
|
528
|
-
else if (mysqlPool) {
|
|
529
|
-
await ensureTiDBBackgroundAgentSchema(mysqlPool);
|
|
530
|
-
backgroundAgentStore = new TiDBBackgroundAgentStore(mysqlPool);
|
|
531
|
-
}
|
|
532
|
-
else if (backend?.kind === "local") {
|
|
533
|
-
backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot);
|
|
534
|
-
}
|
|
535
|
-
if (backgroundAgentStore)
|
|
536
|
-
logger.info("background_agent_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
537
|
-
}
|
|
538
|
-
let taskAttachmentStore;
|
|
539
|
-
{
|
|
540
|
-
const mysqlPool = backend?.mysqlPool?.();
|
|
541
|
-
const pgPool = backend?.pgPool?.();
|
|
542
|
-
if (pgPool || mysqlPool) {
|
|
543
|
-
const minio = config.snapshotBlobStore;
|
|
544
|
-
if (!minio) {
|
|
545
|
-
logger.error("attachments_disabled_object_store_required", {
|
|
546
|
-
hint: "cloud deployments must configure object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) — attachment routes will 501 until it is set",
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
const bytes = new MinioBlobBackend({ ...minio, keyPrefix: `${minio.keyPrefix ?? ""}attachments/` });
|
|
551
|
-
if (pgPool) {
|
|
552
|
-
const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
|
|
553
|
-
await ensurePgTaskAttachmentSchema(q);
|
|
554
|
-
taskAttachmentStore = new PgTaskAttachmentStore(q, bytes);
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
await ensureTiDBTaskAttachmentSchema(mysqlPool);
|
|
558
|
-
taskAttachmentStore = new TiDBTaskAttachmentStore(mysqlPool, bytes);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
else if (backend?.kind === "local") {
|
|
563
|
-
taskAttachmentStore = new LocalTaskAttachmentStore(config.localDataRoot ?? localRoot);
|
|
564
|
-
}
|
|
565
|
-
if (taskAttachmentStore)
|
|
566
|
-
logger.info("task_attachment_store_enabled", { backend: pgPool ? "pg+minio" : mysqlPool ? "tidb+minio" : "file", maxBytes: config.attachmentMaxBytes });
|
|
567
|
-
}
|
|
568
|
-
let mailboxStore;
|
|
569
|
-
{
|
|
570
|
-
const mysqlPool = backend?.mysqlPool?.();
|
|
571
|
-
const pgPool = backend?.pgPool?.();
|
|
572
|
-
if (pgPool) {
|
|
573
|
-
await ensurePgMailboxSchema(async (text, params) => pgPool.query(text, params));
|
|
574
|
-
mailboxStore = new PgMailboxStore(pgPool);
|
|
575
|
-
}
|
|
576
|
-
else if (mysqlPool) {
|
|
577
|
-
await ensureTiDBMailboxSchema(mysqlPool);
|
|
578
|
-
mailboxStore = new TiDBMailboxStore(mysqlPool);
|
|
579
|
-
}
|
|
580
|
-
else if (backend?.kind === "local") {
|
|
581
|
-
mailboxStore = new FileMailboxStore(config.localDataRoot ?? localRoot);
|
|
582
|
-
}
|
|
583
|
-
if (mailboxStore)
|
|
584
|
-
logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
585
|
-
}
|
|
586
|
-
const memoryExportBackend = config.memoryEngineBackend !== "file" ? memoryEngine?.backend : undefined;
|
|
587
|
-
const memoryLane = memoryEngineRemoteLanePosture(config);
|
|
588
|
-
if (config.memoryEngineBackend === "file" || !config.memoryEngineEnabled) {
|
|
589
|
-
logger.info("memory_engine_enabled", memoryEngine
|
|
590
|
-
? { enabled: true, backend: "file", dir: memoryEngine.root }
|
|
591
|
-
: {
|
|
592
|
-
enabled: false,
|
|
593
|
-
reason: config.requirePrincipal === true ? "multi-tenant on the FILE backend (no tenant isolation; set MEMORY_ENGINE_BACKEND=pg|tidb to light up)"
|
|
594
|
-
: !config.memoryEngineEnabled ? "MEMORY_ENGINE=off"
|
|
595
|
-
: `remote exec lane "${memoryLane?.lane}" (worker/sandbox file planes split; MEMORY_ENGINE_REMOTE_LANE=allow overrides)`,
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
if (memoryLane && config.memoryEngineEnabled && config.requirePrincipal !== true) {
|
|
599
|
-
logger.warn("memory_engine_remote_lane", memoryLane.posture === "dark"
|
|
600
|
-
? { 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
|
-
: { 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
|
-
}
|
|
603
|
-
let memorySyncRunner;
|
|
604
|
-
if (config.memorySync) {
|
|
605
|
-
if (memoryEngine) {
|
|
606
|
-
memorySyncRunner = createMemorySyncRunner({
|
|
607
|
-
backend: memoryEngine.backend,
|
|
608
|
-
scope: config.memorySync.scope,
|
|
609
|
-
memoryRoot: memoryEngine.root,
|
|
610
|
-
transport: createMemorySyncTransport({ url: config.memorySync.url, token: config.memorySync.token }),
|
|
611
|
-
log: logger,
|
|
612
|
-
...(config.memorySync.maxPushEntries !== undefined ? { maxPushEntries: config.memorySync.maxPushEntries } : {}),
|
|
613
|
-
...(config.memorySync.maxPullEntries !== undefined ? { maxPullEntries: config.memorySync.maxPullEntries } : {}),
|
|
614
|
-
});
|
|
615
|
-
logger.info("memory_sync_enabled", { url: config.memorySync.url, scope: config.memorySync.scope, cursorPath: memorySyncRunner.cursorPath });
|
|
616
|
-
memorySyncRunner.trigger("boot");
|
|
617
|
-
}
|
|
618
|
-
else {
|
|
619
|
-
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
|
-
}
|
|
621
|
-
}
|
|
622
|
-
const sessionStore = createSessionStore(config, backend, metrics);
|
|
623
|
-
if (config.requirePrincipal && !sessionStore.ownerOf) {
|
|
624
|
-
throw new Error("REQUIRE_PRINCIPAL=true needs an owner-aware session store (SESSION_BACKEND=tidb); " +
|
|
625
|
-
"the in-memory store cannot enforce session ownership.");
|
|
626
|
-
}
|
|
627
|
-
if (config.requirePrincipal && backend?.kind === "local") {
|
|
628
|
-
throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
|
|
629
|
-
"durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 — durable " +
|
|
630
|
-
"owners = P1), so it cannot durably enforce multi-tenant session ownership. Use a SQL backend " +
|
|
631
|
-
"(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
|
|
632
|
-
"still inject x-agent-principal per request for memory scoping and audit attribution).");
|
|
633
|
-
}
|
|
634
|
-
if (backend)
|
|
635
|
-
assertCloudSnapshotBlobPosture(backend.kind, config);
|
|
636
|
-
if (!config.requirePrincipal && sessionStore.ownerOf) {
|
|
637
|
-
logger.warn("principal_optional", {
|
|
638
|
-
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
|
-
});
|
|
640
|
-
}
|
|
641
|
-
metrics.dynamicGauge("sessions_warm", "Sessions held in the warm cache", () => sessionStore.size);
|
|
642
|
-
const breakerState = backend?.breaker && config.resilience.circuitBreaker
|
|
643
|
-
? backend
|
|
644
|
-
.breaker((streak) => {
|
|
645
|
-
metrics.setGauge("counter_flush_fail_streak", streak, { table: "circuit_breaker", kind: "write_through" });
|
|
646
|
-
if (streak > 0)
|
|
647
|
-
metrics.inc("breaker_writethrough_failed_total", { backend: backend.kind });
|
|
648
|
-
if (streak === 3)
|
|
649
|
-
logger.warn("breaker_writethrough_degraded", { streak });
|
|
650
|
-
else if (streak === 0)
|
|
651
|
-
logger.info("breaker_writethrough_recovered", {});
|
|
652
|
-
})
|
|
653
|
-
.startRefresh()
|
|
654
|
-
: undefined;
|
|
655
|
-
const brain = createBrain(config, { breakerState });
|
|
656
|
-
const pricing = buildPricing(config.models);
|
|
657
|
-
const counterDegradeHook = (info) => {
|
|
658
|
-
metrics.setGauge("counter_flush_fail_streak", info.streak, { table: info.table, kind: info.kind });
|
|
659
|
-
if (info.streak > 0)
|
|
660
|
-
metrics.inc("counter_flush_failed_total", { table: info.table, kind: info.kind });
|
|
661
|
-
if (info.streak === 3)
|
|
662
|
-
logger.warn("counter_flush_degraded", { table: info.table, kind: info.kind, streak: info.streak, ...(info.error ? { error: info.error } : {}) });
|
|
663
|
-
else if (info.streak === 0 && info.prevStreak >= 3)
|
|
664
|
-
logger.info("counter_flush_recovered", { table: info.table, kind: info.kind, prevStreak: info.prevStreak });
|
|
665
|
-
};
|
|
666
|
-
const costQuota = config.maxPrincipalCostUsd > 0
|
|
667
|
-
? backend?.costQuota
|
|
668
|
-
? backend.costQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000, counterDegradeHook).startRefresh()
|
|
669
|
-
: new CostQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000)
|
|
670
|
-
: undefined;
|
|
671
|
-
const modelUsageTracker = new ModelUsageTracker();
|
|
672
|
-
const promptManifestTracker = new PromptManifestTracker();
|
|
673
|
-
const fleetUsage = config.configCenter && process.env.FLEET_ADVERTISE_ADDRESS ? new FleetUsageAccumulator() : undefined;
|
|
674
|
-
const fleetLease = createFleetLeaseFromEnv(config, { logger, metrics });
|
|
675
|
-
const tracer = createTracer(metrics, costQuota, modelUsageTracker, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1, promptManifestTracker);
|
|
676
|
-
const sideQueryAccounting = createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1);
|
|
677
|
-
const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined;
|
|
678
|
-
const sessionPolicyStore = backend ? backend.sessionPolicy() : undefined;
|
|
679
|
-
const fileSnapshotStore = backend ? backend.fileSnapshot() : undefined;
|
|
680
|
-
if (fileSnapshotStore && "onSkippedVanished" in fileSnapshotStore) {
|
|
681
|
-
fileSnapshotStore.onSkippedVanished = (count) => metrics.inc("snapshot_skipped_vanished_total", {}, count);
|
|
682
|
-
}
|
|
683
|
-
const perTaskImage = new PerTaskImageRegistry();
|
|
684
|
-
const sessionEnvSelection = new SessionEnvironmentSelection();
|
|
685
|
-
const MAX_CWD_SESSIONS = 4096;
|
|
686
|
-
const perSessionCwd = new Map();
|
|
687
|
-
const setSessionCwd = (sid, cwd) => {
|
|
688
|
-
perSessionCwd.delete(sid);
|
|
689
|
-
perSessionCwd.set(sid, cwd);
|
|
690
|
-
if (perSessionCwd.size > MAX_CWD_SESSIONS)
|
|
691
|
-
perSessionCwd.delete(perSessionCwd.keys().next().value);
|
|
692
|
-
};
|
|
693
|
-
const perSessionShellEnv = new Map();
|
|
694
|
-
const setSessionShellEnv = (sid, env) => {
|
|
695
|
-
perSessionShellEnv.delete(sid);
|
|
696
|
-
perSessionShellEnv.set(sid, env);
|
|
697
|
-
if (perSessionShellEnv.size > MAX_CWD_SESSIONS)
|
|
698
|
-
perSessionShellEnv.delete(perSessionShellEnv.keys().next().value);
|
|
699
|
-
};
|
|
700
|
-
let executionEnvFactory;
|
|
701
|
-
let worktreeReap;
|
|
702
|
-
if (config.remoteExec?.provider === "e2b") {
|
|
703
|
-
executionEnvFactory = e2bExecutionEnvFactory({
|
|
704
|
-
apiKey: config.remoteExec.apiKey,
|
|
705
|
-
...(config.remoteExec.template ? { template: config.remoteExec.template } : {}),
|
|
706
|
-
...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
|
|
707
|
-
...(config.remoteExec.livenessMs != null ? { livenessMs: config.remoteExec.livenessMs } : {}),
|
|
708
|
-
...(config.remoteExec.allowInternetAccess != null ? { allowInternetAccess: config.remoteExec.allowInternetAccess } : {}),
|
|
709
|
-
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
710
|
-
...(() => {
|
|
711
|
-
const merged = { ...derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv()), ...(config.remoteExec.sandboxEnv ?? {}) };
|
|
712
|
-
return Object.keys(merged).length > 0 ? { sandboxEnv: merged } : {};
|
|
713
|
-
})(),
|
|
714
|
-
logger,
|
|
715
|
-
metrics,
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
else if (config.remoteExec?.provider === "k8s") {
|
|
719
|
-
const k8sCfg = {
|
|
720
|
-
image: config.remoteExec.image,
|
|
721
|
-
...(config.remoteExec.apiUrl ? { apiUrl: config.remoteExec.apiUrl } : {}),
|
|
722
|
-
...(config.remoteExec.token ? { token: config.remoteExec.token } : {}),
|
|
723
|
-
...(config.remoteExec.caCert ? { caCert: config.remoteExec.caCert } : {}),
|
|
724
|
-
...(config.remoteExec.insecureTls ? { insecureTls: true } : {}),
|
|
725
|
-
...(config.remoteExec.namespace ? { namespace: config.remoteExec.namespace } : {}),
|
|
726
|
-
...(config.remoteExec.runtimeClass != null ? { runtimeClass: config.remoteExec.runtimeClass } : {}),
|
|
727
|
-
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
728
|
-
...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
|
|
729
|
-
...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
|
|
730
|
-
...(config.remoteExec.cpu ? { cpu: config.remoteExec.cpu } : {}),
|
|
731
|
-
...(config.remoteExec.s3Snapshot ? { s3Snapshot: config.remoteExec.s3Snapshot } : {}),
|
|
732
|
-
...(() => {
|
|
733
|
-
const derived = derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv());
|
|
734
|
-
return Object.keys(derived).length > 0 ? { podEnv: derived } : {};
|
|
735
|
-
})(),
|
|
736
|
-
};
|
|
737
|
-
executionEnvFactory = (ctx) => k8sExecutionEnvFactory({ ...k8sCfg, image: perTaskImage.get(ctx.sessionId) ?? k8sCfg.image })(ctx);
|
|
738
|
-
}
|
|
739
|
-
else if (config.remoteExec?.provider === "ssh") {
|
|
740
|
-
executionEnvFactory = sshExecutionEnvFactory({
|
|
741
|
-
host: config.remoteExec.host,
|
|
742
|
-
username: config.remoteExec.username,
|
|
743
|
-
privateKey: config.remoteExec.privateKey,
|
|
744
|
-
...(config.remoteExec.port != null ? { port: config.remoteExec.port } : {}),
|
|
745
|
-
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
else if (config.remoteExec?.provider === "adb") {
|
|
749
|
-
executionEnvFactory = adbExecutionEnvFactory({
|
|
750
|
-
serial: config.remoteExec.serial,
|
|
751
|
-
...(config.remoteExec.adbPath ? { adbPath: config.remoteExec.adbPath } : {}),
|
|
752
|
-
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
else if (config.remoteExec?.provider === "host") {
|
|
756
|
-
{
|
|
757
|
-
const hostScheduler = config.schedulerEnabled && config.requirePrincipal !== true
|
|
758
|
-
? new FileSchedulerBackend({
|
|
759
|
-
...(config.schedulerStorePath ? { storePath: config.schedulerStorePath } : {}),
|
|
760
|
-
...(config.schedulerSessionWakeup === false ? { caps: { supportsSessionWakeup: false } } : {}),
|
|
761
|
-
})
|
|
762
|
-
: undefined;
|
|
763
|
-
if (hostScheduler)
|
|
764
|
-
logger.info(`R7: self-wake scheduler enabled (host lane), store=${config.schedulerStorePath ?? defaultSchedulerStorePath()}`);
|
|
765
|
-
const hostCfg = {
|
|
766
|
-
...(config.remoteExec.workspaceBase ? { workspaceBase: config.remoteExec.workspaceBase } : {}),
|
|
767
|
-
...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
|
|
768
|
-
...(hostScheduler ? { scheduler: hostScheduler } : {}),
|
|
769
|
-
backgroundShell: config.requirePrincipal !== true,
|
|
770
|
-
};
|
|
771
|
-
executionEnvFactory = (ctx) => {
|
|
772
|
-
const cwd = effectiveHostWorkspace(perSessionCwd.get(ctx.sessionId), ctx);
|
|
773
|
-
const shellEnv = perSessionShellEnv.get(ctx.sessionId);
|
|
774
|
-
const env = hostExecutionEnvFactory({
|
|
775
|
-
...hostCfg,
|
|
776
|
-
...(cwd ? { workspaceDir: cwd } : {}),
|
|
777
|
-
...(shellEnv ? { env: shellEnv } : {}),
|
|
778
|
-
})(ctx);
|
|
779
|
-
return lspManager && !cwd ? evictLspOnDestroy(env, lspManager) : env;
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
else if (config.remoteExec?.provider === "local-docker") {
|
|
784
|
-
executionEnvFactory = localDockerExecutionEnvFactory({
|
|
785
|
-
image: config.remoteExec.image,
|
|
786
|
-
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
787
|
-
...(config.remoteExec.dockerPath ? { dockerPath: config.remoteExec.dockerPath } : {}),
|
|
788
|
-
...(config.remoteExec.dockerHost ? { dockerHost: config.remoteExec.dockerHost } : {}),
|
|
789
|
-
...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
|
|
790
|
-
...(config.remoteExec.cpus != null ? { cpus: config.remoteExec.cpus } : {}),
|
|
791
|
-
...(config.remoteExec.network ? { network: config.remoteExec.network } : {}),
|
|
792
|
-
...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
|
|
793
|
-
...(config.remoteExec.env ? { env: config.remoteExec.env } : {}),
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
if (executionEnvFactory && isRemoteScratchpadLane(config.remoteExec?.provider)) {
|
|
797
|
-
executionEnvFactory = withRemoteScratchpad(executionEnvFactory, logger);
|
|
798
|
-
}
|
|
799
|
-
if (config.worktreeIsolation && executionEnvFactory) {
|
|
800
|
-
const wtRoot = config.worktreeIsolation.repoRoot;
|
|
801
|
-
const wtValid = existsSync(wtRoot) && existsSync(join(wtRoot, ".git"));
|
|
802
|
-
if (config.remoteExec?.provider === "host" && !wtValid) {
|
|
803
|
-
logger.error("worktree_isolation_disabled_bad_repo_root", {
|
|
804
|
-
repoRoot: wtRoot,
|
|
805
|
-
reason: existsSync(wtRoot) ? "not a git repository (no .git)" : "path does not exist",
|
|
806
|
-
note: "WORKTREE_ISOLATION_ENABLED is set but WORKTREE_REPO_ROOT is not a usable git repo — isolation left OFF; isolation:'worktree' agents run on the shared base env. Fix the path or unset the flag.",
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
else if (config.remoteExec?.provider === "host") {
|
|
810
|
-
const gitBaseEnv = new RemoteHostExecutionEnv({ workspaceDir: config.worktreeIsolation.repoRoot, inheritEnv: "all", backgroundShell: config.requirePrincipal !== true });
|
|
811
|
-
executionEnvFactory = withWorktreeIsolation(executionEnvFactory, {
|
|
812
|
-
repoRoot: config.worktreeIsolation.repoRoot,
|
|
813
|
-
...(config.worktreeIsolation.allowedRoots ? { allowedRoots: config.worktreeIsolation.allowedRoots } : {}),
|
|
814
|
-
...(config.worktreeIsolation.commit ? { commit: config.worktreeIsolation.commit } : {}),
|
|
815
|
-
baseEnvForGit: gitBaseEnv,
|
|
816
|
-
rootEnvAt: (dir) => {
|
|
817
|
-
const env = new RemoteHostExecutionEnv({ workspaceDir: dir, backgroundShell: config.requirePrincipal !== true });
|
|
818
|
-
return lspManager ? evictLspOnDestroy(env, lspManager) : env;
|
|
819
|
-
},
|
|
820
|
-
logger,
|
|
821
|
-
});
|
|
822
|
-
worktreeReap = () => reapOrphanWorktrees(gitBaseEnv, config.worktreeIsolation.repoRoot, logger);
|
|
823
|
-
logger.info("worktree_isolation_enabled", { provider: "host", repoRoot: config.worktreeIsolation.repoRoot });
|
|
824
|
-
void worktreeReap();
|
|
825
|
-
}
|
|
826
|
-
else {
|
|
827
|
-
logger.warn("worktree_isolation_unsupported_lane", {
|
|
828
|
-
provider: config.remoteExec?.provider,
|
|
829
|
-
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
|
-
});
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
const sendUserFileTaskEnvs = (() => {
|
|
834
|
-
const provider = config.remoteExec?.provider;
|
|
835
|
-
if (!config.sendUserFile || !executionEnvFactory || !sandboxSendLaneEnabled(provider, config.requirePrincipal))
|
|
836
|
-
return undefined;
|
|
837
|
-
const registry = new TaskEnvRegistry();
|
|
838
|
-
executionEnvFactory = registry.wrapFactory(executionEnvFactory);
|
|
839
|
-
return registry;
|
|
840
|
-
})();
|
|
841
|
-
if (taskAttachmentStore && executionEnvFactory) {
|
|
842
|
-
const inner = executionEnvFactory;
|
|
843
|
-
const attStore = taskAttachmentStore;
|
|
844
|
-
executionEnvFactory = async (ctx) => {
|
|
845
|
-
const env = await inner(ctx);
|
|
846
|
-
const n = await materializeAttachmentsInto(env, attStore, ctx.sessionId);
|
|
847
|
-
if (n > 0)
|
|
848
|
-
logger.info("attachments_materialized", { sessionId: ctx.sessionId, count: n });
|
|
849
|
-
return env;
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
if (executionEnvFactory) {
|
|
853
|
-
const isolated = config.remoteExec.provider === "e2b" ||
|
|
854
|
-
config.remoteExec.provider === "k8s" ||
|
|
855
|
-
config.remoteExec.provider === "local-docker";
|
|
856
|
-
const suspendable = config.remoteExec.provider === "e2b" ||
|
|
857
|
-
(config.remoteExec.provider === "k8s" && !!config.remoteExec.s3Snapshot);
|
|
858
|
-
const sandboxEnvKeys = config.remoteExec.provider === "e2b" ? Object.keys(config.remoteExec.sandboxEnv ?? {}) : [];
|
|
859
|
-
logger.info("remote_exec_enabled", { provider: config.remoteExec.provider, isolated, suspendable, ...(sandboxEnvKeys.length > 0 ? { sandboxEnvKeys } : {}) });
|
|
860
|
-
if (!isolated)
|
|
861
|
-
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
|
-
}
|
|
863
|
-
const lspProvider = config.remoteExec?.provider;
|
|
864
|
-
const lspManager = (lspProvider === "e2b" || lspProvider === "k8s") && config.lspEnabled
|
|
865
|
-
? createE2bLspManager({ log: (event, fields) => logger.info(event, fields), scheme: lspProvider === "k8s" ? "ws" : "wss" })
|
|
866
|
-
:
|
|
867
|
-
lspProvider === "host" && config.lspHostEnabled && config.requirePrincipal !== true
|
|
868
|
-
? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) })
|
|
869
|
-
: undefined;
|
|
870
|
-
if (lspManager)
|
|
871
|
-
logger.info("lsp_enabled", { provider: lspProvider });
|
|
132
|
+
// design/158 A10 尾刀:config-center 的 boot 半场(sealed-key 托管 → provider → prompt-epoch 店 →
|
|
133
|
+
// effective 拉取 / LKG 兜底 / tolerant apply)搬到 src/boot/config-center.ts。该段与 refresh 循环共享的
|
|
134
|
+
// 12 个跨段可变 `let` 现在是那只工厂的闭包私有字段(每一处读写的文本与次序逐字保留,见该文件头注);
|
|
135
|
+
// main 只经窄口读:getKeyResolver/getCenterPrompts/modelReady/restartState/planeDeferredState/promptSource。
|
|
136
|
+
// ⚠️ 位置即契约:必须在 openStores 之前 —— models/roles/teams 要在 Runner/持久层建起来之前应用进 config。
|
|
137
|
+
const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
|
|
138
|
+
// design/158 A10 尾刀:local 形的 config.d/remote-exec.json 覆写搬到 boot/config-center.ts(逐字)。
|
|
139
|
+
// ⚠️ 位置即契约:就地改写 config.remoteExec,必须早于 createExecutionEnv 这个消费点。
|
|
140
|
+
await configCenter.applyLocalRemoteExec();
|
|
141
|
+
// design/158 A10:持久层装配搬到 src/boot/stores.ts(逐字)。⚠️ 该段就地归一 `config.sessionBackend`
|
|
142
|
+
// 且承载三条 fail-loud 拒启断言 —— 位置即契约,理由见该文件头注。
|
|
143
|
+
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, } = await openStores({ config, logger, metrics, localRoot });
|
|
144
|
+
// design/158 A10:计费/追踪/预算装配搬到 src/boot/budget-tracing.ts(逐字;tracer 与 side-query 同 sink 实例的
|
|
145
|
+
// 「同段构造」契约见该文件头注)。
|
|
146
|
+
const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileSnapshotStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
|
|
147
|
+
// design/158 A10:执行环境装配搬到 src/boot/execution-env.ts(逐字)。
|
|
148
|
+
// ⚠️ 工厂装饰顺序=行为(scratchpad → worktree → SendUserFile 登记 → 附件物化最外层),见该文件头注。
|
|
149
|
+
const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
|
|
150
|
+
// prove-before-scale stopgap: a no-op tool tracer (logs every tool call incl. resumed legs that
|
|
151
|
+
// /events hides) so we can diagnose single-agent coding behaviour. Off unless TOOL_TRACE=true.
|
|
872
152
|
const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
|
|
873
153
|
if (toolTracer)
|
|
874
154
|
logger.info("tool_trace_enabled", {});
|
|
155
|
+
// Goal B4 (2026-07-08): the ALWAYS-ON gate-deny meter (`permission_denied_total{source}`) — an
|
|
156
|
+
// operational/security signal that must not sit behind the TOOL_TRACE diagnostic switch. Composed
|
|
157
|
+
// UNDER the tracer (deployment slot order: meter first, tracer second — composeHooks double-runs the
|
|
158
|
+
// void permissionDenied slot); this composite is what every hooks site below uses as the deployment
|
|
159
|
+
// baseline, so a task-hooks fold (resolveSpec) shadows neither.
|
|
875
160
|
const deploymentHooks = toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics);
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
logger.info("workflow_completed", {
|
|
892
|
-
runId: p.runId,
|
|
893
|
-
status: p.status,
|
|
894
|
-
...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
|
|
895
|
-
});
|
|
896
|
-
metrics.inc("workflow_runs_total", { status: p.status });
|
|
897
|
-
if (!workflowCompletionInbox)
|
|
898
|
-
return;
|
|
899
|
-
try {
|
|
900
|
-
const route = await resolveCompletionRoute(p, runStore ? (id) => runStore.getRun(id) : undefined, config.requirePrincipal === true);
|
|
901
|
-
if (!route)
|
|
902
|
-
return;
|
|
903
|
-
await workflowCompletionInbox.enqueue({
|
|
904
|
-
sessionId: route.sessionId,
|
|
905
|
-
owner: route.owner,
|
|
906
|
-
runId: p.runId,
|
|
907
|
-
status: p.status,
|
|
908
|
-
summary: p.summary,
|
|
909
|
-
enqueuedAt: Date.now(),
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
catch (err) {
|
|
913
|
-
logger.warn("workflow_completion_enqueue_failed", { runId: p.runId, err: String(err) });
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
const workflowNotifyGate = baseWorkflowRunStore && workflowNotifyJournal
|
|
917
|
-
? new WorkflowNotifyGate(workflowNotifyJournal, baseWorkflowRunStore, deliverWorkflowCompletion, {
|
|
918
|
-
onError: (stage, runId, err) => logger.warn("workflow_notify_failed", { stage, runId, err: String(err) }),
|
|
919
|
-
})
|
|
920
|
-
: undefined;
|
|
921
|
-
const fleetBus = new FleetEventBus();
|
|
922
|
-
const workflowRunStore = baseWorkflowRunStore
|
|
923
|
-
? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
|
|
924
|
-
: undefined;
|
|
925
|
-
const workflowJournalStore = config.selfOrchestrationEnabled && backend ? backend.workflowJournal() : undefined;
|
|
926
|
-
const outcomeSink = backend?.outcomeSink();
|
|
927
|
-
const workflowAgentRegistry = config.selfOrchestrationEnabled ? new WorkflowAgentRegistry() : undefined;
|
|
928
|
-
const subagentSteerRegistry = new SubagentSteerRegistry();
|
|
929
|
-
const elicitation = config.mcpElicitation.enabled ? new ElicitationCoordinator(config.mcpElicitation.throttle) : undefined;
|
|
930
|
-
const question = config.askQuestionEnabled ? new QuestionCoordinator() : undefined;
|
|
931
|
-
const toolApproval = config.toolApprovalEnabled ? new ToolApprovalCoordinator() : undefined;
|
|
932
|
-
const durableEnabled = backend?.checkpoint !== undefined && config.durableApproval === true;
|
|
933
|
-
const sendUserFileEmitter = (() => {
|
|
934
|
-
if (!config.sendUserFile)
|
|
935
|
-
return undefined;
|
|
936
|
-
if (sendUserFileTaskEnvs)
|
|
937
|
-
return new SendUserFileEmitter();
|
|
938
|
-
const provider = config.remoteExec?.provider ?? "host";
|
|
939
|
-
if (provider === "host" && config.requirePrincipal !== true)
|
|
940
|
-
return new SendUserFileEmitter();
|
|
941
|
-
return undefined;
|
|
942
|
-
})();
|
|
943
|
-
const sendFileLedger = config.sendUserFile && backend ? backend.sendFileLedger() : undefined;
|
|
944
|
-
const sendUserFileToolSpec = (() => {
|
|
945
|
-
if (!sendUserFileEmitter || !config.sendUserFile)
|
|
946
|
-
return undefined;
|
|
947
|
-
const issuer = createSendUserFileIssuer(config.sendUserFile);
|
|
948
|
-
const rawSend = sendUserFileTaskEnvs
|
|
949
|
-
? createSandboxFileSend({
|
|
950
|
-
registry: sendUserFileTaskEnvs,
|
|
951
|
-
prepare: (filename, scope) => issuer.prepareDirectUpload(filename, undefined, scope),
|
|
952
|
-
maxBytes: SEND_USER_FILE_MAX_BYTES,
|
|
953
|
-
})
|
|
954
|
-
: async (p, sendCtx) => {
|
|
955
|
-
const resolved = resolve(p);
|
|
956
|
-
const st = await fsStat(resolved).catch(() => undefined);
|
|
957
|
-
if (!st || !st.isFile())
|
|
958
|
-
throw new Error(`file not found (or not a regular file): ${p} — ls first; absolute paths are safest`);
|
|
959
|
-
if (st.size > SEND_USER_FILE_MAX_BYTES)
|
|
960
|
-
throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${st.size} bytes)`);
|
|
961
|
-
const bytes = await fsReadFile(resolved);
|
|
962
|
-
if (bytes.byteLength > SEND_USER_FILE_MAX_BYTES)
|
|
963
|
-
throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${bytes.byteLength} bytes)`);
|
|
964
|
-
return issuer.issue(bytes, basename(resolved), undefined, sendCtx.principal);
|
|
965
|
-
};
|
|
966
|
-
const send = sendFileLedger
|
|
967
|
-
? withLedgerRecording(rawSend, {
|
|
968
|
-
ledger: sendFileLedger,
|
|
969
|
-
deleteObject: (bucket, key) => issuer.deleteObject(bucket, key),
|
|
970
|
-
warn: (msg, meta) => logger.warn(msg, meta),
|
|
971
|
-
})
|
|
972
|
-
: rawSend;
|
|
973
|
-
return sendUserFileTool({ send, emitter: sendUserFileEmitter });
|
|
974
|
-
})();
|
|
975
|
-
if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
|
|
976
|
-
logger.warn("runtime_caps_scoped_token_no_worker", {
|
|
977
|
-
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
|
-
});
|
|
979
|
-
}
|
|
980
|
-
const principalCaps = config.configCenter && !config.configCenter.dryRun
|
|
981
|
-
? createPrincipalEntitlementsClient({
|
|
982
|
-
baseUrl: config.configCenter.baseUrl,
|
|
983
|
-
token: config.configCenter.token,
|
|
984
|
-
...(config.configCenter.worker ? { worker: config.configCenter.worker } : {}),
|
|
985
|
-
onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
|
|
986
|
-
})
|
|
987
|
-
: undefined;
|
|
988
|
-
const centerRuntimeCapsResolver = principalCaps?.resolveRuntimeCaps;
|
|
989
|
-
const runtimeCapsResolver = applyObserverEnvOptIn(centerRuntimeCapsResolver, {
|
|
990
|
-
experimentalObserverAgents: config.experimentalObserverAgents,
|
|
991
|
-
requirePrincipal: config.requirePrincipal,
|
|
992
|
-
warn: (msg, fields) => logger.warn(msg, fields),
|
|
161
|
+
// design/158 A10:workflow 编排面搬到 src/boot/workflow-orchestration.ts(逐字;构造次序契约见该文件头注)。
|
|
162
|
+
const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
|
|
163
|
+
// design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled 的
|
|
164
|
+
// 「必须早于 runnerDeps 求值」次序契约见该文件头注)。
|
|
165
|
+
const { elicitation, question, toolApproval, durableEnabled, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
|
|
166
|
+
// design/158 A10:per-principal caps 段搬到 src/boot/runtime-caps.ts(逐字)。
|
|
167
|
+
const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
|
|
168
|
+
// design/158 A10:RunnerDeps 装配段搬到 src/boot/runner-deps.ts(逐字;runStore 晚绑改取值,见该文件头注)。
|
|
169
|
+
const runnerDeps = createRunnerDeps({
|
|
170
|
+
config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, brain,
|
|
171
|
+
pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
|
|
172
|
+
memorySyncRunner, toolResultStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
|
|
173
|
+
executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
|
|
174
|
+
workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion,
|
|
175
|
+
getRunStore: () => runStore,
|
|
993
176
|
});
|
|
994
|
-
const workflowModelAllowlist = workflowModelAllowlistFor(config);
|
|
995
|
-
const selfOrchestrationDeps = config.selfOrchestrationEnabled
|
|
996
|
-
? {
|
|
997
|
-
workflowScriptRunner: config.selfOrchestrationWorkerIsolation
|
|
998
|
-
? createWorkerHardenedVmRunner()
|
|
999
|
-
: createHardenedVmRunner(),
|
|
1000
|
-
workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
|
|
1001
|
-
workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
|
|
1002
|
-
workflowScriptStore: (() => {
|
|
1003
|
-
const fileStore = createFileWorkflowScriptStore(join(config.localDataRoot ?? localRoot, "workflow-scripts"));
|
|
1004
|
-
return {
|
|
1005
|
-
scopePartitioned: true,
|
|
1006
|
-
persist: fileStore.persist.bind(fileStore),
|
|
1007
|
-
load: fileStore.load.bind(fileStore),
|
|
1008
|
-
resolveName: (name) => resolveCollabWorkflow(name) ?? fileStore.resolveName?.(name),
|
|
1009
|
-
list: () => listCollabWorkflows(),
|
|
1010
|
-
};
|
|
1011
|
-
})(),
|
|
1012
|
-
onWorkflowAgentSpawn: workflowAgentRegistry
|
|
1013
|
-
? (handle) => {
|
|
1014
|
-
const unregister = workflowAgentRegistry.register(handle);
|
|
1015
|
-
void Promise.resolve(handle.result()).then(unregister, unregister);
|
|
1016
|
-
}
|
|
1017
|
-
: undefined,
|
|
1018
|
-
workflowLimits: config.workflowSizeGuideline ? { sizeGuideline: config.workflowSizeGuideline } : undefined,
|
|
1019
|
-
workflowGovernanceBaseline: {
|
|
1020
|
-
base: config.workflowAgentsReadOnly ? { handsReadOnly: true } : {},
|
|
1021
|
-
worktreeBase: config.workflowAgentsReadOnly ? { handsReadOnly: false } : undefined,
|
|
1022
|
-
workflowModelAllowlist: workflowModelAllowlist ? workflowModelAllowlist : undefined,
|
|
1023
|
-
},
|
|
1024
|
-
workflowCompletionNotifier: {
|
|
1025
|
-
...(workflowNotifyGate
|
|
1026
|
-
? workflowNotifyGate.buildNotifier()
|
|
1027
|
-
: { notify: (input) => deliverWorkflowCompletion(input) }),
|
|
1028
|
-
ackServed: async (input) => {
|
|
1029
|
-
if (!workflowCompletionInbox)
|
|
1030
|
-
return;
|
|
1031
|
-
try {
|
|
1032
|
-
const sid = await resolveServedSession(input, runStore ? (id) => runStore.getRun(id) : undefined);
|
|
1033
|
-
if (sid) {
|
|
1034
|
-
await workflowCompletionInbox.markTerminalServed(sid, input.runId);
|
|
1035
|
-
logger.info("workflow_complete_ack_served", { route: "poll-served", sessionId: sid, runId: input.runId });
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
catch (err) {
|
|
1039
|
-
logger.warn("workflow_ack_served_failed", { runId: input.runId, err: String(err) });
|
|
1040
|
-
}
|
|
1041
|
-
},
|
|
1042
|
-
},
|
|
1043
|
-
}
|
|
1044
|
-
: {};
|
|
1045
|
-
const runnerDeps = {
|
|
1046
|
-
promptSource,
|
|
1047
|
-
rosterStore: rosterStore ? rosterStore : undefined,
|
|
1048
|
-
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
1049
|
-
mailboxStore: mailboxStore ? mailboxStore : undefined,
|
|
1050
|
-
brain,
|
|
1051
|
-
models: config.models,
|
|
1052
|
-
roles: config.roles,
|
|
1053
|
-
tiers: config.tiers,
|
|
1054
|
-
pricing,
|
|
1055
|
-
tracer,
|
|
1056
|
-
hands: config.configProvider === "local" ? { commitCoAuthor: "Sema <noreply@vivi-ai.com>" } : undefined,
|
|
1057
|
-
onTaskOutcome: outcomeSink
|
|
1058
|
-
? (o) => {
|
|
1059
|
-
metrics.inc("task_outcomes_total", { status: o.status, green: String(o.oracle?.green ?? "unknown") });
|
|
1060
|
-
void outcomeSink.recordCore(o).catch((err) => logger.warn("task_outcome_record_failed", { runId: o.runId, err: String(err) }));
|
|
1061
|
-
}
|
|
1062
|
-
: undefined,
|
|
1063
|
-
onElicit: elicitation ? elicitation.elicit : undefined,
|
|
1064
|
-
onQuestion: question ? question.question : undefined,
|
|
1065
|
-
onAsk: toolApproval
|
|
1066
|
-
? (req, signal) => toolApproval.ask(req, signal)
|
|
1067
|
-
: undefined,
|
|
1068
|
-
autoMode: {
|
|
1069
|
-
onBreakerOpen: (info) => {
|
|
1070
|
-
metrics.inc("auto_mode_breaker_open_total");
|
|
1071
|
-
logger.warn("auto_mode_breaker_open", { consecutiveFailures: info.consecutiveFailures, lastCause: info.lastCause });
|
|
1072
|
-
},
|
|
1073
|
-
},
|
|
1074
|
-
sessionStore,
|
|
1075
|
-
memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
|
|
1076
|
-
memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
|
|
1077
|
-
onMemoryHarvestReport: memoryEngine
|
|
1078
|
-
? (report, info) => {
|
|
1079
|
-
metrics.inc("memory_harvest_total", { ok: String(report.ok), phase: info.phase, incident: report.incident?.kind ?? "none" });
|
|
1080
|
-
if (report.patches)
|
|
1081
|
-
metrics.inc("memory_harvest_patches_total", { phase: info.phase }, (report.patches.add ?? 0) + (report.patches.update ?? 0));
|
|
1082
|
-
if (report.incident)
|
|
1083
|
-
logger.warn("memory_harvest_incident", { kind: report.incident.kind, phase: info.phase });
|
|
1084
|
-
if (memorySyncRunner && report.ok && (report.patches?.add ?? 0) + (report.patches?.update ?? 0) > 0)
|
|
1085
|
-
memorySyncRunner.trigger("harvest");
|
|
1086
|
-
}
|
|
1087
|
-
: undefined,
|
|
1088
|
-
toolResultStore,
|
|
1089
|
-
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
|
|
1090
|
-
runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
|
|
1091
|
-
fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
|
|
1092
|
-
executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
|
|
1093
|
-
lspManager: lspManager ? lspManager : undefined,
|
|
1094
|
-
onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
|
|
1095
|
-
loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
|
|
1096
|
-
probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && config.projectMemoryEnabled
|
|
1097
|
-
? makeProbeInstructionSources()
|
|
1098
|
-
: undefined,
|
|
1099
|
-
hooks: deploymentHooks,
|
|
1100
|
-
...selfOrchestrationDeps,
|
|
1101
|
-
onError: (err, ctx) => {
|
|
1102
|
-
if (ctx.phase === "degraded") {
|
|
1103
|
-
logger.warn("task_degraded", { sessionId: ctx.sessionId, info: String(err) });
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (ctx.phase === "prompt-constitution") {
|
|
1107
|
-
logger.warn("prompt_provider_needs_upgrade", { sessionId: ctx.sessionId, info: String(err) });
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
if (ctx.phase === "rewind") {
|
|
1111
|
-
const code = /\((\w+)\)/.exec(String(err))?.[1] ?? "unknown";
|
|
1112
|
-
logger.warn("rewind_snapshot_failed", { sessionId: ctx.sessionId, code, info: String(err) });
|
|
1113
|
-
metrics.inc("rewind_snapshot_failed_total", { code });
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
if (ctx.phase === "memory") {
|
|
1117
|
-
logger.warn("memory_consolidation_failed", { sessionId: ctx.sessionId, info: String(err) });
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
if (ctx.phase === "mcp") {
|
|
1121
|
-
logger.warn("mcp_server_unavailable", { sessionId: ctx.sessionId, info: String(err) });
|
|
1122
|
-
metrics.inc("mcp_server_unavailable_total");
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
if (ctx.phase === "prompt-cache") {
|
|
1126
|
-
metrics.inc("prompt_cache_low_hit_total");
|
|
1127
|
-
const fields = { sessionId: ctx.sessionId, ...(ctx.classification ? { classification: ctx.classification } : {}), err: String(err) };
|
|
1128
|
-
if (ctx.classification === "server-or-ttl")
|
|
1129
|
-
logger.warn("prompt_cache_break", fields);
|
|
1130
|
-
else
|
|
1131
|
-
logger.error("prompt_cache_break", fields);
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
if (ctx.phase === "config") {
|
|
1135
|
-
logger.warn("runner_config_advisory", { sessionId: ctx.sessionId, info: String(err) });
|
|
1136
|
-
metrics.inc("runner_config_advisory_total");
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
logger.error("runner_error", { phase: ctx.phase, sessionId: ctx.sessionId, err: String(err) });
|
|
1140
|
-
},
|
|
1141
|
-
};
|
|
1142
177
|
const runner = new Runner(runnerDeps);
|
|
178
|
+
// codex R10: TRUE ⇒ the Runner just froze a PRIVATE tier-expanded catalog copy (core runtask.js constructor,
|
|
179
|
+
// dist-read) — in-place model-plane mutation no longer reaches it, so refresh-time plane changes must be
|
|
180
|
+
// DEFERRED to restart (see appliedPlaneEff / applyEffective deferModelPlane). Captured HERE, at the same
|
|
181
|
+
// config generation the constructor read.
|
|
1143
182
|
const runnerTierFrozen = Object.keys(config.tiers).length > 0;
|
|
1144
|
-
|
|
183
|
+
// hooks 阶段三b(design/HOOKS-PHASE3B-PROMPT-AGENT.md):prompt/agent 条目的模型调用载体。
|
|
184
|
+
// - hookLlm(prompt 条目)= 直连网关一次非流式 completion——一次判定调用不起 core task(重炮打蚊子
|
|
185
|
+
// 且引入可重入面);无系统注入(hook 作者的 prompt 即全部)。默认模型=summarize 角色(cheap tier,
|
|
186
|
+
// council/压缩同款;缺角色→主模型);entry.model 覆盖须命中部署 catalog(单用户=自家目录全开,与
|
|
187
|
+
// workflow 模型 allowlist 同纪律),未命中=该条目 fail(非阻断记账)。费用不折 task budget(hook=
|
|
188
|
+
// 部署策略面开销;hook_llm_calls_total/hook_llm_cost 由 metrics 记)。
|
|
189
|
+
// - hookAgent(agent 条目)= runner.runTask 读-only 子代理:handsReadOnly + enableFork:false +
|
|
190
|
+
// 不装 hooks(🔴 可重入禁——结构性:这个 spec 永不携带 TaskSpec.hooks)+ maxTurns 10 + maxCostUsd
|
|
191
|
+
// 0.05 独立小预算,影响范围受限。final result 文本即输出(与 prompt 同一 SyncHookOutput 决策面)。
|
|
192
|
+
// hookModelFor/hookLlm 抽到 src/hooks/hook-llm.ts(可注入 fetch/keyResolver ⇒ 可单测):api 形分派
|
|
193
|
+
// (openai /chat/completions 与 anthropic /v1/messages 双腿)、缺省 baseUrl 按 api 分家、per-model key
|
|
194
|
+
// 恒胜 + foreign-no-key 连 prompt 都不发的 fail-closed 纪律,全在该模块内(注释含 audit 原文)。
|
|
195
|
+
// getKeyResolver 取函数:keyResolver 住在 config-center 运行时的闭包里,registry 热应用会整个换引用,
|
|
196
|
+
// 这里必须每次现取。
|
|
197
|
+
const { hookModelFor, hookLlm } = createHookLlm({ config, getKeyResolver: () => configCenter.getKeyResolver(), metrics });
|
|
198
|
+
// hook agent 的 session 绝不进 durable store——每次 runTask 会写 session_meta 且无人
|
|
199
|
+
// 清理(reaper 只扫 staging orphan),cap=4/事件 × 长会话=无主行堆积。专用 Runner 覆盖 sessionStore 为
|
|
200
|
+
// 进程内 TTL 店(1h 短 TTL,hook 子代理无 resume 语义,行随进程/TTL 消失);其余 deps 原样共享。
|
|
1145
201
|
const hookAgentRunner = new Runner({ ...runnerDeps, sessionStore: new TtlSessionStore({ defaultTtlDays: 1 / 24 }) });
|
|
1146
202
|
const hookAgent = async ({ prompt, model, timeoutMs }) => {
|
|
1147
203
|
const pick = hookModelFor(model);
|
|
@@ -1156,7 +212,11 @@ async function main() {
|
|
|
1156
212
|
enableFork: false,
|
|
1157
213
|
maxCostUsd: 0.05,
|
|
1158
214
|
limits: { maxTurns: 10, timeoutSec: Math.max(30, Math.ceil(timeoutMs / 1000)) },
|
|
215
|
+
// spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
|
|
216
|
+
// error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
|
|
217
|
+
// 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
|
|
1159
218
|
toolPolicy: createAllowDenyPolicy({}),
|
|
219
|
+
// 🔴 可重入禁:此 spec 不带 hooks 字段(deps.hooks 是部署观测 tracer,void 观察者,无决策面)。
|
|
1160
220
|
});
|
|
1161
221
|
metrics.inc("hook_llm_calls_total", { type: "agent" });
|
|
1162
222
|
if (result.status !== "completed")
|
|
@@ -1168,12 +228,15 @@ async function main() {
|
|
|
1168
228
|
}
|
|
1169
229
|
};
|
|
1170
230
|
const clipHookErr = (s) => (s ?? "").slice(0, 300);
|
|
231
|
+
// Auto session titles — fire-and-forget at first
|
|
232
|
+
// submit, one cheap hookLlm call (summarize role → MODEL_CHEAP_ID when configured), write-once via the session
|
|
233
|
+
// store's setTitleIfNull (SQL twins + local sidecar carry it; the memory dev store doesn't → titler dark, honest).
|
|
1171
234
|
const titleCapable = sessionStore;
|
|
1172
235
|
const sessionTitler = config.sessionAutoTitle && titleCapable.setTitleIfNull && titleCapable.probeTitle
|
|
1173
236
|
? createSessionTitler({
|
|
1174
237
|
setTitleIfNull: titleCapable.setTitleIfNull.bind(sessionStore),
|
|
1175
238
|
probeTitle: titleCapable.probeTitle.bind(sessionStore),
|
|
1176
|
-
redact: redactSecrets,
|
|
239
|
+
redact: redactSecrets, // audit 2026-07-13: scrub a model-echoed secret before persist/list/log
|
|
1177
240
|
llm: (o) => hookLlm({ prompt: o.prompt, timeoutMs: o.timeoutMs }),
|
|
1178
241
|
logger,
|
|
1179
242
|
metrics,
|
|
@@ -1181,38 +244,112 @@ async function main() {
|
|
|
1181
244
|
: undefined;
|
|
1182
245
|
if (sessionTitler)
|
|
1183
246
|
logger.info("session_auto_title_enabled", {});
|
|
247
|
+
// ① workflowsCapability SINGLE-SOURCE (core 1.155 `workflowsCapability(deps)`; ruling = SPLIT two caps):
|
|
248
|
+
// `capabilities.workflows` = core's own truth (`workflowScriptRunner.safeForUntrustedScripts` ∧ governance), boot-
|
|
249
|
+
// computed from the SAME RunnerDeps the engine uses = the ENGINE-CAN axis ("can the engine self-orchestrate /
|
|
250
|
+
// ultracode"), ORTHOGONAL to the durable-LIST axis (`Boolean(workflowRunStore)`, surfaced as `workflowsList`).
|
|
251
|
+
// ⚠️ Today these COINCIDE — `workflowRunStore` is built iff self-orch (InMemory/File, ~L527), the same gate that
|
|
252
|
+
// wires the safe runner — so this is core's FORWARD-LOOKING contract, not a live-divergence fix (the earlier
|
|
253
|
+
// "memory backend diverges" premise was wrong; double-review caught it — see the cap site in http/server.ts).
|
|
254
|
+
// Static per deployment (deps are boot-fixed).
|
|
1184
255
|
const workflowsCapable = workflowsCapability(runnerDeps);
|
|
256
|
+
// Sub-task runner (code-review council lenses/arbiter, subagent/team children). [1364]①/[1367]①(core
|
|
257
|
+
// 1.350 fork 终修的部署要求):fork 子任务带 `requireExistingSession:true` 在这里执行而源会话在 HOST 店
|
|
258
|
+
// ——拆店=响亮 `resume.session_not_found`(cli fork 案的 server 半场)。但整店共享被 codex 判两 high
|
|
259
|
+
// (普通子任务铸 committed durable 会话:无 reaper 覆盖=无界增长+污染 /v1/sessions 枚举),故走
|
|
260
|
+
// ForkRoutingSessionStore:fork/resume 形(requireExisting)→ host 店优先,普通子任务 → 私有 TTL 店
|
|
261
|
+
// (throwaway 姿势保留)。hookAgentRunner 仍保独立 TtlSessionStore(1h)——纯瞬态 hook 车道,与 fork 无关。
|
|
262
|
+
// CC-parity (CRITICAL — 子代理 hands, root cause is the service spawn path): the subagent delegation
|
|
263
|
+
// tool runs child tasks on THIS subRunner. Without executionEnvFactory the child's `handsEnabled` (= ownedEnv ||
|
|
264
|
+
// deps.executionEnv, prepare-task) is FALSE → the child gets NO hands (Bash/Read/Edit) and can't run `pwd` — the
|
|
265
|
+
// "subagent couldn't run pwd" defect hit in real TUI use. core's createSubagentTool gives hands when the env is
|
|
266
|
+
// wired (gate3: opts.tools:[] still yields Bash), so the fix is to give the subRunner the SAME executionEnvFactory +
|
|
267
|
+
// lspManager as the main runner. Deliberately NOT inherited: selfOrchestration (no recursive workflow spawn),
|
|
268
|
+
// scheduler, memory, fileSnapshot, runtimeCaps — a delegated child is a short focused task, not a full session.
|
|
269
|
+
// codex-2 F1:promptSource 必须随子代——core 1.341 把父的 centerArtifactDigest 传给委派子任务并在子
|
|
270
|
+
// runner 上按 digest 解析;缺席=每个 catalog 任务的 Agent 子代 fail-loud prompt_snapshot_unavailable。
|
|
271
|
+
// backgroundAgentStore(1.248 复审 F2):core prepare-task 给每个 task 的 TaskOutput/TaskStop/
|
|
272
|
+
// AgentTranscript 装 durable 臂用的是所在 runner 的 deps——子代任务跑在 subRunner 上,缺席=子代读
|
|
273
|
+
// 工具无 durable 回落(第三消费点,与 RunnerDeps/scenarioDeps 同一实例)。
|
|
274
|
+
// [1543]§三族A(core 穿透审查,历史欠账键批——主 runner 挂了、subRunner 漏挂 = 子代 lane 静默降级;
|
|
275
|
+
// 全部直引 runnerDeps 同一实例,双点永不漂移):
|
|
276
|
+
// - tiers(H):模型目录双 runner 分叉——tier 词 AgentDefinition 在子代 lane 抛 unknown_model、Agent
|
|
277
|
+
// model 参数("opus")静默 modelNote 降级、S3c revive 臂 row.model 记 tier 词时复活换模型。
|
|
278
|
+
// - rosterStore(M):具名 spawn 不落 durable 名册 → tier-3 按名复活从子代不可达。
|
|
279
|
+
// - onBackgroundChildEvent(M):子代触发的 revive/孙代对 fleet 流全盲(同一 publisher 实例=单管道)。
|
|
280
|
+
// - loadProjectMemory/probeInstructionSources(M):委派子代 project-blind(core types 已为子代 ctx
|
|
281
|
+
// 设计 isSubagent/agentName;门禁条件随 runnerDeps 条件键自动一致)。
|
|
282
|
+
// - hooks/onError/toolResultStore/sessionPolicyStore(L):子代进 tracer/计量;best-effort 故障不静默;
|
|
283
|
+
// fork 子代跨 wake 保 offload 引用;fork 形(有 sessionId)会话规则可达。
|
|
284
|
+
// 1877 注释的「Deliberately NOT inherited」清单(selfOrchestration/scheduler/memory/fileSnapshot/
|
|
285
|
+
// runtimeCaps)不变——本批只补漏配,不动裁定。
|
|
286
|
+
// checkpointStore 构造(从 runStore 区上移,只依赖 backend/config——顺序安全;DURABLE_APPROVAL 仍是
|
|
287
|
+
// 唯一门,ALL three backends carry checkpoint(),local = core FileCheckpointStore + service half)。
|
|
288
|
+
// 三个消费点同一实例:主 runner 的 per-task spec 面(resolveSpec durableEnabled 分支)、scenarioDeps
|
|
289
|
+
// (subagent spawn 面 forwardDurableApproval)、subRunner deps(子代执行面 suspendAsk——[1584] 接线,
|
|
290
|
+
// 裁定见下方 subRunner 构造内注)。
|
|
1185
291
|
const checkpointStore = backend?.checkpoint && config.durableApproval ? backend.checkpoint(logger) : undefined;
|
|
292
|
+
// [1593] 提名成变量:parked 子代的 ensureChildSessionDurable 晋升半场要拿它做 promoteToHost
|
|
293
|
+
// (子会话住私有 TTL 店,park 前整树拷进 host durable 店——只查不迁的 1.263 形在拆店生产形下恒
|
|
294
|
+
// 否决 park,cli 真机 14ms~2s expired 即此)。
|
|
1186
295
|
const subRunnerSessions = new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 }));
|
|
1187
296
|
const subRunner = new Runner({
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
297
|
+
// [1543]§三族A 结构性根治(design/158 冲刺尾件):与主 runnerDeps 重复的 ~15 键统一走
|
|
298
|
+
// createSharedRunnerDeps 展开——「双点挂载」从人工纪律变结构性保证(新共享键漏配其一=不可能,
|
|
299
|
+
// 基座只有一份)。差异键在展开后显式列出,每个都有为何不同的理由(见 boot/runner-deps.ts 头注)。
|
|
300
|
+
...createSharedRunnerDeps({
|
|
301
|
+
config,
|
|
302
|
+
brain,
|
|
303
|
+
pricing,
|
|
304
|
+
tracer,
|
|
305
|
+
promptSource: configCenter.promptSource,
|
|
306
|
+
executionEnvFactory,
|
|
307
|
+
lspManager,
|
|
308
|
+
backgroundAgentStore,
|
|
309
|
+
mailboxStore,
|
|
310
|
+
rosterStore,
|
|
311
|
+
deploymentHooks,
|
|
312
|
+
toolResultStore,
|
|
313
|
+
sessionPolicyStore,
|
|
314
|
+
}),
|
|
315
|
+
// ── 以下为 subRunner 差异键(不在共享基座;逐个有因)──────────────────────────────────────
|
|
316
|
+
sessionStore: subRunnerSessions, // 子代转录=私有短 TTL fork 路由店,生命周期异于宿主 durable 店
|
|
1201
317
|
onBackgroundChildEvent: runnerDeps.onBackgroundChildEvent ? runnerDeps.onBackgroundChildEvent : undefined,
|
|
1202
318
|
loadProjectMemory: runnerDeps.loadProjectMemory ? runnerDeps.loadProjectMemory : undefined,
|
|
1203
319
|
probeInstructionSources: runnerDeps.probeInstructionSources ? runnerDeps.probeInstructionSources : undefined,
|
|
1204
|
-
hooks: deploymentHooks,
|
|
1205
320
|
onError: runnerDeps.onError ? runnerDeps.onError : undefined,
|
|
1206
|
-
|
|
1207
|
-
|
|
321
|
+
// checkpointStore = 子代**执行面**的 park 设施([1582] cli TiDB 真验撞获缺口,[1584] core 定谳
|
|
322
|
+
// 接线步):core prepare-task `spec.checkpointStore ?? deps.checkpointStore` 里 spec 侧永不被
|
|
323
|
+
// subagent 设置,子代 suspendAsk 全靠这里——缺席 = 具名 bg 子代的 gated ask 只能 deny+continue,
|
|
324
|
+
// checkpoint 表零行(cli T2 复现原话「denied by an approval constraint」)。必须与 scenarioDeps
|
|
325
|
+
// (spawn 面 forwardDurableApproval 判定)**同一实例**:claim/expire/consumeParkedFlip 都作用于
|
|
326
|
+
// 同一行。旧 [1546] HIGH-2 撤载(「core 不转发 durableApproval + SAFETY ask 会 park 出搁浅
|
|
327
|
+
// checkpoint」)的前提被 core 1.388 件4(转发臂)消除,搁浅顾虑由件1-4+1.389 逐项闭合:watcher
|
|
328
|
+
// park 翻行 + reap 豁免 + reconciliation 兜孤儿;unattended SAFETY park 恒带缺省 deadline;
|
|
329
|
+
// TaskStop on parked 走 expire CAS 仲裁(1.389 F-1);/decide 可赎回。行为钉 =
|
|
330
|
+
// test/subagent-park-wiring.test.ts(SOURCE PIN + park 全链 + 旧形反向控制)。
|
|
1208
331
|
checkpointStore: checkpointStore ? checkpointStore : undefined,
|
|
1209
332
|
});
|
|
333
|
+
// S6: resolve identity + owned session from the authenticated channel (never the body).
|
|
1210
334
|
const authorize = createAuthorizer(config, sessionStore);
|
|
335
|
+
// S1/S2: durable async run registry + replayable event log (TiDB-backed).
|
|
1211
336
|
const runStore = backend ? backend.run() : undefined;
|
|
337
|
+
// E18 resume-at: the eventId→entryId anchor map. REQUIRED on every backend (works local — needs only the session
|
|
338
|
+
// tree + this map, no cloud-only checkpoint), so the only undefined case is the env-only/no-backend deploy (which
|
|
339
|
+
// also has no async runs — consistent). The shell sends body.resumeAt=eventId; resolveSpec resolves it here.
|
|
1212
340
|
const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
|
|
341
|
+
// Per-session approval exemption ("本会话不再询问") — REQUIRED on every backend like the anchor
|
|
342
|
+
// map; undefined only on the env-only/no-backend deploy (which has no durable approvals either — consistent).
|
|
1213
343
|
const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
|
|
1214
344
|
const instanceId = uuidv7();
|
|
345
|
+
// Per-task wall-clock override (seconds). 0/absent = keep the built-in 600 (1500 council/debate/team) —
|
|
346
|
+
// the override only ever RAISES (Math.max at the use site), so a misconfigured low value cannot shrink
|
|
347
|
+
// the council budget.
|
|
1215
348
|
const taskTimeoutSec = Math.max(0, Math.floor(Number(process.env.TASK_TIMEOUT_SEC ?? "0")) || 0);
|
|
349
|
+
// [854]④ per-request 配速的运营方上限旋钮(可选;缺省不设=不封顶):body.limits 每键各自被对应旋钮
|
|
350
|
+
// Math.min 封顶(normalizeLimits)。多租部署想约束 caller 自报配速时才设;单用户 turnkey 通常留空。
|
|
351
|
+
// ⚠️ 语义=只钳「显式请求值」,不是全队默认限额:body 缺席(或重放体里的非法值被 defensive DROP)⇒ 该键
|
|
352
|
+
// 不设限,与升级前行为同形——要给所有任务强加限额是另一个旋钮(未建),别指望这三个(交叉评审 F1 判读)。
|
|
1216
353
|
const capEnv = (name) => {
|
|
1217
354
|
const n = Math.floor(Number(process.env[name] ?? "0"));
|
|
1218
355
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
@@ -1222,12 +359,32 @@ async function main() {
|
|
|
1222
359
|
maxOutputTokens: capEnv("TASK_MAX_OUTPUT_TOKENS_MAX"),
|
|
1223
360
|
maxTurns: capEnv("TASK_MAX_TURNS_MAX"),
|
|
1224
361
|
};
|
|
1225
|
-
|
|
362
|
+
// F4: human-in-the-loop approval gate (pending state durable so any instance can decide).
|
|
363
|
+
const approvalStore = backend?.approval ? backend.approval() : undefined; // local omits approval() → legacy poll-gate off (durable HITL needs checkpoint anyway)
|
|
1226
364
|
const approvalEnabled = approvalStore && config.approvalRequire.length > 0;
|
|
365
|
+
// Finding #2 (edge case): the single-user auto-accept tool-policy BASELINE applies ONLY when the operator
|
|
366
|
+
// expressed NO gate intent. If they SET approval flags (APPROVAL_REQUIRE/APPROVAL_DENY/DURABLE_APPROVAL) but no
|
|
367
|
+
// backing store is wired (e.g. DB_BACKEND=local → no approvalStore/checkpointStore ⇒ approvalEnabled/durableEnabled
|
|
368
|
+
// false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined so core's UNGATED warning
|
|
369
|
+
// still surfaces it. So the baseline needs single-user turnkey ∧ zero expressed gate intent.
|
|
1227
370
|
const operatorGateIntent = config.approvalRequire.length > 0 || config.approvalDeny.length > 0 || config.durableApproval;
|
|
1228
371
|
const singleUserAutoAcceptBaseline = config.requirePrincipal !== true && !operatorGateIntent;
|
|
372
|
+
// design/45 durable-checkpoint (core 1.67): opt-in (DURABLE_APPROVAL + a pool). When on, F4 uses durable
|
|
373
|
+
// suspend/resume instead of the poll gate — a gated `ask` suspends the task (status:"suspended" + checkpoint)
|
|
374
|
+
// and an operator resumes it on any replica. Default off → unchanged poll-policy.
|
|
375
|
+
// checkpointStore 构造已上移到 subRunner 之前([1535] server 半场:子代 park 设施);本行原位注释保留。
|
|
376
|
+
// TOC integration ask③ (INTEGRATION-DESIGN §11): reclaim runs orphaned by a previous host-lane engine
|
|
377
|
+
// process (orphaned `running` → failed + release its EEXIST-blocking claim; exclude resume-in-progress rows). The
|
|
378
|
+
// logic + its null-safety (runStore undefined on the env-only/no-backend lane) live in the unit-tested
|
|
379
|
+
// `reclaimOrphansAtBoot` helper — extracted after a workflow re-review caught a boot crash there (HIGH, 1.49.0).
|
|
1229
380
|
await reclaimOrphansAtBoot(runStore, checkpointStore, logger);
|
|
1230
|
-
|
|
381
|
+
// Sandbox-image-pool index (IMAGE-API-DESIGN.md §2): enables /v1/images* query + select. TiDB-backed projection
|
|
382
|
+
// of the bake manifests + publish lifecycle. Present whenever a pool is — read-only catalog until P2/P3 bakes.
|
|
383
|
+
const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined; // local omits imageIndex() → sandbox-image-pool is cloud/fleet-only
|
|
384
|
+
// RFC A2 (A 切片先行): the model-facing SelectEnvironment tool — mounted via spec.tools
|
|
385
|
+
// (RFC 判定②(a)) whenever the per-task image chain is actually usable (k8s backend + image catalog), opt-out
|
|
386
|
+
// via SELECT_ENVIRONMENT_TOOL=false. Viewer scope is rebuilt per-execute from the Runner-threaded read-only
|
|
387
|
+
// ctx.principal (auth-channel-derived — a tool argument can never widen visibility).
|
|
1231
388
|
const selectEnvTool = config.selectEnvironmentTool && config.remoteExec?.provider === "k8s" && imageIndex
|
|
1232
389
|
? selectEnvironmentTool({
|
|
1233
390
|
catalog: imageIndex,
|
|
@@ -1235,90 +392,123 @@ async function main() {
|
|
|
1235
392
|
viewerFor: (principal) => ({ operator: explicitOperator(principal, config.operatorPrincipals), tenantId: principal ?? null }),
|
|
1236
393
|
})
|
|
1237
394
|
: undefined;
|
|
1238
|
-
|
|
395
|
+
// Sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2): enables /v1/images/bakes* when a pool exists
|
|
396
|
+
// AND IMAGE_BAKES_ENABLED. The config boot-invariant already fail-fasts an empty OPERATOR_PRINCIPALS (a
|
|
397
|
+
// world-writable build-host-RCE door). image-api stays the UNPRIVILEGED controller; the privileged build runs
|
|
398
|
+
// on the sema-registry-managed bake-runner worker on build-host (it claims + ingests over the internal routes).
|
|
399
|
+
const imageBakes = backend?.imageBake && config.imageBakes.enabled ? backend.imageBake() : undefined; // local omits imageBake() → cloud/fleet-only
|
|
1239
400
|
if (imageBakes)
|
|
1240
401
|
logger.info("image_bakes_enabled", { operators: config.operatorPrincipals.length, runner: config.imageBakes.runnerPrincipal });
|
|
402
|
+
// Durable HITL surface = checkpointStore ALONE (DURABLE_APPROVAL=true). It mounts AskUserQuestion-suspend (the
|
|
403
|
+
// agent asks the human + the portal approval center) AND the resumable checkpoint. The F4 TOOL-APPROVAL
|
|
404
|
+
// gate is an ADDITIVE layer driven by APPROVAL_REQUIRE — empty = no tool is gated, but AskUserQuestion HITL
|
|
405
|
+
// still works. (Decoupled 2026-06-13: previously these were coupled, forcing a placeholder APPROVAL_REQUIRE
|
|
406
|
+
// to get HITL — clay: OA writes don't gate (OA-backend RBAC is the authority), but portal HITL must work.)
|
|
407
|
+
// durableEnabled 已在 runnerDeps 前按同一条件求值([1.294 G1] 前置声明处)——checkpointStore 构造条件与其
|
|
408
|
+
// 同源,恒有 durableEnabled === !!checkpointStore。
|
|
1241
409
|
if (durableEnabled)
|
|
1242
410
|
logger.info("durable_approval_enabled", { toolApprovalGate: config.approvalRequire.length });
|
|
411
|
+
// 修4(三路复审 W3):the fs-write ask gate (resolveSpec, applyTaskSettings wiring below) mounts purely off the
|
|
412
|
+
// host-semantics lane — DELIBERATELY decoupled from any approval face, so no assembly path is ever gateless:
|
|
413
|
+
// with no live coordinator and no durable park, a permissionMode default/auto Write/Edit ask fail-closed
|
|
414
|
+
// auto-denies via core resolveAsk (onAsk absent ⇒ headless auto-deny, loud in-transcript). What that posture
|
|
415
|
+
// must NOT be is silent at boot: an operator who explicitly set TOOL_APPROVAL_ENABLED=false (or a multi-tenant
|
|
416
|
+
// default) would otherwise read the resulting hand-tool denials as an outage. Warn once, at boot.
|
|
1243
417
|
if ((config.remoteExec === undefined || config.remoteExec.provider === "host") && !toolApproval && !durableEnabled) {
|
|
1244
418
|
logger.warn("fs_write_gate_no_responder", {
|
|
1245
419
|
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
420
|
});
|
|
1247
421
|
}
|
|
422
|
+
// [1245]/[1248]② the workflow ask leg attaches on EVERY lane (not fs-dependent — see createWorkflowAskPolicy),
|
|
423
|
+
// so a sandbox-lane deployment running self-orchestration with no approval face hits the same fail-closed
|
|
424
|
+
// auto-deny class. Same loud-at-boot posture; host lanes are already covered by the broader warn above.
|
|
1248
425
|
if (config.selfOrchestrationEnabled && config.remoteExec !== undefined && config.remoteExec.provider !== "host" && !toolApproval && !durableEnabled) {
|
|
1249
426
|
logger.warn("workflow_gate_no_responder", {
|
|
1250
427
|
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
428
|
});
|
|
1252
429
|
}
|
|
430
|
+
// Finding #2: a single-user turnkey worker with no explicit approval/durable/commandPolicy gets an
|
|
431
|
+
// ADJUDICATED auto-accept tool-policy BASELINE (createAllowDenyPolicy — a PRESENT effect-aware policy, so core's
|
|
432
|
+
// write-capable hand tools are never UNGATED / unadjudicated). Log the posture so the auto-accept is OBSERVABLE,
|
|
433
|
+
// not silent; tighten irreversible ops via APPROVAL_REQUIRE / AUTONOMY / commandPolicy (layered tighten-only).
|
|
1253
434
|
if (singleUserAutoAcceptBaseline) {
|
|
435
|
+
// Blanket auto-accept over ALL write/hand/MCP tool surfaces (core's per-surface UNGATED enumeration — hand band /
|
|
436
|
+
// destructive-MCP / first-party-write — is suppressed for single-user turnkey by design). Log it so the posture is
|
|
437
|
+
// observable; tighten (which also re-surfaces per-surface gating) via APPROVAL_REQUIRE / AUTONOMY / commandPolicy.
|
|
1254
438
|
logger.info("tool_policy_single_user_default", {
|
|
1255
439
|
posture: "adjudicated auto-accept — ALL write/hand/MCP tool surfaces auto-allowed (core per-surface UNGATED enumeration suppressed for single-user turnkey)",
|
|
1256
440
|
tighten: "APPROVAL_REQUIRE / AUTONOMY / commandPolicy (re-surfaces + gates specific surfaces)",
|
|
1257
441
|
});
|
|
1258
442
|
}
|
|
443
|
+
// D-G: a boot-time signal that this worker is in crypto-bound direct-door mode (trusted-header path disabled).
|
|
444
|
+
// Pairs with the /health directDoorActive field for the DG-M2 go-live runbook (a positive go signal, not just
|
|
445
|
+
// a behavioral 401). The boot invariant in config.ts already fail-fasts a half-configured direct door.
|
|
1259
446
|
if (config.directDoorActive)
|
|
1260
447
|
logger.info("direct_door_active", { iss: config.principalJwtIss, aud: config.principalJwtAud, operators: config.operatorPrincipals.length });
|
|
448
|
+
// CC-parity (adversarial-review finding): a MULTI-TENANT deployment running self-orchestration WITHOUT worker
|
|
449
|
+
// isolation is an availability risk — the in-process hardened VM is ESCAPE-isolated but NOT RESOURCE-isolated, so an
|
|
450
|
+
// entitled tenant's workflow script can starve the host event loop (microtask loop / OOM), affecting every tenant
|
|
451
|
+
// (src/orchestration/hardened-vm-runner.ts documents this). Loud boot warning so the operator sets
|
|
452
|
+
// SELF_ORCHESTRATION_WORKER_ISOLATION=true. (Single-user is unaffected — the one principal owns its own worker.)
|
|
1261
453
|
if (config.requirePrincipal && config.selfOrchestrationEnabled && !config.selfOrchestrationWorkerIsolation)
|
|
1262
454
|
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." });
|
|
455
|
+
// 🔒 (systematic-audit HIGH fix, defense-in-depth): a multi-tenant deployment (requirePrincipal) with NO service/trace
|
|
456
|
+
// token now correctly fails CLOSED — fleet/trace/session-list are owner-gated, never fleet-wide (isFleetWide, server.ts).
|
|
457
|
+
// Warn the operator: without SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN those endpoints only ever return the caller's own
|
|
458
|
+
// rows (safe, but may surprise an operator expecting cross-tenant ops visibility — set a token for ops-relay).
|
|
1263
459
|
if (config.requirePrincipal && !config.authToken && Object.keys(config.authTokens ?? {}).length === 0 && !config.traceToken)
|
|
1264
460
|
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." });
|
|
461
|
+
// Rate limiter: TiDB-backed (cross-replica SOFT limit, same write-behind counter as the cost quota)
|
|
462
|
+
// when a pool exists, else in-memory per-replica. Both satisfy RateGate. (design/30 P0-2)
|
|
1265
463
|
const rateLimiter = config.rateLimitPerMin > 0
|
|
1266
|
-
? backend?.rateLimiter
|
|
464
|
+
? backend?.rateLimiter // local omits rateLimiter() → in-memory per-process RateLimiter (single-replica)
|
|
1267
465
|
? backend.rateLimiter(config.rateLimitPerMin, counterDegradeHook).startRefresh()
|
|
1268
466
|
: new RateLimiter(config.rateLimitPerMin)
|
|
1269
467
|
: undefined;
|
|
468
|
+
// Capability layer: scenario → {tools (+ subagent roster), prompt, skills}. Heavy bits (skills,
|
|
469
|
+
// git client) are built ONCE here; per request we only select a scenario and bind coordinates.
|
|
1270
470
|
let skills = loadSkills(config.skillsDir);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
const pluginOut = await applyCenterPlugins(skills, effective, {
|
|
1276
|
-
cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
|
|
1277
|
-
allowHosts: config.pluginsAllowHosts,
|
|
1278
|
-
logger,
|
|
1279
|
-
}).catch((err) => {
|
|
1280
|
-
logger.warn("center_plugins_apply_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
1281
|
-
return undefined;
|
|
1282
|
-
});
|
|
1283
|
-
if (pluginOut) {
|
|
1284
|
-
skills = pluginOut.skills;
|
|
1285
|
-
if (pluginOut.failures.length + pluginOut.letGo.length + pluginOut.lkgUsed.length > 0) {
|
|
1286
|
-
logger.info("center_plugins_applied", { failures: pluginOut.failures, letGo: pluginOut.letGo, lkgUsed: pluginOut.lkgUsed });
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
if (bootLkgCandidate && lkgEnabled) {
|
|
1291
|
-
const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
|
|
1292
|
-
logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
|
|
1293
|
-
return false;
|
|
1294
|
-
});
|
|
1295
|
-
{
|
|
1296
|
-
const enabledManifest = (bootLkgCandidate.effective.skills?.skills ?? []).filter((m) => m.enabled !== false);
|
|
1297
|
-
const canonScenarios = (a) => [...(a ?? [])].sort().join("\u0000");
|
|
1298
|
-
const liveSkillMiss = enabledManifest.some((m) => !skills.some((sk) => sk.spec.name === m.name &&
|
|
1299
|
-
skillContentHash(sk.spec.content) === m.contentHash &&
|
|
1300
|
-
sk.spec.description === m.description &&
|
|
1301
|
-
canonScenarios(sk.scenarios) === canonScenarios(m.scenarios)));
|
|
1302
|
-
if (liveSkillMiss)
|
|
1303
|
-
bootSkillStale = true;
|
|
1304
|
-
if (liveSkillMiss && (bootPublished || (!lkgSurvivesRestart && lastPrewarmBodiesVerified))) {
|
|
1305
|
-
pendingRestart = { restartRequired: true, reasons: ["skills"], version: bootLkgCandidate.effective.version, since: Date.now() };
|
|
1306
|
-
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
|
-
}
|
|
1308
|
-
else if (liveSkillMiss || (!bootPublished && lkgSurvivesRestart)) {
|
|
1309
|
-
planeDeferredNoHandoff = { version: bootLkgCandidate.effective.version, since: Date.now(), blocked: liveSkillMiss ? ["skills"] : [], candidate: bootLkgCandidate.effective, ...(bootLkgCandidate.etag !== undefined ? { candidateEtag: bootLkgCandidate.etag } : {}) };
|
|
1310
|
-
logger.warn("config_lkg_boot_publication_retained", { version: bootLkgCandidate.effective.version, liveSkillMiss, durableDeclared: lkgSurvivesRestart, note: (lkgSurvivesRestart ? "boot LKG publication incomplete on a durable-declared deployment — candidate retained; the refresh loop retries after each confirming 304" : "live skill application incomplete on a non-durable deployment and the body cache could not be verified — skills debt retained WITHOUT a restart signal (codex R37: signaling now could restart-storm; each confirming 304 re-verifies and publishes once the bodies prove fetchable)") + (liveSkillMiss && lkgSurvivesRestart ? "; this process runs skill-stale, promotion will publish a skills restart" : "") });
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
if (effective?.mcp)
|
|
1315
|
-
config.mcpServers = resolveMcpServers(effective.mcp, logger);
|
|
471
|
+
// design/158 A10 尾刀:center skills overlay + plugins 消费 + boot LKG 落盘/skill-stale 判定 + mcp
|
|
472
|
+
// 解析搬到 boot/config-center.ts(逐字)。⚠️ 位置即契约:loadSkills 之后、buildScenarios 之前 ——
|
|
473
|
+
// LKG 落盘点必须晚于 skill 正文装载(F7/codex R26),plugins 让位判据要求 plugins 晚于 center 直发 skills。
|
|
474
|
+
skills = await configCenter.applyCenterCapabilities(skills);
|
|
1316
475
|
const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
|
|
476
|
+
// CC-parity: deployment-injected WebSearch backend (the leg core leaves open). Absent WEB_SEARCH_PROVIDER →
|
|
477
|
+
// undefined → the default scenario doesn't assemble the WebSearch tool. The API key stays in the backend closure.
|
|
1317
478
|
const webSearchCfg = webSearchConfigFromEnv();
|
|
1318
479
|
const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
|
|
1319
480
|
if (webSearch)
|
|
1320
481
|
logger.info(`WebSearch enabled (provider=${webSearchCfg.provider}, maxResults=${webSearch.maxResults})`);
|
|
482
|
+
// core 1.382([1561] 提货单①②,design/153 parked 状态机)「同车必接」的 ensureChildSessionDurable 挂点:
|
|
483
|
+
// 只在 checkpointStore 真在场时构造(与 core parkEligible 门「两者全在场才成立」的判据同源——checkpoint
|
|
484
|
+
// 不在场时构造这个闭包毫无意义,连带把「local/无 durable backend 部署不该跑这条 acquire/release」的
|
|
485
|
+
// 边界直接编码进构造条件,不用额外判断)。实现体拆到 `ensureSessionDurable`(session-store.ts,可独立
|
|
486
|
+
// 测试;双复审②抓的 F-7 同族空壳缺口——host 店可能存在 `register()` 留下的零内容 session_meta 行——
|
|
487
|
+
// 的校验也在那边)。
|
|
488
|
+
// 🔴 [1575] F3 修(cli 复查,红先行确认真红):`checkpointStore` 在场 ⇏ `sessionStore` 就是 TiDB 真店
|
|
489
|
+
// ——这是两个独立 config 轴(DB_BACKEND 供 checkpoint/runs 等用途 vs SESSION_BACKEND 单独控 session
|
|
490
|
+
// 落哪),`SESSION_BACKEND` 显式留在默认值时 `sessionStore` 是纯内存 `TtlSessionStore`(无 repo 构造,
|
|
491
|
+
// core `release()` 语义在该形态下是**整删会话**,不是"归还租约")——探针的 finally release 会销毁它
|
|
492
|
+
// 刚证明存在的会话。追加门 `config.sessionBackend === "tidb"`(此刻已被前面装配区从 "auto" 归一,
|
|
493
|
+
// 恒是 "tidb"|"memory" 二选一,时序安全):唯一保留可达的店形正是 release() 真安全的那种
|
|
494
|
+
// (TiDBSessionStore.release 逐字注释「Durable history is retained; just drop any in-flight cache
|
|
495
|
+
// entry」)。非 tidb 场景下 undefined = 子代按 pre-153 settle failed,零回归(与 checkpointStore 缺席
|
|
496
|
+
// 同一处置)。
|
|
497
|
+
// [1593] 只查不迁 → 主动晋升(cli TiDB 真机撞获:bg 子会话住 subRunner 私有 TTL 店、从不在 host 店,
|
|
498
|
+
// 旧形恒 veto → core 当场 expire checkpoint(14ms~2s)+ settle failed,赎回窗口不存在)。晋升链=
|
|
499
|
+
// 快查 host → promoteToHost(整树拷贝+私店 forget+路由记 host)→ 同一严检复验(leaf 非空);任何
|
|
500
|
+
// 一步失败仍如实 veto(pre-153 行为,不放大)。tidb 门与 F3 裁定不变。
|
|
1321
501
|
const ensureChildSessionDurable = checkpointStore && config.sessionBackend === "tidb" ? (sessionId) => ensureChildSessionDurableWithPromotion(sessionStore, subRunnerSessions, sessionId) : undefined;
|
|
502
|
+
// [1900]/[1904] WebFetch 摘要器:core 工厂 + summarize 角色模型(cheap tier;resolveTaskModel 自带
|
|
503
|
+
// 默认回落——与压缩线同族档位,不新造)。构造一次全场景共享;多租户形 WebFetch 整体被 filter,
|
|
504
|
+
// 本注入无效化由 scenarios 现行为保证。
|
|
505
|
+
// 🔴 复审 E-MED-2/E-MED-3 两修:
|
|
506
|
+
// ① **惰性解析**:模型每次调用现取——`config.models`/`config.roles` 会被 center 的 effective
|
|
507
|
+
// 热应用 mutateInPlace 整表替换,boot 期急求值会把摘要器永久 pin 在占位/旧模型上(故障形态是
|
|
508
|
+
// 静默退回上下文放大 + 每次一发无效 LLM 调用)。
|
|
509
|
+
// ② **fail-soft**:core resolveTaskModel 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用;
|
|
510
|
+
// center 发一张既无 summarize 又无 default 的 roles 表就会拒启。解析失败 ⇒ 本次不摘要
|
|
511
|
+
// (core 的 summarize 抛错是 fail-open:回退原文+note),绝不崩 boot。
|
|
1322
512
|
const webFetchSummarize = async (content, prompt, signal) => {
|
|
1323
513
|
const model = (() => {
|
|
1324
514
|
try {
|
|
@@ -1333,14 +523,29 @@ async function main() {
|
|
|
1333
523
|
throw new Error("summarize model unresolved for this deployment");
|
|
1334
524
|
return createWebFetchSummarizer(brain, model)(content, prompt, signal);
|
|
1335
525
|
};
|
|
526
|
+
// design/158 S4:标注 `: ScenarioDeps` —— 无标注的字面量连自己的键名都不校验(打错/多写一个键
|
|
527
|
+
// 只是多一个没人读的属性,编译期无声),这是 1.304 落错家那一族的另一半土壤。
|
|
1336
528
|
const scenarioDeps = {
|
|
1337
529
|
runner, subRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
|
|
1338
530
|
webSearch: webSearch ? webSearch : undefined, metrics, logger, webFetchSummarize,
|
|
531
|
+
// core 1.364 写半场:与 RunnerDeps.backgroundAgentStore 同实例(组装区注释;半接=静默死特性)。
|
|
1339
532
|
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
533
|
+
// core 1.382 parked 状态机「同车必接」一对——checkpointStore 与 main runner 的 checkpointStore(subRunner
|
|
534
|
+
// 构造区上方 const)同实例;ensureChildSessionDurable 见上方构造闭包注释。
|
|
1340
535
|
checkpointStore: checkpointStore ? checkpointStore : undefined,
|
|
1341
536
|
ensureChildSessionDurable: ensureChildSessionDurable ? ensureChildSessionDurable : undefined,
|
|
1342
537
|
oaApiBaseUrl: config.oaApiBaseUrl, oaServiceToken: config.oaServiceToken, oaIssue: config.oaIssue,
|
|
1343
538
|
brandIdentity: config.configProvider === "local",
|
|
539
|
+
// core 1.287 extraTools seam ([802]③ server half): SendUserFile reaches the delegated-child roster. SAME
|
|
540
|
+
// existence gate as the top-level mount (the one sendUserFileToolSpec built above — lane/tenancy/config
|
|
541
|
+
// posture already encoded in its presence). The spec is execute-time late-bound (ctx.taskId →
|
|
542
|
+
// TaskEnvRegistry lookup; emit via ALS to the top-level frame tail), so returning the SAME instance per
|
|
543
|
+
// spawn is correct — per-spawn state lives in the registry, not the spec ([797] confirmed with core).
|
|
544
|
+
// codex F1/F2 gates (subagentSendUserFileExtraTools): (F1) the subRunner carries no toolPolicy and core
|
|
545
|
+
// does not propagate the parent's policy into child specs, so an APPROVAL_DENY/ask-gated SendUserFile was
|
|
546
|
+
// reachable from delegated children — the factory now pre-checks the LIVE deployment approval face
|
|
547
|
+
// (deny/require/neverAuto, canonical-space) and injects nothing when gated (conservative stopgap until a
|
|
548
|
+
// core child-policy seam lands); (F2) read-only builtin agents (Explore/Plan) never receive the tool.
|
|
1344
549
|
subagentExtraTools: sendUserFileToolSpec
|
|
1345
550
|
? subagentSendUserFileExtraTools({
|
|
1346
551
|
spec: sendUserFileToolSpec,
|
|
@@ -1349,7 +554,16 @@ async function main() {
|
|
|
1349
554
|
: undefined,
|
|
1350
555
|
};
|
|
1351
556
|
const scenarios = buildScenarios(scenarioDeps);
|
|
557
|
+
// design/153 件3d:/decide parked 赎回腿的裸 Agent 工具——boot 期从**内建 default** 场景工厂取一份
|
|
558
|
+
// (必须在 center overlay 盖名之前取:赎回腿要的是与 spawn 同源的生产装配,subRunner/bg opts/
|
|
559
|
+
// checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
|
|
560
|
+
// driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
|
|
1352
561
|
const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
|
|
562
|
+
// [1596]/[1597] 跨进程父约束重供席工厂:host 任务的 toolPolicy 解析槽按部署配置**同参重建**
|
|
563
|
+
// (resolveSpec durableEnabled 分支的同款折叠:durableQuestion + durableAsk 四旋钮),单层链
|
|
564
|
+
// count=1,durableMandate=true(host 有 durableApproval 且 durable 车道无 live onAsk 的恒形)。
|
|
565
|
+
// exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
|
|
566
|
+
// 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
|
|
1353
567
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
1354
568
|
? (row) => ({
|
|
1355
569
|
parentConstraints: [{
|
|
@@ -1366,464 +580,55 @@ async function main() {
|
|
|
1366
580
|
}],
|
|
1367
581
|
})
|
|
1368
582
|
: undefined;
|
|
583
|
+
// 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
|
|
584
|
+
// center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
|
|
1369
585
|
const scenarioDetails = builtinScenarioDetails(scenarios, scenarioDeps);
|
|
1370
|
-
const builtinScenarioNames = Object.keys(scenarioDetails);
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
Object.assign(scenarioDetails, centerScenarioDetails(effective.scenarios.scenarios, builtinScenarioNames));
|
|
1375
|
-
if (Object.keys(overlay).length > 0)
|
|
1376
|
-
logger.info("sema_registry_scenarios", { scenarios: Object.keys(overlay), ...(shadows.length > 0 ? { shadowsBuiltin: shadows } : {}) });
|
|
1377
|
-
}
|
|
586
|
+
const builtinScenarioNames = Object.keys(scenarioDetails); // overlay 前捕获(六内建名)
|
|
587
|
+
// design/158 A10 尾刀:center 声明场景 overlay 搬到 boot/config-center.ts(逐字)。⚠️ 位置即契约:
|
|
588
|
+
// 必须在内建 details + 六内建名捕获之后 —— 探针要打纯内建工厂,不是被 center 顶掉的。
|
|
589
|
+
configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
|
|
1378
590
|
logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
|
|
591
|
+
// Background reapers: recover runs whose instance died, expire never-answered approvals, sweep
|
|
592
|
+
// rate windows. Unref'd so they never hold the process open.
|
|
593
|
+
// design/80 D-D (SLA-timer): the deny-sweep reuses the server's resumeCheckpoint, so it is wired in AFTER
|
|
594
|
+
// createHttpServer returns it (below) via this holder — the reaper is defined before the server.
|
|
1379
595
|
let runDenySweep;
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
? async () => {
|
|
1391
|
-
const store = backgroundAgentStore;
|
|
1392
|
-
const scopes = await (store.listScopes?.() ?? Promise.resolve(["default"]));
|
|
1393
|
-
let rows = 0, sessions = 0, skipped = 0, failedScopes = 0;
|
|
1394
|
-
for (const scope of scopes) {
|
|
1395
|
-
try {
|
|
1396
|
-
if (checkpointStore) {
|
|
1397
|
-
const pr = await defaultTaskRegistry.reconcileParkedAgents({ agentStore: store, checkpointStore }, scope, Date.now(), { staleClaimMaxAgeMs: config.backgroundAgentParkClaimStaleMs });
|
|
1398
|
-
if (pr.failed > 0 || pr.rolledBack > 0) {
|
|
1399
|
-
metrics.inc("bg_agents_park_reconciled_failed_total", {}, pr.failed);
|
|
1400
|
-
metrics.inc("bg_agents_park_reconciled_rolledback_total", {}, pr.rolledBack);
|
|
1401
|
-
logger.info("reaper_swept", { metric: "bg_agents_park_reconciled", scope, failed: pr.failed, rolledBack: pr.rolledBack });
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
const r = await defaultTaskRegistry.reapDurableAgents(scope, { store, sessions: subRunner.sessions, ...(mailboxStore ? { mailbox: mailboxStore } : {}) }, {
|
|
1405
|
-
maxAgeMs: config.backgroundAgentRetentionMs,
|
|
1406
|
-
staleRunningMaxAgeMs: config.backgroundAgentStaleRunningMs,
|
|
1407
|
-
});
|
|
1408
|
-
rows += r.rowsReaped;
|
|
1409
|
-
sessions += r.sessionsReleased;
|
|
1410
|
-
skipped += r.skippedNoSessions;
|
|
1411
|
-
}
|
|
1412
|
-
catch (err) {
|
|
1413
|
-
failedScopes++;
|
|
1414
|
-
logger.warn("bg_agent_reap_scope_failed", { scope, err: err instanceof Error ? err.message : String(err) });
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
if (failedScopes > 0)
|
|
1418
|
-
metrics.inc("bg_agent_reap_scope_failures_total", {}, failedScopes);
|
|
1419
|
-
if (rows > 0 || sessions > 0) {
|
|
1420
|
-
metrics.inc("bg_agents_reaped_total", {}, rows);
|
|
1421
|
-
logger.info("reaper_swept", { metric: "bg_agents_reaped_total", count: rows, sessionsReleased: sessions, skippedNoSessions: skipped });
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
: undefined;
|
|
1425
|
-
const probeClockSkew = async () => {
|
|
1426
|
-
try {
|
|
1427
|
-
const dbMs = await backend?.dbNowMs?.();
|
|
1428
|
-
if (typeof dbMs === "number" && Number.isFinite(dbMs)) {
|
|
1429
|
-
metrics.setGauge("fleet_counter_bucket_skew_ms", dbMs - Date.now());
|
|
1430
|
-
metrics.setGauge("fleet_clock_probe_ok", 1);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
catch {
|
|
1434
|
-
metrics.setGauge("fleet_clock_probe_ok", 0);
|
|
1435
|
-
}
|
|
1436
|
-
};
|
|
1437
|
-
const reaper = setInterval(() => {
|
|
1438
|
-
void probeClockSkew().catch(() => undefined);
|
|
1439
|
-
void runStore?.reapStale(config.runStaleSec * 1000).then(reapCount("runs_reaped_total", { kind: "stale" })).catch(() => undefined);
|
|
1440
|
-
if (checkpointStore) {
|
|
1441
|
-
void checkpointStore.reapExpired(Date.now()).then(reapCount("checkpoints_reaped_total", {})).catch(() => undefined);
|
|
1442
|
-
void runStore?.failSuspendedWithExpiredCheckpoint().then(reapCount("runs_reaped_total", { kind: "expired_checkpoint" })).catch(() => undefined);
|
|
1443
|
-
void runDenySweep?.(Date.now()).catch(() => undefined);
|
|
1444
|
-
}
|
|
1445
|
-
if (config.approvalTimeoutSec > 0) {
|
|
1446
|
-
void approvalStore?.expireStale(config.approvalTimeoutSec * 1000).catch(() => undefined);
|
|
1447
|
-
void runStore?.reapSuspended(config.approvalTimeoutSec * 1000).then(reapCount("runs_reaped_total", { kind: "suspended" })).catch(() => undefined);
|
|
1448
|
-
}
|
|
1449
|
-
void checkpointStore?.reapCtx(Date.now() - config.runStaleSec * 1000).catch(() => undefined);
|
|
1450
|
-
rateLimiter?.sweep();
|
|
1451
|
-
if (costQuota instanceof CostQuota)
|
|
1452
|
-
costQuota.reap();
|
|
1453
|
-
void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined);
|
|
1454
|
-
void fileSnapshotStore?.sweepOrphanBlobs?.().catch(() => undefined);
|
|
1455
|
-
void taskAttachmentStore?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs).then(reapCount("attachments_reaped_total", {})).catch(() => undefined);
|
|
1456
|
-
if (config.attachmentOrphanGraceMs > 0 && !attachmentSweepInFlight && taskAttachmentStore?.sweepOrphanObjects) {
|
|
1457
|
-
attachmentSweepInFlight = true;
|
|
1458
|
-
void taskAttachmentStore
|
|
1459
|
-
.sweepOrphanObjects(config.attachmentOrphanGraceMs)
|
|
1460
|
-
.then((n) => { if (n > 0) {
|
|
1461
|
-
metrics.inc("attachment_orphan_objects_swept_total", {}, n);
|
|
1462
|
-
logger.info("attachment_orphan_objects_swept", { removed: n });
|
|
1463
|
-
} })
|
|
1464
|
-
.catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }))
|
|
1465
|
-
.finally(() => { attachmentSweepInFlight = false; });
|
|
1466
|
-
}
|
|
1467
|
-
void backend?.session()?.sweepStagingSessions?.().catch(() => undefined);
|
|
1468
|
-
void imageBakes?.reapStaleBakes(config.imageBakes.staleMs).then(reapCount("bakes_reaped_total", {})).catch(() => undefined);
|
|
1469
|
-
void worktreeReap?.();
|
|
1470
|
-
void workflowNotifyGate?.recover({ orphanGraceMs: config.workflowOrphanGraceMs }).catch(() => undefined);
|
|
1471
|
-
void workflowJournalStore?.reapExpired?.(Date.now(), config.workflowJournalRetentionMs).catch(() => undefined);
|
|
1472
|
-
if (!wfRunReapInFlight) {
|
|
1473
|
-
const sweep = sqlWorkflowRunStore?.reapAllScopes?.(Date.now(), { maxAgeMs: config.workflowRunRetentionMs });
|
|
1474
|
-
if (sweep) {
|
|
1475
|
-
wfRunReapInFlight = true;
|
|
1476
|
-
void sweep.catch(() => undefined).finally(() => (wfRunReapInFlight = false));
|
|
1477
|
-
}
|
|
1478
|
-
void workflowNotifyJournal
|
|
1479
|
-
?.reapAcked?.(Date.now() - config.workflowRunRetentionMs)
|
|
1480
|
-
.catch(() => undefined);
|
|
1481
|
-
}
|
|
1482
|
-
if (reapBgAgents && !bgAgentReapInFlight) {
|
|
1483
|
-
bgAgentReapInFlight = true;
|
|
1484
|
-
void reapBgAgents().catch(() => undefined).finally(() => (bgAgentReapInFlight = false));
|
|
1485
|
-
}
|
|
1486
|
-
void rosterStore
|
|
1487
|
-
?.reapOlderThan?.(Date.now() - config.rosterRetentionMs)
|
|
1488
|
-
.then(reapCount("roster_rows_reaped_total", {}))
|
|
1489
|
-
.catch(() => undefined);
|
|
1490
|
-
if (config.scratchpadSweepTtlMs > 0) {
|
|
1491
|
-
void sweepStaleScratchpads(config.localDataRoot ?? localRoot, { olderThanMs: config.scratchpadSweepTtlMs })
|
|
1492
|
-
.then((removed) => {
|
|
1493
|
-
if (removed > 0)
|
|
1494
|
-
logger.info("scratchpads_swept", { removed });
|
|
1495
|
-
})
|
|
1496
|
-
.catch(() => undefined);
|
|
1497
|
-
}
|
|
1498
|
-
}, config.reapIntervalSec * 1000);
|
|
1499
|
-
reaper.unref?.();
|
|
596
|
+
// design/158 A10:reaper 段搬到 src/boot/reapers.ts(逐字;deny-sweep 晚绑改取值,见该文件头注)。
|
|
597
|
+
const reaper = startReapers({
|
|
598
|
+
config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, approvalStore,
|
|
599
|
+
rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap,
|
|
600
|
+
workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
|
|
601
|
+
backgroundAgentStore, mailboxStore,
|
|
602
|
+
getRunDenySweep: () => runDenySweep,
|
|
603
|
+
});
|
|
604
|
+
// Optional OTLP/HTTP metrics export (1.37). Periodically pushes the registry to an OTel collector;
|
|
605
|
+
// best-effort (a collector outage is logged, never affects serving). /metrics stays available too.
|
|
1500
606
|
const otelExporter = config.otel
|
|
1501
607
|
? startOtlpExporter(metrics, {
|
|
1502
608
|
...config.otel,
|
|
1503
609
|
onError: (err) => logger.warn("otel_export_failed", { err: String(err) }),
|
|
1504
610
|
})
|
|
1505
611
|
: undefined;
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
|
|
1523
|
-
const skillsProofPromotion = !promoted && !lkgSurvivesRestart && skillsDebt && lastPrewarmBodiesVerified && planeDeferredNoHandoff.planeDeferred !== true;
|
|
1524
|
-
if (skillsProofPromotion) {
|
|
1525
|
-
pendingRestart = { restartRequired: true, reasons: ["skills"], version: planeDeferredNoHandoff.version, since: Date.now() };
|
|
1526
|
-
logger.info("config_boot_skill_debt_promoted", { version: planeDeferredNoHandoff.version, note: "skill bodies verified on a source-confirmed-current candidate — the withheld skills restart is now published (non-durable lane, repair proven)" });
|
|
1527
|
-
planeDeferredNoHandoff = undefined;
|
|
1528
|
-
}
|
|
1529
|
-
else if (promoted) {
|
|
1530
|
-
const promotedReasons = [...new Set([...(planeDeferredNoHandoff.blocked ?? []), ...(planeDeferredNoHandoff.planeDeferred ? ["models-tiers"] : [])])];
|
|
1531
|
-
if (promotedReasons.length > 0) {
|
|
1532
|
-
pendingRestart = { restartRequired: true, reasons: promotedReasons, version: planeDeferredNoHandoff.version, since: Date.now() };
|
|
1533
|
-
}
|
|
1534
|
-
logger.info("models_tiers_deferred_promoted", { version: planeDeferredNoHandoff.version, reasons: promotedReasons, note: promotedReasons.length > 0 ? "handoff persistence recovered on a source-confirmed-current candidate — the withheld restart signal is now published" : "boot publication retry completed — LKG now durable, no restart needed (nothing was withheld)" });
|
|
1535
|
-
planeDeferredNoHandoff = undefined;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
if (r) {
|
|
1539
|
-
const promptsRaw = r.effective.prompts;
|
|
1540
|
-
const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
|
|
1541
|
-
const badDomains = [
|
|
1542
|
-
...new Set([...(r.domainErrors ?? []).map((de) => de.domain), ...(promptsGate.ok ? [] : ["prompts"])]),
|
|
1543
|
-
]
|
|
1544
|
-
.sort()
|
|
1545
|
-
.join(",");
|
|
1546
|
-
if (badDomains) {
|
|
1547
|
-
if (lastRejectedCandidate?.version !== r.effective.version || lastRejectedCandidate.domains !== badDomains) {
|
|
1548
|
-
lastRejectedCandidate = { version: r.effective.version, domains: badDomains, at: Date.now() };
|
|
1549
|
-
logger.warn("config_candidate_rejected", {
|
|
1550
|
-
version: r.effective.version,
|
|
1551
|
-
domains: badDomains,
|
|
1552
|
-
errors: [
|
|
1553
|
-
...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })),
|
|
1554
|
-
...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }]),
|
|
1555
|
-
],
|
|
1556
|
-
lkgVersion: latestEffective?.version ?? "(env)",
|
|
1557
|
-
note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
lastRejectedCandidate = undefined;
|
|
1563
|
-
if (ccRef?.dryRun) {
|
|
1564
|
-
logEffectiveDiff(config, r.effective, logger);
|
|
1565
|
-
ccEtag = r.etag;
|
|
1566
|
-
}
|
|
1567
|
-
else {
|
|
1568
|
-
const planeDeferred = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
|
|
1569
|
-
applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferred ? { deferModelPlane: true } : {}) });
|
|
1570
|
-
if (planeDeferred) {
|
|
1571
|
-
logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner: the changed model plane (models/roles/tiers/default) is NOT hot-applied — admission stays on the Runner's generation; restart applies the new plane (models-tiers restart signal rides /health)" });
|
|
1572
|
-
}
|
|
1573
|
-
else {
|
|
1574
|
-
appliedPlaneEff = r.effective;
|
|
1575
|
-
planeDeferredNoHandoff = undefined;
|
|
1576
|
-
}
|
|
1577
|
-
await adoptCenterPrompts(r.effective, "refresh");
|
|
1578
|
-
if (!planeDeferred)
|
|
1579
|
-
markRosterLanded(r.effective);
|
|
1580
|
-
mutateInPlace(pricing, buildPricing(config.models));
|
|
1581
|
-
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
1582
|
-
const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
|
|
1583
|
-
const reasons = restartReasons(effective, r.effective);
|
|
1584
|
-
const skillsChangeDetected = reasons.includes("skills");
|
|
1585
|
-
if (bootSkillStale && !skillsChangeDetected)
|
|
1586
|
-
reasons.push("skills");
|
|
1587
|
-
if (planeDeferred) {
|
|
1588
|
-
if (lkgPersisted) {
|
|
1589
|
-
if (!reasons.includes("models-tiers"))
|
|
1590
|
-
reasons.push("models-tiers");
|
|
1591
|
-
planeDeferredNoHandoff = undefined;
|
|
1592
|
-
}
|
|
1593
|
-
else {
|
|
1594
|
-
const blocked = reasons.splice(0, reasons.length);
|
|
1595
|
-
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
|
-
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
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
else if (lkgSurvivesRestart && !lkgPersisted) {
|
|
1600
|
-
const blocked = reasons.splice(0, reasons.length);
|
|
1601
|
-
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1602
|
-
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
|
-
}
|
|
1604
|
-
else if (!planeDeferred && !lkgSurvivesRestart && bootSkillStale && !skillsChangeDetected && !lastPrewarmBodiesVerified) {
|
|
1605
|
-
const i = reasons.indexOf("skills");
|
|
1606
|
-
if (i >= 0) {
|
|
1607
|
-
reasons.splice(i, 1);
|
|
1608
|
-
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked: ["skills"], candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1609
|
-
logger.warn("config_boot_skill_debt_withheld", { version: r.effective.version, note: "boot skill debt still unproven on a non-durable deployment (body cache does not verify) — skills restart withheld this tick; re-verified each tick and published once the bodies prove fetchable" });
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
if (reasons.length === 0) {
|
|
1613
|
-
pendingRestart = undefined;
|
|
1614
|
-
}
|
|
1615
|
-
else if (!pendingRestart || pendingRestart.reasons.join(",") !== reasons.join(",")) {
|
|
1616
|
-
pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
|
|
1617
|
-
}
|
|
1618
|
-
else {
|
|
1619
|
-
pendingRestart = { ...pendingRestart, version: r.effective.version };
|
|
1620
|
-
}
|
|
1621
|
-
logger.info("sema_registry_refreshed", {
|
|
1622
|
-
version: r.effective.version,
|
|
1623
|
-
note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
|
|
1624
|
-
...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
|
|
1625
|
-
});
|
|
1626
|
-
latestEffective = r.effective;
|
|
1627
|
-
ccEtag = r.etag;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
catch (err) {
|
|
1632
|
-
logger.warn("sema_registry_refresh_failed", { err: String(err), note: "etag NOT advanced — the same candidate is re-fetched and re-applied next poll" });
|
|
1633
|
-
}
|
|
1634
|
-
finally {
|
|
1635
|
-
refreshInFlight = false;
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
const ccTimer = setInterval(() => void refreshTick(), 60_000);
|
|
1639
|
-
ccTimer.unref?.();
|
|
1640
|
-
const deferredBootApply = async (r) => {
|
|
1641
|
-
if (!r)
|
|
1642
|
-
return;
|
|
1643
|
-
if (refreshInFlight)
|
|
1644
|
-
return;
|
|
1645
|
-
refreshInFlight = true;
|
|
1646
|
-
try {
|
|
1647
|
-
await deferredBootApplyInner(r);
|
|
1648
|
-
}
|
|
1649
|
-
finally {
|
|
1650
|
-
refreshInFlight = false;
|
|
1651
|
-
}
|
|
1652
|
-
};
|
|
1653
|
-
const deferredBootApplyInner = async (r) => {
|
|
1654
|
-
const promptsRaw = r.effective.prompts;
|
|
1655
|
-
const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
|
|
1656
|
-
if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
|
|
1657
|
-
logger.warn("config_boot_deferred_candidate_rejected", {
|
|
1658
|
-
version: r.effective.version,
|
|
1659
|
-
errors: [...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })), ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }])],
|
|
1660
|
-
note: "late boot pull carried an invalid candidate — env fallback keeps serving; the refresh cadence re-judges the same tree (etag not advanced)",
|
|
1661
|
-
});
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
if (ccRef?.dryRun) {
|
|
1665
|
-
logEffectiveDiff(config, r.effective, logger);
|
|
1666
|
-
ccEtag = r.etag;
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
const planeDeferredLate = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
|
|
1670
|
-
applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferredLate ? { deferModelPlane: true } : {}) });
|
|
1671
|
-
if (planeDeferredLate)
|
|
1672
|
-
logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner (env tiers): the late-boot center model plane is NOT hot-applied — restart applies it" });
|
|
1673
|
-
else {
|
|
1674
|
-
appliedPlaneEff = r.effective;
|
|
1675
|
-
planeDeferredNoHandoff = undefined;
|
|
1676
|
-
}
|
|
1677
|
-
await adoptCenterPrompts(r.effective, "boot-deferred");
|
|
1678
|
-
if (!planeDeferredLate)
|
|
1679
|
-
markRosterLanded(r.effective);
|
|
1680
|
-
mutateInPlace(pricing, buildPricing(config.models));
|
|
1681
|
-
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
1682
|
-
effective = r.effective;
|
|
1683
|
-
latestEffective = r.effective;
|
|
1684
|
-
ccEtag = r.etag;
|
|
1685
|
-
const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
|
|
1686
|
-
const reasons = restartReasons(undefined, r.effective);
|
|
1687
|
-
if (planeDeferredLate) {
|
|
1688
|
-
if (lkgPersistedLate) {
|
|
1689
|
-
if (!reasons.includes("models-tiers"))
|
|
1690
|
-
reasons.push("models-tiers");
|
|
1691
|
-
planeDeferredNoHandoff = undefined;
|
|
1692
|
-
}
|
|
1693
|
-
else {
|
|
1694
|
-
const blocked = reasons.splice(0, reasons.length);
|
|
1695
|
-
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
|
-
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
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
else if (planeDeferredLate === false && lkgSurvivesRestart && !lkgPersistedLate) {
|
|
1700
|
-
const blocked = reasons.splice(0, reasons.length);
|
|
1701
|
-
planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
|
|
1702
|
-
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" });
|
|
1703
|
-
}
|
|
1704
|
-
if (reasons.length > 0)
|
|
1705
|
-
pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
|
|
1706
|
-
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
|
-
};
|
|
1708
|
-
if (bootConfigPending)
|
|
1709
|
-
void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
|
|
1710
|
-
}
|
|
1711
|
-
let keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
1712
|
-
if (process.env.ANTHROPIC_AUTH_TOKEN && !process.env.ANTHROPIC_API_KEY && (Object.keys(config.modelApiKeys).length > 0 || Object.keys(config.modelApiKeyEnv).length > 0)) {
|
|
1713
|
-
logger.warn("per_model_key_bearer_gateway_combo", {
|
|
1714
|
-
perModelKeys: Object.keys(config.modelApiKeys).length + Object.keys(config.modelApiKeyEnv).length,
|
|
1715
|
-
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
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
const mysqlPool = backend?.mysqlPool();
|
|
1719
|
-
const auditPgPool = backend?.pgPool();
|
|
1720
|
-
const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
|
|
1721
|
-
const ownerAware = sessionStore;
|
|
1722
|
-
const sessionAudit = mysqlPool || auditPgPool || auditLocalRoot
|
|
1723
|
-
? async (sessionId) => {
|
|
1724
|
-
const audit = mysqlPool
|
|
1725
|
-
? await buildSessionAudit(mysqlPool, sessionId)
|
|
1726
|
-
: auditPgPool
|
|
1727
|
-
? await buildSessionAuditPg(auditPgPool, sessionId)
|
|
1728
|
-
: await buildSessionAuditLocal(auditLocalRoot, sessionId);
|
|
1729
|
-
if (!audit)
|
|
1730
|
-
return undefined;
|
|
1731
|
-
return { ...audit, owner: (await ownerAware.ownerOf?.(sessionId)) ?? null };
|
|
1732
|
-
}
|
|
1733
|
-
: undefined;
|
|
1734
|
-
const sessionWatchRegistry = ownerAware.getHead && ownerAware.ownerOf
|
|
1735
|
-
? new SessionWatchRegistry(async (sid) => (await ownerAware.getHead(sid)) ?? { owner: undefined, leafId: null }, {
|
|
1736
|
-
hotMs: posIntEnv(process.env.SESSION_EVENTS_HOT_MS, 200),
|
|
1737
|
-
warmMs: posIntEnv(process.env.SESSION_EVENTS_WARM_MS, 2000),
|
|
1738
|
-
maxWatchedSessions: posIntEnv(process.env.SESSION_EVENTS_MAX_PROBES, 512, 100_000),
|
|
1739
|
-
})
|
|
1740
|
-
: undefined;
|
|
1741
|
-
setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
|
|
1742
|
-
const purgeSession = ownerAware.deleteSession
|
|
1743
|
-
? async (sessionId, owner) => {
|
|
1744
|
-
const runs = runStore ? await runStore.deleteBySession(sessionId, owner) : { removed: 0 };
|
|
1745
|
-
if ("active" in runs)
|
|
1746
|
-
return { active: runs.active };
|
|
1747
|
-
if (checkpointStore)
|
|
1748
|
-
await checkpointStore.deleteBySession(sessionId, owner);
|
|
1749
|
-
if (toolResultStore)
|
|
1750
|
-
await toolResultStore.deleteBySession?.(sessionId);
|
|
1751
|
-
if (resumeAnchorStore)
|
|
1752
|
-
await resumeAnchorStore.deleteBySession(sessionId);
|
|
1753
|
-
if (approvalExemptionStore)
|
|
1754
|
-
await approvalExemptionStore.deleteBySession(sessionId);
|
|
1755
|
-
if (sessionPolicyStore?.deleteBySession)
|
|
1756
|
-
await sessionPolicyStore.deleteBySession(sessionId);
|
|
1757
|
-
if (taskAttachmentStore)
|
|
1758
|
-
await taskAttachmentStore.deleteBySession(sessionId);
|
|
1759
|
-
if (fileSnapshotStore?.deleteBySession)
|
|
1760
|
-
await fileSnapshotStore.deleteBySession(sessionId);
|
|
1761
|
-
if (workflowCompletionInbox)
|
|
1762
|
-
await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
|
|
1763
|
-
await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
|
|
1764
|
-
try {
|
|
1765
|
-
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
|
|
1766
|
-
if (reaped > 0)
|
|
1767
|
-
logger.info("session_background_reaped", { sessionId, reaped });
|
|
1768
|
-
}
|
|
1769
|
-
catch (err) {
|
|
1770
|
-
logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
|
|
1771
|
-
}
|
|
1772
|
-
const historyDeleted = await ownerAware.deleteSession(sessionId, owner);
|
|
1773
|
-
sessionWatchRegistry?.dropSession(sessionId);
|
|
1774
|
-
return { deleted: historyDeleted || runs.removed > 0 };
|
|
1775
|
-
}
|
|
1776
|
-
: undefined;
|
|
1777
|
-
const instrumentDegenerate = mysqlPool ? makeDegenerateInstrument(mysqlPool, metrics, logger) : undefined;
|
|
1778
|
-
const planCacheProbe = new PlanCacheProbe(metrics, logger);
|
|
1779
|
-
const leaderMinio = process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
1780
|
-
? {
|
|
1781
|
-
s3: {
|
|
1782
|
-
endpoint: process.env.MINIO_ENDPOINT,
|
|
1783
|
-
bucket: process.env.MINIO_BUCKET ?? "workspaces",
|
|
1784
|
-
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
1785
|
-
secretKey: process.env.MINIO_SECRET_KEY,
|
|
1786
|
-
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
1787
|
-
},
|
|
1788
|
-
}
|
|
1789
|
-
: {};
|
|
1790
|
-
const leaderProvider = config.remoteExec?.provider;
|
|
1791
|
-
const leaderEndpoint = config.leaderEnabled &&
|
|
1792
|
-
(leaderProvider === "e2b" ||
|
|
1793
|
-
(leaderProvider === "k8s" && "s3" in leaderMinio && executionEnvFactory) ||
|
|
1794
|
-
(leaderProvider === "host" && executionEnvFactory))
|
|
1795
|
-
? createLeaderEndpoint(createLeaderRunner({
|
|
1796
|
-
brain, models: config.models, roles: config.roles, pricing, logger,
|
|
1797
|
-
fanoutEnabled: config.leaderFanoutEnabled,
|
|
1798
|
-
...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
|
|
1799
|
-
? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
|
|
1800
|
-
: {}),
|
|
1801
|
-
...(leaderProvider === "e2b" ? { e2bApiKey: config.remoteExec.apiKey } : {}),
|
|
1802
|
-
...(executionEnvFactory ? { envFactory: executionEnvFactory } : {}),
|
|
1803
|
-
...leaderMinio,
|
|
1804
|
-
...(toolResultStore ? { toolResultStore } : {}),
|
|
1805
|
-
...(leaderProvider === "k8s" ? { workspace: "/workspace" } : {}),
|
|
1806
|
-
sessionStore,
|
|
1807
|
-
...(checkpointStore && config.approvalRequire.length > 0
|
|
1808
|
-
? {
|
|
1809
|
-
durable: {
|
|
1810
|
-
checkpointStore,
|
|
1811
|
-
requireApproval: config.approvalRequire,
|
|
1812
|
-
deny: config.approvalDeny,
|
|
1813
|
-
autoBudget: config.approvalAutoBudget,
|
|
1814
|
-
neverAuto: config.approvalNeverAuto,
|
|
1815
|
-
...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
|
|
1816
|
-
},
|
|
1817
|
-
}
|
|
1818
|
-
: {}),
|
|
1819
|
-
}), {
|
|
1820
|
-
logger,
|
|
1821
|
-
requiredFields: ["objective", "durableRemote", "testCmd", "seedCmd", "baseSha"],
|
|
1822
|
-
})
|
|
1823
|
-
: undefined;
|
|
1824
|
-
if (leaderEndpoint)
|
|
1825
|
-
logger.info("leader_endpoint_enabled", { provider: leaderProvider, mode: "s3" in leaderMinio ? "factory" : "static" });
|
|
612
|
+
// design/158 A10 尾刀:60s refresh 循环 + boot-deferred 到货续接搬到 boot/config-center.ts(逐字)。
|
|
613
|
+
// ⚠️ 位置即契约:必须在 new Runner 之后 —— runnerTierFrozen 与构造函数读的是同一 config 代际。
|
|
614
|
+
configCenter.startRefreshLoop({ runnerTierFrozen, pricing });
|
|
615
|
+
// design/158 A10 尾刀:per-model key resolver 首建(+ Bearer 网关组合告警)搬到 boot/config-center.ts。
|
|
616
|
+
// ⚠️ 位置即契约:首建读的必须是 boot apply **之后**的 config.modelApiKeyEnv/modelApiKeys。
|
|
617
|
+
configCenter.initKeyResolver();
|
|
618
|
+
// design/158 A10:session 只读/删除面搬到 src/boot/session-faces.ts(逐字;watch→purge 次序即契约,见该文件头注)。
|
|
619
|
+
const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
|
|
620
|
+
config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore,
|
|
621
|
+
resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore,
|
|
622
|
+
workflowCompletionInbox,
|
|
623
|
+
});
|
|
624
|
+
// design/158 A10:leader 段搬到 src/boot/leader.ts(逐字)。
|
|
625
|
+
const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore });
|
|
626
|
+
// Graceful drain: shared mutable state — SIGTERM flips `draining` (shutdown below), createHttpServer
|
|
627
|
+
// assigns `inflight` (live leg count on this instance), /health + the submit 503 gate read it.
|
|
1826
628
|
const drainState = { draining: false };
|
|
629
|
+
// 鉴权桥(registry-core api/auth-bridge):SEMA_REGISTRY_URL 在位即点亮(零新配置——worker 已信
|
|
630
|
+
// registry 的 effective config,同一信任根延伸到其 JWKS);split 部署用 AUTH_BRIDGE_ISSUER/JWKS_URL/AUDIENCE
|
|
631
|
+
// 显式覆盖。缺 registry(纯 env 部署)=分支不存在,bearer 行为与今天字节一致。
|
|
1827
632
|
const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
|
|
1828
633
|
const registryJwtVerifier = authBridgeIssuer
|
|
1829
634
|
? createRegistryJwtVerifier({
|
|
@@ -1836,520 +641,170 @@ async function main() {
|
|
|
1836
641
|
: undefined;
|
|
1837
642
|
if (registryJwtVerifier)
|
|
1838
643
|
logger.info("auth_bridge_enabled", { issuer: authBridgeIssuer });
|
|
644
|
+
// asyncRewake wake 管道 holder:server 启动时装 deliver(session→本副本活流 steer);createTaskHooks 的
|
|
645
|
+
// ctx.wake 闭包经它投递(main 建壳/server 填实,双向不循环依赖)。
|
|
1839
646
|
const hookWakeBus = {};
|
|
647
|
+
// ═══════════ design/158 A8:ServiceDeps 分组装配(七组窄接口)═══════════
|
|
648
|
+
// 每组一个**带类型标注**的中间 const —— 标注位触发 TS 多余属性检查,键放错组当场编译红。
|
|
649
|
+
// (平铺形做不到:69 键的全可选面里任何键名都「属于」它,于是键落错家/从未接线只能靠源码钉
|
|
650
|
+
// 逐个盯 —— 1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族。)组形状门见
|
|
651
|
+
// test/deps-literal-shape-gate.test.ts;消费点仍读平铺 `deps.x`(createHttpServer 入口摊平)。
|
|
652
|
+
/** durable 持久面(键 present ⇔ 路由/能力位在场;absent ⇒ 501 诚实缺席) */
|
|
1840
653
|
const stores = {
|
|
1841
654
|
runStore,
|
|
1842
|
-
resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
|
|
655
|
+
resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined, // E18 resume-at eventId→entryId map (tidb/pg/local)
|
|
1843
656
|
approvalStore,
|
|
1844
|
-
approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined,
|
|
657
|
+
approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined, // decide remember="session" + list/revoke
|
|
1845
658
|
checkpointStore,
|
|
1846
|
-
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
|
|
659
|
+
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined, // E6 operator session-rule store (PUT/GET /v1/sessions/:id/policy)
|
|
660
|
+
// E19 cap: core's gate-split (1.134.0) REMOVED the isRemoteExecutionEnv skip — core now snapshots each
|
|
661
|
+
// completed turn + restores on resumeAt for ANY ExecutionEnv when fileSnapshotStore is wired (captureManifest/
|
|
662
|
+
// applyManifest run over any env's FileSystem ops; a 30s timeout bounds a slow remote walk). So rewind works for
|
|
663
|
+
// host/e2b/k8s/ssh/adb/local-docker AND the in-process worker → advertise `rewindFiles` whenever the store is wired.
|
|
1847
664
|
fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
|
|
1848
|
-
taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
|
|
665
|
+
taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined, // D-1 上传/取回/删除三动词面
|
|
666
|
+
// design/153 件3d(/decide parked 赎回腿):durable bg 行店 + boot 裸 Agent 工具,与 RunnerDeps/
|
|
667
|
+
// scenarioDeps 同实例(claim/expire/consumeParkedFlip 作用于同一行)。任一缺席=分支不存在。
|
|
1849
668
|
backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
|
|
669
|
+
// §0.5 session-ownership: the durable session abstraction (SessionRepo-style list/fork/delete) + the delete
|
|
670
|
+
// coordinator. `ownerAware` is the (possibly cache-wrapped) session store; both delegate to the DB-backed
|
|
671
|
+
// TiDB/PG store. Undefined-safe: on an in-memory dev store these carry no SessionRepo ops → the routes 501.
|
|
1850
672
|
sessionStorage: ownerAware,
|
|
1851
673
|
workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
|
|
1852
|
-
workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
|
|
674
|
+
workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined, // [1402] journal 读面(GET /v1/workflows/:id/journal 数据源,与 runner 同实例)
|
|
1853
675
|
imageIndex,
|
|
1854
676
|
imageBakes,
|
|
1855
|
-
outcomeSink: outcomeSink ? outcomeSink : undefined,
|
|
1856
|
-
sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
|
|
677
|
+
outcomeSink: outcomeSink ? outcomeSink : undefined, // P2: GET /v1/outcomes 读面(design/73 §7.2 仪表盘底料)
|
|
678
|
+
sendFileLedger: sendFileLedger ? sendFileLedger : undefined, // SendUserFile 账本读面:GET /v1/sendfile-links(owner-scope 镜像 sessions list)
|
|
679
|
+
// 2c session-sync (P1d): the WHOLE backend so the /v1/sessions/:id/sync/* routes can call exportSession/
|
|
680
|
+
// importSession (they fan out across session entries + fileSnapshot + sessionPolicy + resumeAnchor, which must
|
|
681
|
+
// come from ONE backend in the §8 atomic order). Undefined-safe: the env-only/no-backend worker 501s the routes.
|
|
1857
682
|
backend: backend ? backend : undefined,
|
|
1858
683
|
};
|
|
684
|
+
/** 进程内活体协调面(本副本 only,重启即空:HITL 协调器/可 steer 句柄注册表/总线/收件箱) */
|
|
1859
685
|
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,
|
|
686
|
+
elicitation: elicitation ? elicitation : undefined, // E23: live-only inbound-elicitation coordinator (respond route + ALS wraps)
|
|
687
|
+
question: question ? question : undefined, // §4④: AskUserQuestion live coordinator (respond route + ALS wraps on streaming legs)
|
|
688
|
+
toolApproval: toolApproval ? toolApproval : undefined, // [816]/[820]②: live tool-approval coordinator (respond route + ALS wrap on the sync streaming leg)
|
|
689
|
+
workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined, // SVC-5: steerable workflow-agent handle registry
|
|
690
|
+
subagentSteerRegistry, // C2 (core 1.219): steerable Task-subagent handle registry (POST /v1/runs/:id/subagents/:target/steer)
|
|
691
|
+
workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined, // P1 ①②: drain → workflow_complete frame on stream-open
|
|
1866
692
|
fleetBus,
|
|
1867
|
-
|
|
693
|
+
// [1196] session 级 SSE 订阅:watch registry(N 订阅共享一条探针,自适应拍距,无订阅即停)。谓词与
|
|
694
|
+
// /head 探针同门(getLeafId+ownerOf);缺 store 面不建=路由 501+capabilities.sessionEvents=false。
|
|
695
|
+
sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined, // [1196] 建于 purgeSession 之前(delete 即 dropSession 的同副本围栏)
|
|
1868
696
|
hookWakeBus,
|
|
1869
|
-
sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined,
|
|
697
|
+
sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined, // SendUserFile 切片2:file_link 帧 ALS(bg/sync 腿 wrap)
|
|
1870
698
|
leaderEndpoint,
|
|
1871
|
-
sessionTitler: sessionTitler ? sessionTitler : undefined,
|
|
699
|
+
sessionTitler: sessionTitler ? sessionTitler : undefined, // fire-and-forget auto-title at first submit
|
|
1872
700
|
};
|
|
701
|
+
/** 函数/构件缝(main 把跨子系统的动作编译成一个可调用面交给路由) */
|
|
1873
702
|
const seams = {
|
|
1874
703
|
sessionAudit,
|
|
1875
704
|
purgeSession: purgeSession ? purgeSession : undefined,
|
|
705
|
+
// [1488]③(b): bg agent-handle read face — GET /v1/runs/:id/subagents/:handle/output → the same registry
|
|
706
|
+
// face the TaskOutput tool reads (defaultTaskRegistry is the process singleton every run leg registers
|
|
707
|
+
// its background children into). Access tuple is derived from the run row in server.ts, never the caller.
|
|
708
|
+
// codex R2: kind-gated BEFORE the poll (backgroundAgentOutput) — generic pollTask has side effects on
|
|
709
|
+
// other kinds (workflow onServedTerminal ack, bash output-cursor advance).
|
|
1876
710
|
subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
711
|
+
// [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs over the same registry singleton
|
|
712
|
+
// (kind gate at the seam: bash/monitor/agent pass, workflow refused — journal face owns it).
|
|
1877
713
|
taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
1878
714
|
taskHandleStop: (handle, access) => taskHandleStop(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
715
|
+
// 142-S5 §1.4: GET /v1/memory/export 数据源(DB memory plane only;file 形态 route 501)。
|
|
1879
716
|
memoryExport: memoryExportBackend ? (scope) => exportMemoryScope(memoryExportBackend, scope) : undefined,
|
|
717
|
+
// 142-S2.5: POST /v1/memory/sync/:scope 执行位(同一 DB entry plane + sync_cursors 游标面;
|
|
718
|
+
// plan 随数据走——core 纯函数在 performMemorySync 内直调,file 形态 route 501)。
|
|
1880
719
|
memorySync: memoryExportBackend && memorySyncCursors
|
|
1881
720
|
? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
|
|
1882
721
|
: undefined,
|
|
722
|
+
// sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
|
|
723
|
+
// 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
|
|
1883
724
|
sessionMirrorRuling: principalCaps
|
|
1884
725
|
? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
|
|
1885
726
|
: undefined,
|
|
1886
727
|
instrumentDegenerate,
|
|
1887
|
-
sideQueryAccounting,
|
|
728
|
+
sideQueryAccounting, // [1469] side-query 四路 sink 记账(与 tracer 同源装配)
|
|
1888
729
|
parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
|
|
730
|
+
// design/158 S2(F-1)修:工厂在 :2308 造好、server.ts 消费分支在等,但从未进本实参段——HIGH-1
|
|
731
|
+
// 同族第二例(条件 spread 的键从不接线,编译期无声)。缺席后果=带 requiresParentConstraint 的
|
|
732
|
+
// parked checkpoint 重启后恒被 core pre-CAS 门拒,**不可赎回**。接线钉:test/parked-revive-gate-wiring.test.ts。
|
|
1889
733
|
parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
|
|
1890
734
|
};
|
|
735
|
+
/** 可观测面(全体 fail-open,观测失败绝不改业务面行为) */
|
|
1891
736
|
const observability = {
|
|
1892
737
|
logger,
|
|
1893
738
|
metrics,
|
|
1894
|
-
modelUsage: modelUsageTracker,
|
|
1895
|
-
promptManifests: promptManifestTracker,
|
|
739
|
+
modelUsage: modelUsageTracker, // E8: shared with createTracer above → runs drain it into the modelUsage echo
|
|
740
|
+
promptManifests: promptManifestTracker, // [998]②: shared with createTracer → the durable legs drain prompt_assembled events
|
|
1896
741
|
planCacheProbe,
|
|
1897
742
|
};
|
|
743
|
+
/** 准入/治理面(每件都是拒绝的权力;缺席 = 该道门不存在,不是「默认放行的门」) */
|
|
1898
744
|
const governance = {
|
|
1899
745
|
authorize,
|
|
1900
746
|
registryJwtVerifier: registryJwtVerifier ? registryJwtVerifier : undefined,
|
|
1901
747
|
rateLimiter,
|
|
1902
748
|
costQuota,
|
|
1903
|
-
fleetLease: fleetLease ? fleetLease : undefined,
|
|
749
|
+
fleetLease: fleetLease ? fleetLease : undefined, // lease admission 门(提交面,镜像 quotaExceeded)
|
|
1904
750
|
};
|
|
751
|
+
/** 部署自述面(/health + /v1/capabilities + 提交前置门读的部署事实;多为 live getter) */
|
|
1905
752
|
const deployment = {
|
|
753
|
+
// 海外 pilot 实机发现:/health 曾 verbatim 回显 sessionBackend 枚举——auto/DB_BACKEND 收编把它
|
|
754
|
+
// coerce 成 LABEL "tidb"(泛指 durable),pg 部署上误读成 SQL-TiDB 带偏排障。回显有效 store(与 listening
|
|
755
|
+
// 日志 session 标签同款诚实修)。
|
|
1906
756
|
sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
|
|
1907
|
-
modelReady: () =>
|
|
1908
|
-
scenarioDetails,
|
|
1909
|
-
workflowsCapable,
|
|
757
|
+
modelReady: () => configCenter.modelReady(), // boot ready 门:roster 未落地=计费提交 503+health ready:false
|
|
758
|
+
scenarioDetails, // 场景详情只读面(内建+center 同判定源)
|
|
759
|
+
workflowsCapable, // ① core split: capabilities.workflows = engine-can (workflowsCapability); workflowsList = store (below)
|
|
1910
760
|
instanceId,
|
|
761
|
+
// Capability discovery: the deployment facts only main knows. Store-derived booleans
|
|
762
|
+
// (asyncRuns/artifacts/approvals/leader) are computed inside the server from the deps themselves.
|
|
763
|
+
// pricingConfigured disambiguates costMicroUsd=0 ("free run" vs "MODEL_COST_* not configured") —
|
|
764
|
+
// it is worker-level config, so one flag here beats decorating every result's stats.
|
|
1911
765
|
capabilities: {
|
|
1912
766
|
version: serviceVersion(),
|
|
1913
767
|
scenarios: Object.keys(scenarios),
|
|
1914
768
|
pricingConfigured: Object.values(pricing).some((p) => Object.values(p).some((v) => typeof v === "number" && v > 0)),
|
|
1915
769
|
},
|
|
1916
|
-
|
|
1917
|
-
|
|
770
|
+
// center: orchestrator polls GET /health for this — set by the sema-registry refresh loop when a
|
|
771
|
+
// pull's restart-to-apply slices differ from boot. A live getter (not a snapshot) so /health always reads current.
|
|
772
|
+
restartState: () => configCenter.restartState(),
|
|
773
|
+
planeDeferredState: () => configCenter.planeDeferredState(), // codex R18: /health.modelPlaneDeferred (stuck-deferred plane, no handoff)
|
|
774
|
+
// shared drain state — SIGTERM flips `draining`, createServer assigns `inflight`, /health mirrors it.
|
|
1918
775
|
drainState,
|
|
1919
|
-
storeDegraded: storeBackendDegraded,
|
|
776
|
+
storeDegraded: storeBackendDegraded, // S5: /health twin of the store_backend_degraded gauge
|
|
1920
777
|
};
|
|
778
|
+
/** 数值旋钮(缺省写在消费点,此处只承载覆写) */
|
|
1921
779
|
const knobs = {
|
|
780
|
+
// (a) SQL 默认店的 per-blob 帽 → PUT /sync/blobs 前置 413(与 SqlBlobBackend 方言默认同源:
|
|
781
|
+
// 显式 env > tidb 的 6MiB entry-限默认;pg 默认无帽=不前置)。
|
|
782
|
+
// 🔴 复审 2026-07-28(E-HIGH-1)修:此键是 **ServiceDeps** 字段,原先被 spread 进 `runnerDeps`
|
|
783
|
+
// (core 的 RunnerDeps 无此字段,条件 spread 绕过 TS 多余属性检查 ⇒ 编译期无声)⇒ HTTP 面恒
|
|
784
|
+
// undefined、413 前置是死枝、超帽仍走 502。落错家的死键病族(verify-cost 先例)——接线钉见
|
|
785
|
+
// test/review-2026-07-28-batch1.test.ts。
|
|
1922
786
|
snapshotBlobSqlCapBytes: backend && backend.kind !== "local" && !config.snapshotBlobStore
|
|
1923
787
|
? (config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
|
|
1924
788
|
: undefined,
|
|
789
|
+
// 帽只在 watch 面在场时才有意义(缺席时路由 501,读不到它)——保持与原条件段同门,不单独接线。
|
|
1925
790
|
sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
|
|
1926
791
|
};
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
|
|
1939
|
-
const objective = typeof body.objective === "string" ? body.objective : "";
|
|
1940
|
-
const parsedSettings = parseTaskSettings(body.settings);
|
|
1941
|
-
if (opts?.leg === "fresh" && appendLessPack) {
|
|
1942
|
-
const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
|
|
1943
|
-
if (riderPresent || parsedSettings.settings?.outputStyle) {
|
|
1944
|
-
throw new HttpError(400, `${riderPresent ? "appendSystemPrompt" : "settings.outputStyle"} is not supported when the effective prompt is already assembled (constitution anchors in systemPrompt, or a center assembled-identity pack for this scenario): the assembler's pass-through path cannot mount the append slot — fold it into the assembled prompt instead`);
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
if (parsedSettings.deferred.length > 0) {
|
|
1948
|
-
logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
|
|
1949
|
-
}
|
|
1950
|
-
let attachmentNotice;
|
|
1951
|
-
{
|
|
1952
|
-
const reqAtt = body.attachmentIds;
|
|
1953
|
-
if (reqAtt !== undefined) {
|
|
1954
|
-
if (!Array.isArray(reqAtt) || !reqAtt.every((x) => typeof x === "string" && x.length > 0 && x.length <= 64)) {
|
|
1955
|
-
throw new HttpError(400, "`attachmentIds` must be an array of attachment ids (strings)");
|
|
1956
|
-
}
|
|
1957
|
-
if (reqAtt.length > 16)
|
|
1958
|
-
throw new HttpError(400, "`attachmentIds` exceeds the per-task limit (16)");
|
|
1959
|
-
if (reqAtt.length > 0) {
|
|
1960
|
-
if (!taskAttachmentStore)
|
|
1961
|
-
throw new HttpError(501, "attachmentIds require a store backend (DB_BACKEND=tidb|pg|local)");
|
|
1962
|
-
if (!auth?.sessionId)
|
|
1963
|
-
throw new HttpError(400, "`attachmentIds` requires a session-resolving deployment (no authorizer session)");
|
|
1964
|
-
const sid = auth.sessionId;
|
|
1965
|
-
const r = await bindAttachmentsForTask({
|
|
1966
|
-
store: taskAttachmentStore,
|
|
1967
|
-
owner: auth?.principal ?? "default",
|
|
1968
|
-
ids: reqAtt,
|
|
1969
|
-
sessionId: sid,
|
|
1970
|
-
leg: opts?.leg === "fresh" ? "fresh" : "resume",
|
|
1971
|
-
nowMs: Date.now(),
|
|
1972
|
-
onMissing: (id) => {
|
|
1973
|
-
if (opts?.leg === "fresh")
|
|
1974
|
-
throw new HttpError(400, `attachment not found: ${id}`);
|
|
1975
|
-
logger.warn("attachment_missing_on_resume", { sessionId: sid, id });
|
|
1976
|
-
},
|
|
1977
|
-
});
|
|
1978
|
-
attachmentNotice = r.notice;
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
const execLane = config.remoteExec?.provider ?? "in-process";
|
|
1983
|
-
if (typeof body.cwd === "string" && body.cwd.length > 0 && auth?.sessionId) {
|
|
1984
|
-
if (cwdHonored(config) && isValidCwd(body.cwd))
|
|
1985
|
-
setSessionCwd(auth.sessionId, body.cwd);
|
|
1986
|
-
else if (inProcessSingleUserLane(config) && isValidCwd(body.cwd) && satisfiedByProcessCwd(body.cwd, realpathSync))
|
|
1987
|
-
logger.debug("task_cwd_inherited", { lane: execLane, sessionId: auth.sessionId });
|
|
1988
|
-
else
|
|
1989
|
-
logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
|
|
1990
|
-
}
|
|
1991
|
-
if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
|
|
1992
|
-
if (cwdHonored(config))
|
|
1993
|
-
setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
|
|
1994
|
-
else if (inProcessSingleUserLane(config)) {
|
|
1995
|
-
const requested = parsedSettings.settings.shellEnv;
|
|
1996
|
-
const mismatched = shellEnvMismatchCount(requested, process.env);
|
|
1997
|
-
if (mismatched === 0)
|
|
1998
|
-
logger.debug("task_shell_env_inherited", { lane: execLane, keys: Object.keys(requested).length, sessionId: auth.sessionId });
|
|
1999
|
-
else
|
|
2000
|
-
logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, mismatchedKeys: mismatched, sessionId: auth.sessionId });
|
|
2001
|
-
}
|
|
2002
|
-
else
|
|
2003
|
-
logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
|
|
2004
|
-
}
|
|
2005
|
-
const taskHooks = (() => {
|
|
2006
|
-
const hc = parsedSettings.settings?.hooks;
|
|
2007
|
-
if (!hc)
|
|
2008
|
-
return undefined;
|
|
2009
|
-
if (config.requirePrincipal === true) {
|
|
2010
|
-
logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
|
|
2011
|
-
return undefined;
|
|
2012
|
-
}
|
|
2013
|
-
const effMode = coercePermissionMode(body.permissionMode) ?? parsedSettings.settings?.permissions?.defaultMode;
|
|
2014
|
-
return createTaskHooks(hc, {
|
|
2015
|
-
logger,
|
|
2016
|
-
sessionId: auth?.sessionId ?? "",
|
|
2017
|
-
cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
|
|
2018
|
-
...(effMode ? { permissionMode: effMode } : {}),
|
|
2019
|
-
...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
|
|
2020
|
-
hookLlm,
|
|
2021
|
-
hookAgent,
|
|
2022
|
-
onHookNotice: (n) => fleetBus.publishHookNotice({
|
|
2023
|
-
...n,
|
|
2024
|
-
...(auth?.principal ? { ownerScope: auth.principal } : {}),
|
|
2025
|
-
...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
|
|
2026
|
-
}),
|
|
2027
|
-
...(auth?.sessionId
|
|
2028
|
-
? {
|
|
2029
|
-
wake: async (text) => hookWakeBus.deliver
|
|
2030
|
-
?
|
|
2031
|
-
hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
|
|
2032
|
-
: false,
|
|
2033
|
-
}
|
|
2034
|
-
: {}),
|
|
2035
|
-
});
|
|
2036
|
-
})();
|
|
2037
|
-
const rawAddDirs = body.additionalDirectories;
|
|
2038
|
-
const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
|
|
2039
|
-
if (rawAddDirs !== undefined && !cwdHonored(config)) {
|
|
2040
|
-
logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
|
|
2041
|
-
}
|
|
2042
|
-
const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
|
|
2043
|
-
const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog);
|
|
2044
|
-
if (picked.unknownExplicit !== undefined) {
|
|
2045
|
-
metrics.inc("task_model_unknown_fallback_total");
|
|
2046
|
-
logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
|
|
2047
|
-
}
|
|
2048
|
-
if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
|
|
2049
|
-
metrics.inc("images_omitted_total", { model: picked.model });
|
|
2050
|
-
logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
|
|
2051
|
-
}
|
|
2052
|
-
let resumeAtEntryId;
|
|
2053
|
-
if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
|
|
2054
|
-
if (!auth?.sessionId)
|
|
2055
|
-
throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
|
|
2056
|
-
if (!resumeAnchorStore || !ownerAware.getLeafId)
|
|
2057
|
-
throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
|
|
2058
|
-
resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
|
|
2059
|
-
if (resumeAtEntryId === undefined)
|
|
2060
|
-
throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
|
|
2061
|
-
}
|
|
2062
|
-
let rewindFilesToEntryId;
|
|
2063
|
-
if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
|
|
2064
|
-
if (!auth?.sessionId)
|
|
2065
|
-
throw new HttpError(422, "rewindFilesTo requires a session (rewind_files_to.no_session)");
|
|
2066
|
-
if (!resumeAnchorStore || !ownerAware.getLeafId)
|
|
2067
|
-
throw new HttpError(501, "rewind-files-to is not available on this worker (no session-store backend for the anchor map)");
|
|
2068
|
-
rewindFilesToEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.rewindFilesTo, auth.principal ?? null);
|
|
2069
|
-
if (rewindFilesToEntryId === undefined)
|
|
2070
|
-
throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
|
|
2071
|
-
}
|
|
2072
|
-
const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
|
|
2073
|
-
const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
|
|
2074
|
-
const spec = {
|
|
2075
|
-
objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
|
|
2076
|
-
systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
|
|
2077
|
-
appendSystemPrompt: acceptedAppend,
|
|
2078
|
-
sessionId: auth?.sessionId,
|
|
2079
|
-
principal: auth?.principal,
|
|
2080
|
-
images: body.images,
|
|
2081
|
-
...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
|
|
2082
|
-
...(() => {
|
|
2083
|
-
const ct = body.compaction?.clampTolerance;
|
|
2084
|
-
return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
|
|
2085
|
-
})(),
|
|
2086
|
-
outputSchema: body.outputSchema,
|
|
2087
|
-
...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
|
|
2088
|
-
? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
|
|
2089
|
-
: {}),
|
|
2090
|
-
resumeAt: resumeAtEntryId,
|
|
2091
|
-
resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
|
|
2092
|
-
requireExistingSession: body.requireExistingSession === true ? true : undefined,
|
|
2093
|
-
enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
|
|
2094
|
-
suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
|
|
2095
|
-
rewindFiles: body.rewindFiles === true ? true : undefined,
|
|
2096
|
-
rewindFilesTo: rewindFilesToEntryId,
|
|
2097
|
-
additionalDirectories,
|
|
2098
|
-
enablePlanMode: config.planModeEnabled ? true : undefined,
|
|
2099
|
-
selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
|
|
2100
|
-
forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
|
|
2101
|
-
retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
|
|
2102
|
-
...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
|
|
2103
|
-
...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
|
|
2104
|
-
? { retainBackgroundProcesses: true }
|
|
2105
|
-
: {}),
|
|
2106
|
-
...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
|
|
2107
|
-
...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
|
|
2108
|
-
...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
|
|
2109
|
-
...(typeof body.interactiveTools === "boolean"
|
|
2110
|
-
? { interactiveTools: body.interactiveTools }
|
|
2111
|
-
: {}),
|
|
2112
|
-
resilience: normalizeResilience(body.resilience, explicitOperator(auth?.principal, config.operatorPrincipals)),
|
|
2113
|
-
finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
|
|
2114
|
-
attachments: normalizeAttachments(body.attachments),
|
|
2115
|
-
...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
|
|
2116
|
-
model: picked.model,
|
|
2117
|
-
...((() => {
|
|
2118
|
-
if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
|
|
2119
|
-
return {};
|
|
2120
|
-
const cm = matchCatalogModel(body.compactionModel, wireCatalog);
|
|
2121
|
-
if (cm === undefined) {
|
|
2122
|
-
if (opts?.leg === "fresh") {
|
|
2123
|
-
throw new HttpError(400, `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id; a catalog refresh may have removed it mid-request)`);
|
|
2124
|
-
}
|
|
2125
|
-
metrics.inc("task_model_unknown_fallback_total");
|
|
2126
|
-
logger.warn("compaction_model_unknown_fallback", { requested: body.compactionModel.slice(0, 120), sessionId: auth?.sessionId ?? null });
|
|
2127
|
-
return {};
|
|
2128
|
-
}
|
|
2129
|
-
return { compactionModel: cm };
|
|
2130
|
-
})()),
|
|
2131
|
-
thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
|
|
2132
|
-
getApiKeyAndHeaders: keyResolver,
|
|
2133
|
-
...(() => {
|
|
2134
|
-
const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
|
|
2135
|
-
const declarations = [];
|
|
2136
|
-
if (taskLimitCaps.timeoutSec !== undefined)
|
|
2137
|
-
declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
|
|
2138
|
-
if (taskLimitCaps.maxOutputTokens !== undefined)
|
|
2139
|
-
declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
|
|
2140
|
-
if (taskLimitCaps.maxTurns !== undefined)
|
|
2141
|
-
declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
|
|
2142
|
-
if (config.maxTaskCostUsd > 0)
|
|
2143
|
-
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
|
-
if (config.maxTaskTokens > 0)
|
|
2145
|
-
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)" });
|
|
2146
|
-
if (config.requirePrincipal === true)
|
|
2147
|
-
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" });
|
|
2148
|
-
return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
|
|
2149
|
-
})(),
|
|
2150
|
-
...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
|
|
2151
|
-
maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
|
|
2152
|
-
maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
|
|
2153
|
-
degrade: (() => {
|
|
2154
|
-
if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
|
|
2155
|
-
return undefined;
|
|
2156
|
-
if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
|
|
2157
|
-
metrics.inc("degrade_dropped_total", { reason: "vision_target" });
|
|
2158
|
-
logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
|
|
2159
|
-
return undefined;
|
|
2160
|
-
}
|
|
2161
|
-
return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
|
|
2162
|
-
})(),
|
|
2163
|
-
memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
|
|
2164
|
-
tools: ((base) => {
|
|
2165
|
-
const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
|
|
2166
|
-
const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
|
|
2167
|
-
return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
|
|
2168
|
-
})(cap.tools),
|
|
2169
|
-
skills: mergeUserSkills(cap.skills, body.skills, logger),
|
|
2170
|
-
mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
|
|
2171
|
-
promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
|
|
2172
|
-
toolPolicy: durableEnabled
|
|
2173
|
-
? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
|
|
2174
|
-
requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
|
|
2175
|
-
...(approvalExemptionStore && auth?.sessionId
|
|
2176
|
-
? {
|
|
2177
|
-
exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
|
|
2178
|
-
onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: auth.sessionId, toolName, rawToolName }),
|
|
2179
|
-
}
|
|
2180
|
-
: {}),
|
|
2181
|
-
}))
|
|
2182
|
-
: approvalEnabled && approvalStore
|
|
2183
|
-
? createOaApprovalPolicy({
|
|
2184
|
-
store: approvalStore,
|
|
2185
|
-
requireApproval: config.approvalRequire,
|
|
2186
|
-
deny: config.approvalDeny,
|
|
2187
|
-
pollMs: config.approvalPollMs,
|
|
2188
|
-
context: () => ({ sessionId: auth?.sessionId ?? null, owner: auth?.principal ?? null }),
|
|
2189
|
-
neverAuto: config.approvalNeverAuto,
|
|
2190
|
-
...(approvalExemptionStore
|
|
2191
|
-
? {
|
|
2192
|
-
exempt: (sessionId, toolName) => approvalExemptionStore.has(sessionId, toolName),
|
|
2193
|
-
onExempted: (sessionId, toolName) => logger.info("approval_exempted", { sessionId, toolName }),
|
|
2194
|
-
}
|
|
2195
|
-
: {}),
|
|
2196
|
-
})
|
|
2197
|
-
: singleUserAutoAcceptBaseline
|
|
2198
|
-
? createAllowDenyPolicy({})
|
|
2199
|
-
: undefined,
|
|
2200
|
-
...(durableEnabled
|
|
2201
|
-
? {
|
|
2202
|
-
checkpointStore,
|
|
2203
|
-
durableApproval: {
|
|
2204
|
-
scope: auth?.principal ?? "_",
|
|
2205
|
-
...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
|
|
2206
|
-
},
|
|
2207
|
-
...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
|
|
2208
|
-
enabled: config.resourceSuspend,
|
|
2209
|
-
ttlSec: config.resourceSuspendTtlSec,
|
|
2210
|
-
isVerify: body.verify === true,
|
|
2211
|
-
isCascade: body.cascade === true,
|
|
2212
|
-
scope: auth?.principal ?? "_",
|
|
2213
|
-
})),
|
|
2214
|
-
onQuestion: QUESTION_AWAITS_RESUME,
|
|
2215
|
-
}
|
|
2216
|
-
: {}),
|
|
2217
|
-
};
|
|
2218
|
-
const governedBase = applyRuntimeGovernance(spec, { autonomy: config.autonomy, commandPolicy: config.commandPolicy });
|
|
2219
|
-
let governed = governedBase;
|
|
2220
|
-
const bodyMode = coercePermissionMode(body.permissionMode);
|
|
2221
|
-
let effectiveSettings = bodyMode ? withPermissionMode(parsedSettings.settings, bodyMode) : parsedSettings.settings;
|
|
2222
|
-
if (appendLessPack && effectiveSettings?.outputStyle) {
|
|
2223
|
-
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
|
-
const { outputStyle: _dropped, ...rest } = effectiveSettings;
|
|
2225
|
-
effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
|
|
2226
|
-
}
|
|
2227
|
-
if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
|
|
2228
|
-
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
|
-
const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
|
|
2230
|
-
effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
|
|
2231
|
-
}
|
|
2232
|
-
const hostSemanticsLane = config.remoteExec === undefined || config.remoteExec.provider === "host";
|
|
2233
|
-
const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
|
|
2234
|
-
requirePrincipal: config.requirePrincipal,
|
|
2235
|
-
hostSemanticsLane,
|
|
2236
|
-
warn: (msg, meta) => logger.warn(msg, { ...(meta ?? {}), sessionId: auth?.sessionId ?? null }),
|
|
2237
|
-
});
|
|
2238
|
-
const scratchpadDir = shellScratchpad ??
|
|
2239
|
-
(hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
|
|
2240
|
-
if (effectiveSettings) {
|
|
2241
|
-
const fsWriteGate = hostSemanticsLane
|
|
2242
|
-
? (() => {
|
|
2243
|
-
const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
|
|
2244
|
-
const gateCwd = sessionCwd ??
|
|
2245
|
-
(config.remoteExec === undefined
|
|
2246
|
-
? process.cwd()
|
|
2247
|
-
: join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
|
|
2248
|
-
return {
|
|
2249
|
-
env: new NodeExecutionEnv({ cwd: gateCwd }),
|
|
2250
|
-
cwd: gateCwd,
|
|
2251
|
-
...(scratchpadDir ? { scratchpadDir } : {}),
|
|
2252
|
-
...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
|
|
2253
|
-
...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
|
|
2254
|
-
...(approvalExemptionStore && auth?.sessionId
|
|
2255
|
-
? {
|
|
2256
|
-
isExempt: async (toolName) => {
|
|
2257
|
-
const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
|
|
2258
|
-
if (hit)
|
|
2259
|
-
logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
|
|
2260
|
-
return hit;
|
|
2261
|
-
},
|
|
2262
|
-
}
|
|
2263
|
-
: {}),
|
|
2264
|
-
};
|
|
2265
|
-
})()
|
|
2266
|
-
: undefined;
|
|
2267
|
-
const workflowGate = approvalExemptionStore && auth?.sessionId
|
|
2268
|
-
? {
|
|
2269
|
-
isExempt: async (toolName) => {
|
|
2270
|
-
const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
|
|
2271
|
-
if (hit)
|
|
2272
|
-
logger.info("workflow_gate_exempted", { toolName, sessionId: auth.sessionId });
|
|
2273
|
-
return hit;
|
|
2274
|
-
},
|
|
2275
|
-
}
|
|
2276
|
-
: undefined;
|
|
2277
|
-
try {
|
|
2278
|
-
governed = applyTaskSettings(governedBase, effectiveSettings, fsWriteGate, workflowGate);
|
|
2279
|
-
}
|
|
2280
|
-
catch (e) {
|
|
2281
|
-
throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
|
|
2285
|
-
governed = { ...governed, tools: stripDelegationTools(governed.tools) };
|
|
2286
|
-
}
|
|
2287
|
-
const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
|
|
2288
|
-
const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
|
|
2289
|
-
let resolvedImageCaps;
|
|
2290
|
-
if (taskImageProfile !== undefined) {
|
|
2291
|
-
if (config.remoteExec?.provider !== "k8s") {
|
|
2292
|
-
throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
|
|
2293
|
-
}
|
|
2294
|
-
if (!imageIndex) {
|
|
2295
|
-
throw new HttpError(400, "sandboxImageProfile requires the image index (sema-registry backend not configured)");
|
|
2296
|
-
}
|
|
2297
|
-
if (!auth?.sessionId) {
|
|
2298
|
-
throw new HttpError(400, "sandboxImageProfile requires a resolved session");
|
|
2299
|
-
}
|
|
2300
|
-
const capsNeeded = Array.isArray(body.capabilitiesNeeded)
|
|
2301
|
-
? body.capabilitiesNeeded.filter((c) => typeof c === "string")
|
|
2302
|
-
: undefined;
|
|
2303
|
-
const resolved = await resolveSandboxImageRef({
|
|
2304
|
-
profile: taskImageProfile,
|
|
2305
|
-
...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
|
|
2306
|
-
viewer: { operator: explicitOperator(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
|
|
2307
|
-
index: imageIndex,
|
|
2308
|
-
});
|
|
2309
|
-
if (!resolved.ok)
|
|
2310
|
-
throw new HttpError(resolved.status, resolved.message);
|
|
2311
|
-
perTaskImage.set(auth.sessionId, resolved.ref);
|
|
2312
|
-
resolvedImageCaps = resolved.capabilities;
|
|
2313
|
-
}
|
|
2314
|
-
if (config.envFactsEnabled) {
|
|
2315
|
-
const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
|
|
2316
|
-
const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
|
|
2317
|
-
? remoteScratchpadDirFor(auth?.sessionId)
|
|
2318
|
-
: undefined;
|
|
2319
|
-
const factScratchpadDir = scratchpadDir ?? remoteScratchpadDir;
|
|
2320
|
-
const facts = buildEnvFacts({
|
|
2321
|
-
profile: taskImageProfile,
|
|
2322
|
-
capabilities: resolvedImageCaps,
|
|
2323
|
-
...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
|
|
2324
|
-
egress: egressForRemoteExec(config.remoteExec),
|
|
2325
|
-
...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
|
|
2326
|
-
resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
|
|
2327
|
-
});
|
|
2328
|
-
if (facts)
|
|
2329
|
-
governed = { ...governed, envFacts: facts };
|
|
2330
|
-
}
|
|
2331
|
-
if (scratchpadDir !== undefined) {
|
|
2332
|
-
const dirs = governed.additionalDirectories ?? [];
|
|
2333
|
-
if (!dirs.includes(scratchpadDir))
|
|
2334
|
-
governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
|
|
2335
|
-
}
|
|
2336
|
-
if (config.routerEnabled) {
|
|
2337
|
-
const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
|
|
2338
|
-
const decision = routeServiceTask({
|
|
2339
|
-
isolatedExecEnv,
|
|
2340
|
-
hasExecutionEnv: config.remoteExec != null,
|
|
2341
|
-
...(config.autonomy ? { autonomy: config.autonomy } : {}),
|
|
2342
|
-
explicitTeam: body.council === true || body.debate === true || scenarioName === "team",
|
|
2343
|
-
});
|
|
2344
|
-
logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
|
|
2345
|
-
if (decision.mode === "supervisor") {
|
|
2346
|
-
return tightenTaskSpec(governed, supPostureOverrides());
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
return governed;
|
|
2350
|
-
};
|
|
792
|
+
// design/158 A10:resolveSpec 段搬到 src/boot/resolve-spec.ts(逐字;两处活引用改取值,见该文件头注)。
|
|
793
|
+
const resolveSpec = createResolveSpec({
|
|
794
|
+
config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
|
|
795
|
+
getCenterPrompts: () => configCenter.getCenterPrompts(),
|
|
796
|
+
getKeyResolver: () => configCenter.getKeyResolver(),
|
|
797
|
+
taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv,
|
|
798
|
+
hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec,
|
|
799
|
+
selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, approvalEnabled,
|
|
800
|
+
approvalStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
|
|
801
|
+
sessionEnvSelection,
|
|
802
|
+
});
|
|
2351
803
|
const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
|
|
804
|
+
// D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
|
|
805
|
+
// before the server, so it calls through this late-bound reference).
|
|
2352
806
|
runDenySweep = server.denyExpiredApprovals;
|
|
807
|
+
// [1934]:绑址旋钮 + 无鉴权自收窄(理由见 resolveBindHost 顶注)。undefined = Node 默认全接口。
|
|
2353
808
|
const bindHost = resolveBindHost(config);
|
|
2354
809
|
await new Promise((resolve) => (bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve)));
|
|
2355
810
|
logger.info("listening", {
|
|
@@ -2359,6 +814,9 @@ async function main() {
|
|
|
2359
814
|
? { note: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
|
|
2360
815
|
: {}),
|
|
2361
816
|
});
|
|
817
|
+
// fleet worker 接线(announce/heartbeat + usage 批报;lease 消费后一拍):boot 完成后
|
|
818
|
+
// 注册(=首次心跳)。门 = center lane 配置 ∧ FLEET_ADVERTISE_ADDRESS 显式声明(不自猜可达地址);任一缺 → undefined
|
|
819
|
+
// = 非 fleet 部署零行为。draining 翻转的即刻 announce 在 drainThenShutdown;退出 DELETE 在 hardShutdown。
|
|
2362
820
|
const fleetClient = startFleetClientFromEnv(config, {
|
|
2363
821
|
instanceId,
|
|
2364
822
|
version: serviceVersion(),
|
|
@@ -2372,108 +830,51 @@ async function main() {
|
|
|
2372
830
|
port: config.port,
|
|
2373
831
|
model: config.model.id,
|
|
2374
832
|
brain: brainSummary(config),
|
|
2375
|
-
|
|
833
|
+
// shared = cross-replica TiDB breaker state; in-process = core's per-replica Map; off = breaker disabled
|
|
834
|
+
breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off", // 1.108: was hard-coded "shared(tidb)" — lied under DB_BACKEND=pg
|
|
835
|
+
// On DB_BACKEND=local the wiring coerces sessionBackend to the LABEL "tidb" (meaning "the
|
|
836
|
+
// backend's durable store, generically") — printing it verbatim read as a SQL deployment on a file-everything
|
|
837
|
+
// box. Print the effective store instead (same honesty fix as breakerState above).
|
|
2376
838
|
session: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
|
|
839
|
+
// design/138 S1: the file-based memory engine (single-user only); "off" = MEMORY_ENGINE=off or multi-tenant.
|
|
2377
840
|
memory: memoryEngine ? `engine(${memoryEngine.root})` : "off",
|
|
2378
841
|
instanceId,
|
|
2379
842
|
rateLimitPerMin: config.rateLimitPerMin,
|
|
2380
843
|
rateLimit: config.rateLimitPerMin > 0 ? (backend ? `shared()` : "in-process") : "off",
|
|
2381
844
|
maxPrincipalCostUsd: config.maxPrincipalCostUsd || null,
|
|
2382
845
|
costQuota: config.maxPrincipalCostUsd > 0 ? (backend ? `shared()` : "in-process") : "off",
|
|
846
|
+
// durable offload store for large tool results (cross-replica wake); else core's in-process default
|
|
2383
847
|
toolResultStore: toolResultStore ? (backend?.kind === "local" ? "file(local)" : "shared(sql)") : "in-process",
|
|
2384
848
|
otel: config.otel ? config.otel.endpoint : null,
|
|
2385
|
-
perModelKeys: Object.keys(config.modelApiKeyEnv).length,
|
|
849
|
+
perModelKeys: Object.keys(config.modelApiKeyEnv).length, // models with their own upstream key (else gateway)
|
|
2386
850
|
sealedModelKeys: Object.values(config.modelApiKeys).filter((v) => typeof v === "string").length,
|
|
2387
|
-
sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length,
|
|
851
|
+
sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length, // sealed-box custody keys unsealed (count only — values never log)
|
|
852
|
+
// SVC-1: durable workflow record + at-least-once notify backend (file = crash-safe ledger; memory = ephemeral)
|
|
2388
853
|
workflowRunStore: config.selfOrchestrationEnabled
|
|
2389
854
|
? config.workflowRunStoreBackend === "memory"
|
|
2390
855
|
? "in-process"
|
|
2391
856
|
: sqlWorkflowRunStore
|
|
2392
|
-
? `db(${backend.kind})`
|
|
857
|
+
? `db(${backend.kind})` // P1: cross-replica twin active
|
|
2393
858
|
: "file"
|
|
2394
859
|
: "off",
|
|
2395
860
|
});
|
|
861
|
+
// SVC-1 at-least-once notify — CRASH-RECOVERY sweep: once at boot, re-derive every un-acked (started-but-not-
|
|
862
|
+
// delivered) workflow's terminal state from the durable run store and re-deliver the completion notify that a
|
|
863
|
+
// prior replica crash dropped (idempotent on the receiver). Fire-and-forget AFTER listen so startup isn't
|
|
864
|
+
// blocked on it (recovery only re-fires completion notifications — independent of serving new requests). A
|
|
865
|
+
// sweep throw is logged, never fatal.
|
|
2396
866
|
if (workflowNotifyGate) {
|
|
2397
867
|
void workflowNotifyGate
|
|
2398
868
|
.recover({ orphanGraceMs: config.workflowOrphanGraceMs })
|
|
2399
869
|
.then((tally) => logger.info("workflow_notify_recovered", tally))
|
|
2400
870
|
.catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }));
|
|
2401
871
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
otelExporter?.stop();
|
|
2409
|
-
breakerState?.stop();
|
|
2410
|
-
if (costQuota && "stop" in costQuota)
|
|
2411
|
-
costQuota.stop();
|
|
2412
|
-
if (rateLimiter && "stop" in rateLimiter)
|
|
2413
|
-
rateLimiter.stop();
|
|
2414
|
-
runner.sessions.dispose();
|
|
2415
|
-
subRunner.sessions.dispose();
|
|
2416
|
-
void lspManager?.dispose?.();
|
|
2417
|
-
void workflowNotifyJournal?.close?.();
|
|
2418
|
-
try {
|
|
2419
|
-
const reaped = defaultTaskRegistry.reapAllSessionBackground();
|
|
2420
|
-
if (reaped > 0)
|
|
2421
|
-
logger.info("shutdown_background_reaped", { reaped });
|
|
2422
|
-
}
|
|
2423
|
-
catch (err) {
|
|
2424
|
-
logger.warn("shutdown_background_reap_failed", { err: String(err) });
|
|
2425
|
-
}
|
|
2426
|
-
void fleetClient?.stop();
|
|
2427
|
-
server.close(() => {
|
|
2428
|
-
void (backend ? backend.close() : Promise.resolve()).finally(() => process.exit(0));
|
|
2429
|
-
});
|
|
2430
|
-
const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
|
|
2431
|
-
finalExit.unref?.();
|
|
2432
|
-
server.closeIdleConnections?.();
|
|
2433
|
-
const forceClose = setTimeout(() => server.closeAllConnections?.(), 10_000);
|
|
2434
|
-
forceClose.unref?.();
|
|
2435
|
-
};
|
|
2436
|
-
let draining = false;
|
|
2437
|
-
const drainThenShutdown = () => {
|
|
2438
|
-
if (closing)
|
|
2439
|
-
return;
|
|
2440
|
-
if (draining) {
|
|
2441
|
-
logger.info("drain_second_signal_hard_stop", {});
|
|
2442
|
-
hardShutdown();
|
|
2443
|
-
return;
|
|
2444
|
-
}
|
|
2445
|
-
draining = true;
|
|
2446
|
-
drainState.draining = true;
|
|
2447
|
-
drainState.since = Date.now();
|
|
2448
|
-
void fleetClient?.announceNow();
|
|
2449
|
-
const inflight = drainState.inflight?.() ?? 0;
|
|
2450
|
-
logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
|
|
2451
|
-
if (inflight === 0) {
|
|
2452
|
-
hardShutdown();
|
|
2453
|
-
return;
|
|
2454
|
-
}
|
|
2455
|
-
const t0 = performance.now();
|
|
2456
|
-
const tick = setInterval(() => {
|
|
2457
|
-
const n = drainState.inflight?.() ?? 0;
|
|
2458
|
-
const elapsed = Math.round(performance.now() - t0);
|
|
2459
|
-
if (n === 0 || elapsed >= config.drainGraceMs) {
|
|
2460
|
-
clearInterval(tick);
|
|
2461
|
-
logger.info("drain_complete", { inflight: n, elapsedMs: elapsed, timedOut: n > 0 });
|
|
2462
|
-
hardShutdown();
|
|
2463
|
-
}
|
|
2464
|
-
}, 1_000);
|
|
2465
|
-
};
|
|
2466
|
-
process.on("SIGTERM", drainThenShutdown);
|
|
2467
|
-
process.on("SIGINT", hardShutdown);
|
|
2468
|
-
process.on("SIGHUP", createSighupIdleHandler({
|
|
2469
|
-
inflight: () => drainState.inflight?.() ?? 0,
|
|
2470
|
-
lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
|
|
2471
|
-
isStopped: () => closing || draining,
|
|
2472
|
-
escalate: drainThenShutdown,
|
|
2473
|
-
shutdown: hardShutdown,
|
|
2474
|
-
idleGraceMs: config.sighupIdleGraceMs,
|
|
2475
|
-
log: (event, fields) => logger.info(event, fields),
|
|
2476
|
-
}));
|
|
872
|
+
// design/158 A10:收尾段搬到 src/boot/shutdown.ts(逐字)。⚠️ 调用点必须留在 listen 之后 —— 位置即契约,
|
|
873
|
+
// 理由(信号注册时点/三信号相对次序/clearInterval 先于 server.close)见该文件头注。
|
|
874
|
+
installShutdownHandlers({
|
|
875
|
+
config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter,
|
|
876
|
+
runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
|
|
877
|
+
});
|
|
2477
878
|
}
|
|
2478
879
|
void main().catch((err) => {
|
|
2479
880
|
createLogger().error("fatal", { err: String(err) });
|