@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/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, RECOMMENDED_SENSITIVE_PATTERNS } from "@sema-agent/core";
|
|
5
5
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
|
|
6
|
-
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
6
|
+
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js"; // design/158 A4: the parser leaf — NOT security.js (base config layer must not value-import the 55KiB auth module)
|
|
7
7
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
8
8
|
function csv(name) {
|
|
9
9
|
return (process.env[name] ?? "")
|
|
@@ -12,6 +12,11 @@ function csv(name) {
|
|
|
12
12
|
.filter(Boolean);
|
|
13
13
|
}
|
|
14
14
|
const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
15
|
+
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
16
|
+
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
17
|
+
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
|
18
|
+
* config overlay (sema-registry.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
19
|
+
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
15
20
|
export function parseAutonomy(raw) {
|
|
16
21
|
const v = raw?.trim();
|
|
17
22
|
if (!v)
|
|
@@ -21,6 +26,9 @@ export function parseAutonomy(raw) {
|
|
|
21
26
|
}
|
|
22
27
|
return v;
|
|
23
28
|
}
|
|
29
|
+
/** Resolve a CSV of env-var NAMES into a {NAME: value} map from THIS process's env (out-of-band secret
|
|
30
|
+
* forwarding into a sandbox). A name with no value set is skipped (not forwarded as undefined). The values
|
|
31
|
+
* are secrets — they live only in the returned map (in-memory), never logged. */
|
|
24
32
|
function resolveEnvForward(csvNames) {
|
|
25
33
|
const out = {};
|
|
26
34
|
for (const name of csvNames.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
@@ -39,14 +47,21 @@ function env(name, fallback) {
|
|
|
39
47
|
}
|
|
40
48
|
return v;
|
|
41
49
|
}
|
|
50
|
+
/** 出厂 skill 目录的缺省 = 包内 `skills/`(相对本模块解析),不是 cwd 相对的裸 "skills"。
|
|
51
|
+
* 真 bug(live 战役 2026-07-15 抓获):npm 全局装的 CLE 从用户 cwd 起引擎,裸 "skills" 解析到
|
|
52
|
+
* cwd/skills(不存在)⇒ skills:0,所有烤入 skill 在 npm 分发面全空转(bench 日志 skills:0 即此)。
|
|
53
|
+
* dist/config.js 与 skills/ 同在包根下的 ../skills;src 跑测试时 ../skills 同样是仓根 skills(一致)。
|
|
54
|
+
* SKILLS_DIR 显式设仍覆盖此缺省;Docker(WORKDIR 包根+COPY skills)解析到 /app/skills 不破。 */
|
|
42
55
|
function defaultSkillsDir() {
|
|
43
56
|
try {
|
|
44
57
|
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
45
58
|
}
|
|
46
59
|
catch {
|
|
47
|
-
return "skills";
|
|
60
|
+
return "skills"; // import.meta 不可用的兜底(不应发生在 ESM 构建里)
|
|
48
61
|
}
|
|
49
62
|
}
|
|
63
|
+
/** Two-name env with the first set winning (MYSQL_* preferred over the TIDB_* back-compat alias); throws
|
|
64
|
+
* when neither is set and no fallback is given — same fail-loud contract as `env`. */
|
|
50
65
|
function env2(primary, alias, fallback) {
|
|
51
66
|
const v = process.env[primary] || process.env[alias];
|
|
52
67
|
if (v === undefined || v === "") {
|
|
@@ -56,6 +71,8 @@ function env2(primary, alias, fallback) {
|
|
|
56
71
|
}
|
|
57
72
|
return v;
|
|
58
73
|
}
|
|
74
|
+
/** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
|
|
75
|
+
* (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
|
|
59
76
|
function numEnv(name, fallback) {
|
|
60
77
|
const raw = env(name, fallback);
|
|
61
78
|
const n = Number(raw);
|
|
@@ -63,25 +80,51 @@ function numEnv(name, fallback) {
|
|
|
63
80
|
throw new Error(`env ${name}="${raw}" must be a number`);
|
|
64
81
|
return n;
|
|
65
82
|
}
|
|
83
|
+
/** Numeric env with a [min,max] bound (BL-17): an out-of-range value FAILS at startup instead of being
|
|
84
|
+
* silently clamped/applied (e.g. a multi-hour MCP_ELICITATION_TTL_MS would silently never expire). */
|
|
66
85
|
function numEnvBounded(name, fallback, min, max) {
|
|
67
86
|
const n = numEnv(name, fallback);
|
|
68
87
|
if (n < min || n > max)
|
|
69
88
|
throw new Error(`env ${name}=${n} must be in [${min}, ${max}]`);
|
|
70
89
|
return n;
|
|
71
90
|
}
|
|
91
|
+
/** Optional FAIL-SAFE numeric env for an OPTIONAL feature knob: a non-numeric / non-positive value yields `undefined`
|
|
92
|
+
* (the caller falls back to its own default) rather than NaN OR a hard startup throw. Used for SESSION_SNAPSHOT_TTL_SEC
|
|
93
|
+
* (finding 20): a typo like "600s" must NOT sign every presigned URL with X-Amz-Expires=NaN (total snapshot outage),
|
|
94
|
+
* but ALSO must not crash boot of an unrelated subsystem — degrade to the MinioBlobBackend default. */
|
|
95
|
+
/** S20 (SILENT-FALLBACK P1): soft-knob parse rejects, collected here because config parsing runs BEFORE the
|
|
96
|
+
* logger exists. main.ts drains this once after createLogger and warns per entry ("configured X=raw is
|
|
97
|
+
* invalid, using the default"). Module-level on purpose; drain clears it (repeat loadConfig in tests is fine). */
|
|
72
98
|
const CONFIG_WARNINGS = [];
|
|
73
99
|
export function drainConfigWarnings() {
|
|
74
100
|
return CONFIG_WARNINGS.splice(0, CONFIG_WARNINGS.length);
|
|
75
101
|
}
|
|
102
|
+
/** Boot NOTICES ([792]④): non-error decisions loadConfig makes that the operator should see once (e.g. the
|
|
103
|
+
* MODEL_PROVIDER inference below). Same pre-logger collection pattern as CONFIG_WARNINGS, but each entry
|
|
104
|
+
* carries its own event name + fields (the warnings drain has one fixed "invalid, using default" message
|
|
105
|
+
* which would misdescribe an inference). main.ts drains + logs right after createLogger. */
|
|
76
106
|
const CONFIG_NOTICES = [];
|
|
77
107
|
export function drainConfigNotices() {
|
|
78
108
|
return CONFIG_NOTICES.splice(0, CONFIG_NOTICES.length);
|
|
79
109
|
}
|
|
110
|
+
/** F10 (codex audit): drain + log BOTH pre-logger diagnostic collectors (S20 soft-knob warnings + [792]④ boot
|
|
111
|
+
* notices) in one place. EVERY entrypoint that calls loadConfig() then builds a logger must call this right
|
|
112
|
+
* after (main.ts, run-local.ts) — a new entry that forgets it would silently drop operator-facing diagnostics
|
|
113
|
+
* (run-local historically did exactly that), so the drain lives beside the collectors, not in one entrypoint. */
|
|
80
114
|
export function logConfigDiagnostics(logger) {
|
|
81
115
|
for (const w of drainConfigWarnings())
|
|
82
116
|
logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
|
|
83
117
|
for (const n of drainConfigNotices())
|
|
84
118
|
logger.warn(n.event, n.fields);
|
|
119
|
+
// design/158 N9 (polarity self-report): a `*_ENABLED` suffix does NOT carry its default — the repo grew four
|
|
120
|
+
// dialects for it (opt-in / opt-out / posture tri-state / negative name). Rather than force operators to read
|
|
121
|
+
// config.ts to learn what a knob does when unset, boolEnv-parsed knobs register themselves and we print one
|
|
122
|
+
// debug line per knob: name → polarity → effective value → where that value came from. `debug` is optional so
|
|
123
|
+
// an older embedder-supplied logger shape (warn-only) still compiles and still gets the warnings above.
|
|
124
|
+
// 复审 2026-07-29 #6 — the table covers a LOAD, not the catalog: five boolEnv calls sit inside conditional
|
|
125
|
+
// config shapes (E2B_ALLOW_NET only when set; K8S_INSECURE_TLS / DOCKER_DROP_CAPS only under their provider
|
|
126
|
+
// branch; MODEL_DEGRADE_REACTIVE / MODEL_DEGRADE_TO_VISION only when MODEL_DEGRADE_TO is set) and register
|
|
127
|
+
// only when that shape is active — absence of a row means "not a live knob in this config", not "off".
|
|
85
128
|
if (logger.debug) {
|
|
86
129
|
for (const k of configKnobTable()) {
|
|
87
130
|
logger.debug("config_knob_polarity", {
|
|
@@ -94,6 +137,11 @@ export function logConfigDiagnostics(logger) {
|
|
|
94
137
|
}
|
|
95
138
|
}
|
|
96
139
|
}
|
|
140
|
+
/** F8 (codex audit): first NON-EMPTY env among aliased names — "" = unset, the same convention `env()` uses.
|
|
141
|
+
* A plain `??` chain treats an EMPTY first choice as SET and shadows a configured fallback (e.g.
|
|
142
|
+
* `ANTHROPIC_BASEURL=""` from a compose `${VAR:-}` template hiding a real `ANTHROPIC_BASE_URL`, or an empty
|
|
143
|
+
* `ANTHROPIC_API_KEY` suppressing the whole route despite a valid `ANTHROPIC_AUTH_TOKEN`). Shared by the
|
|
144
|
+
* MODEL_PROVIDER inference AND the anthropic route assembly so the two predicates can never drift. */
|
|
97
145
|
function firstSetEnv(...names) {
|
|
98
146
|
for (const n of names) {
|
|
99
147
|
const v = process.env[n];
|
|
@@ -108,13 +156,18 @@ function optFinitePositiveEnv(name) {
|
|
|
108
156
|
return undefined;
|
|
109
157
|
const n = Number(raw);
|
|
110
158
|
if (!(Number.isFinite(n) && n > 0)) {
|
|
111
|
-
CONFIG_WARNINGS.push({ env: name, raw });
|
|
159
|
+
CONFIG_WARNINGS.push({ env: name, raw }); // S20: previously a silent swallow — caller quietly used the default
|
|
112
160
|
return undefined;
|
|
113
161
|
}
|
|
114
162
|
return n;
|
|
115
163
|
}
|
|
164
|
+
/** Keyed by canonical name so a knob read twice in one load (e.g. DURABLE_APPROVAL, once for the D-G boot
|
|
165
|
+
* invariant and once for the returned field) registers one row, not two. Reset per `loadConfig()`. */
|
|
116
166
|
const CONFIG_KNOBS = new Map();
|
|
167
|
+
/** Deprecated aliases already announced for this load — the lazily-read twins (HOST_BG/HOST_EXEC_SPOOL) resolve
|
|
168
|
+
* again on every use, and an operator does not need the same deprecation line once per command. */
|
|
117
169
|
const KNOB_DEPRECATIONS_SEEN = new Set();
|
|
170
|
+
/** The registered boolean knobs, sorted by name — the data behind `logConfigDiagnostics`'s polarity table. */
|
|
118
171
|
export function configKnobTable() {
|
|
119
172
|
return [...CONFIG_KNOBS.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
120
173
|
}
|
|
@@ -126,6 +179,7 @@ function boolEnv(name, def) {
|
|
|
126
179
|
let value = def;
|
|
127
180
|
let source = "default";
|
|
128
181
|
if (raw !== undefined && raw !== "") {
|
|
182
|
+
// "" = unset, the same convention `env()`/`firstSetEnv()` use (a compose `${VAR:-}` template passes empty through).
|
|
129
183
|
if (raw === "true") {
|
|
130
184
|
value = true;
|
|
131
185
|
source = "env";
|
|
@@ -135,12 +189,31 @@ function boolEnv(name, def) {
|
|
|
135
189
|
source = "env";
|
|
136
190
|
}
|
|
137
191
|
else if (!CONFIG_KNOBS.has(name)) {
|
|
192
|
+
// Previously a silent default at every hand-written site. Guarded on first registration: the LAZILY
|
|
193
|
+
// re-read knobs (boolEnvWithLegacyNegated's host twins, read per exec) must not grow this array for the
|
|
194
|
+
// process lifetime on one typo — one typo, one warning. (复审 2026-07-30 F6:早期理由「IMAGE_BAKES_
|
|
195
|
+
// ENABLED/DURABLE_APPROVAL 一次 load 读两次」已随九域拆分的去重失效,懒读族成为本守卫的现行理由。)
|
|
138
196
|
CONFIG_WARNINGS.push({ env: name, raw });
|
|
139
197
|
}
|
|
140
198
|
}
|
|
141
199
|
registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
|
|
142
200
|
return value;
|
|
143
201
|
}
|
|
202
|
+
/** design/158 N8 (D-family) — a knob whose CANONICAL name is positive (`X_ENABLED`, default ON) but which keeps
|
|
203
|
+
* its retired negative name (`X_DISABLED`) working for one deprecation window.
|
|
204
|
+
*
|
|
205
|
+
* Why the flip at all: `X_DISABLED !== "true"` is a double negative at the read site, so the four knobs that
|
|
206
|
+
* used it (`PROJECT_MEMORY`, `CONFIG_LKG`, `HOST_BG`, `HOST_EXEC_SPOOL`) could not be read for their default
|
|
207
|
+
* without inverting twice in your head, and `projectMemoryDisabled` was the ONE negative boolean on a
|
|
208
|
+
* `ServiceConfig` that otherwise carries 16 positive `*Enabled`/`*Allowed` fields.
|
|
209
|
+
*
|
|
210
|
+
* Resolution order (new name wins, so there is never an ambiguous both-set state):
|
|
211
|
+
* 1. `X_ENABLED` set to a literal ⇒ that.
|
|
212
|
+
* 2. `X_ENABLED` set to a non-literal ⇒ default + warning (the legacy name is NOT consulted — a typo in the
|
|
213
|
+
* new name must not silently hand control back to the deprecated one).
|
|
214
|
+
* 3. `X_ENABLED` unset ∧ `X_DISABLED` set ⇒ the legacy site's exact contract, `X_DISABLED !== "true"`, plus a
|
|
215
|
+
* one-shot deprecation notice naming the replacement.
|
|
216
|
+
* 4. neither ⇒ default. */
|
|
144
217
|
export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
145
218
|
const raw = process.env[name];
|
|
146
219
|
const legacy = process.env[legacyNegatedName];
|
|
@@ -152,13 +225,16 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
|
152
225
|
}
|
|
153
226
|
else if (raw !== undefined && raw !== "") {
|
|
154
227
|
if (!CONFIG_KNOBS.has(name)) {
|
|
228
|
+
// Same first-registration guard as boolEnv — the three consumers of this helper are the LAZILY re-read
|
|
229
|
+
// ones (remote-env-host reads per exec/probe), so an unguarded push would grow CONFIG_WARNINGS for the
|
|
230
|
+
// whole process lifetime on a single operator typo (复审 2026-07-29 #1).
|
|
155
231
|
CONFIG_WARNINGS.push({ env: name, raw });
|
|
156
232
|
}
|
|
157
233
|
value = def;
|
|
158
234
|
source = "default";
|
|
159
235
|
}
|
|
160
236
|
else if (legacy !== undefined && legacy !== "") {
|
|
161
|
-
value = legacy !== "true";
|
|
237
|
+
value = legacy !== "true"; // verbatim legacy contract: only the literal "true" disabled the feature
|
|
162
238
|
source = "legacy-env";
|
|
163
239
|
if (!KNOB_DEPRECATIONS_SEEN.has(legacyNegatedName)) {
|
|
164
240
|
KNOB_DEPRECATIONS_SEEN.add(legacyNegatedName);
|
|
@@ -175,33 +251,55 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
|
175
251
|
registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source, legacyName: legacyNegatedName });
|
|
176
252
|
return value;
|
|
177
253
|
}
|
|
254
|
+
/** LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
|
|
255
|
+
* rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
|
|
256
|
+
* so its polarity row and any deprecation notice appear at boot with everything else. */
|
|
178
257
|
export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
|
|
258
|
+
/** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
|
|
179
259
|
export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
|
|
260
|
+
/** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
|
|
180
261
|
export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
|
|
262
|
+
/** Enum env with validation: a typo (e.g. SESSION_BACKEND=Tidb) FAILS at startup instead of being silently
|
|
263
|
+
* cast to the union type and degrading to the default backend (dropping the persistence the operator asked for). */
|
|
181
264
|
function enumEnv(name, fallback, allowed) {
|
|
182
265
|
const v = (process.env[name] ?? fallback);
|
|
183
266
|
if (!allowed.includes(v))
|
|
184
267
|
throw new Error(`env ${name}="${v}" must be one of: ${allowed.join(", ")}`);
|
|
185
268
|
return v;
|
|
186
269
|
}
|
|
270
|
+
/** A single HTTP header-name env: it is both read as a request header AND interpolated into the CORS
|
|
271
|
+
* `access-control-allow-headers` list, so a value with a comma/space would split that list (and isn't a
|
|
272
|
+
* valid header name anyway). Reject anything but header-token chars at startup (council). */
|
|
187
273
|
function headerNameEnv(name, fallback) {
|
|
188
274
|
const v = env(name, fallback);
|
|
189
275
|
if (!/^[A-Za-z0-9_-]+$/.test(v))
|
|
190
276
|
throw new Error(`env ${name}="${v}" must be a single header name ([A-Za-z0-9_-]+)`);
|
|
191
277
|
return v;
|
|
192
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Accept an SSH private key in whatever single-line form a secret store can hold. A PEM key is multi-line, but
|
|
281
|
+
* sema-registry's `POST /api/config/secrets` rejects newlines — so let the value arrive base64-encoded (the
|
|
282
|
+
* recommended self-serve form) or with escaped `\n`, and reconstruct the real PEM here for ssh2.
|
|
283
|
+
*/
|
|
193
284
|
function normalizeSshPrivateKey(raw) {
|
|
194
285
|
const v = raw.trim();
|
|
195
286
|
if (v.includes("BEGIN ") && v.includes("PRIVATE KEY")) {
|
|
196
|
-
return v.includes("\n") ? raw : v.replace(/\\n/g, "\n");
|
|
287
|
+
return v.includes("\n") ? raw : v.replace(/\\n/g, "\n"); // already PEM (real newlines) or escaped-\n single line
|
|
197
288
|
}
|
|
198
289
|
try {
|
|
199
|
-
return Buffer.from(v, "base64").toString("utf8");
|
|
290
|
+
return Buffer.from(v, "base64").toString("utf8"); // base64-of-PEM (newline-free → secrets-API friendly)
|
|
200
291
|
}
|
|
201
292
|
catch {
|
|
202
293
|
return raw;
|
|
203
294
|
}
|
|
204
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Build `Model.extraBody` (core 1.60 generation-tuning passthrough) from env, or undefined when nothing
|
|
298
|
+
* is set (so a deployment that sets none is byte-for-byte unaffected). MODEL_EXTRA_BODY (raw JSON) is the
|
|
299
|
+
* general escape hatch; MODEL_FREQUENCY_PENALTY / MODEL_PRESENCE_PENALTY are typed conveniences merged on
|
|
300
|
+
* top (they win over the same key in MODEL_EXTRA_BODY). Throws on malformed JSON — fail loud at boot, not
|
|
301
|
+
* a silently-ignored tuning knob.
|
|
302
|
+
*/
|
|
205
303
|
function buildExtraBody() {
|
|
206
304
|
const body = {};
|
|
207
305
|
const raw = process.env.MODEL_EXTRA_BODY;
|
|
@@ -226,6 +324,10 @@ function buildExtraBody() {
|
|
|
226
324
|
body.presence_penalty = Number(pp);
|
|
227
325
|
return Object.keys(body).length > 0 ? body : undefined;
|
|
228
326
|
}
|
|
327
|
+
/** [818]① 1M dual-window derivation (core 1.289 `Model.autoCompactTokens`): an EXPLICIT positive value wins
|
|
328
|
+
* (a center roster may declare its own); else contextWindow>=1e6 derives the CC-aligned 967000; else the field
|
|
329
|
+
* is REMOVED (a spread-inherited value from a bigger-window base model would mis-trigger a small window).
|
|
330
|
+
* Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
|
|
229
331
|
export function applyAutoCompactWindow(m, explicit) {
|
|
230
332
|
const target = m;
|
|
231
333
|
if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) {
|
|
@@ -237,23 +339,27 @@ export function applyAutoCompactWindow(m, explicit) {
|
|
|
237
339
|
else
|
|
238
340
|
delete target.autoCompactTokens;
|
|
239
341
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
342
|
+
/** 域:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
|
|
343
|
+
function parseStoreDomain(ctx) {
|
|
344
|
+
const { requirePrincipal } = ctx; // 跨域入参①:多租户裸 boot 的 memory 缺省由它决定(见下方注释)
|
|
345
|
+
// `local` (clay 2026-06-25 seamless local↔cloud) = the DB-less in-memory/file StoreBackend, so a locally-run HTTP
|
|
346
|
+
// service serves the same contract a cloud worker does. It has no SQL host/coords (createStoreBackend builds it
|
|
347
|
+
// without tidb/pg), so it's always "reachable". (Parsed FIRST so the session/memory posture defaults below can
|
|
348
|
+
// key off the chosen engine.) Wording (clay 2026-07-06): canonical value `mysql` = any MySQL-protocol server
|
|
349
|
+
// (MySQL / TiDB / MariaDB); `tidb` accepted as a back-compat alias. TiDB stays fully supported and is the
|
|
350
|
+
// recommended MySQL-protocol engine when semantic memory is on (native VECTOR(dim) recall).
|
|
351
|
+
// clay 拍(2026-07-27,[1845] 桌面撞 501 案后):**裸 boot 默认 durable(local file 店)**。
|
|
352
|
+
// 旧默认「纯内存」让每个宿主(cli fixture/桌面/web BFF)都要手工记得 DB_BACKEND=local,漏设 =
|
|
353
|
+
// 「跑完的任务重启就丢 + durable runs 面 501」——单机是主流形,直觉预期是留得住。三态:
|
|
354
|
+
// - 未设 ⇒ local(单用户形;多租户例外见下)。默认推导的 local 在 main.ts 侧 mkdir/ensure 失败
|
|
355
|
+
// 时**降级 memory + warn + gauge**(auto-DB 不可达先例),显式 DB_BACKEND=local 才 fail-loud。
|
|
356
|
+
// - DB_BACKEND=memory ⇒ 显式纯内存 opt-out(CI/临时 server「不留盘上残余」的出口)。
|
|
357
|
+
// - 多租户裸 boot(REQUIRE_PRINCIPAL=true 且未设)⇒ 保持 memory + notice:local 与多租户互斥
|
|
358
|
+
// (main.ts 硬拒,owner map 无租户隔离),默认不得把多租户裸 boot 变成拒启。
|
|
256
359
|
const dbBackendSet = !!process.env.DB_BACKEND;
|
|
360
|
+
// 「裸 boot」判定必须**无任何 SQL 信号**才成立——SESSION_BACKEND=mysql/auto 或 TIDB/MYSQL/PG coords
|
|
361
|
+
// 在场时,dbBackend 的旧默认 "mysql" 是**引擎选择器**(auto 探测/coords 构建都 key 它),默认切
|
|
362
|
+
// local 会把这些既有形错切到文件店(回归钉在 config-coverage)。
|
|
257
363
|
const sqlSignal = (!!process.env.SESSION_BACKEND && process.env.SESSION_BACKEND !== "memory") ||
|
|
258
364
|
!!(process.env.TIDB_HOST || process.env.MYSQL_HOST || process.env.PG_HOST);
|
|
259
365
|
const bareBoot = !dbBackendSet && !sqlSignal;
|
|
@@ -268,49 +374,121 @@ export function loadConfig() {
|
|
|
268
374
|
},
|
|
269
375
|
});
|
|
270
376
|
}
|
|
377
|
+
// Asymmetry收编 (clay 2026-07-06, overseas-pilot pit #1): an EXPLICIT `DB_BACKEND=mysql/tidb/pg` is the operator
|
|
378
|
+
// asking for a durable SQL deployment — default sessions onto it (fail-fast, same as an explicit
|
|
379
|
+
// SESSION_BACKEND=mysql) instead of silently keeping the in-memory default ("DB_BACKEND=pg engaged nothing,
|
|
380
|
+
// session:memory, PG 0 tables"). DB_BACKEND=local keeps its own coercion in main.ts (it needs the built
|
|
381
|
+
// backend); an UNSET DB_BACKEND keeps the memory default (no surprise DB requirement on a bare boot).
|
|
382
|
+
// `mysql` is accepted as an alias of the internal durable label `tidb` (see ServiceConfig.sessionBackend).
|
|
271
383
|
const sqlEngineExplicit = !!process.env.DB_BACKEND && dbBackend !== "local" && dbBackend !== "memory";
|
|
272
384
|
const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", ["memory", "mysql", "tidb", "auto"]);
|
|
273
385
|
const sessionBackend = sessionBackendRaw === "mysql" ? "tidb" : sessionBackendRaw;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const memoryEngineBackend = memoryEngineBackendRaw;
|
|
280
|
-
const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
|
|
281
|
-
if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
|
|
282
|
-
throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
|
|
283
|
-
}
|
|
284
|
-
const workflowSizeGuideline = workflowSizeGuidelineRaw;
|
|
285
|
-
const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
|
|
286
|
-
const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
|
|
287
|
-
const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
|
|
288
|
-
const memorySyncScopeRaw = process.env.MEMORY_SYNC_SCOPE || undefined;
|
|
289
|
-
let memorySync;
|
|
290
|
-
if (memorySyncUrl === undefined) {
|
|
291
|
-
if (memorySyncToken !== undefined || memorySyncScopeRaw !== undefined) {
|
|
292
|
-
throw new Error("MEMORY_SYNC_TOKEN/MEMORY_SYNC_SCOPE are set but MEMORY_SYNC_URL is not — refusing to start half-configured (set MEMORY_SYNC_URL to enable memory sync, or unset the other MEMORY_SYNC_* keys)");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
if (memoryEngineBackend !== "file") {
|
|
297
|
-
throw new Error(`MEMORY_SYNC_URL is a FILE-memory (TOC client) knob, but MEMORY_ENGINE_BACKEND=${memoryEngineBackend} — the DB memory plane IS the central sync authority (it serves POST /v1/memory/sync/:scope) and must not also run the client leg; unset MEMORY_SYNC_* here`);
|
|
298
|
-
}
|
|
299
|
-
if (memorySyncToken === undefined) {
|
|
300
|
-
throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
|
|
301
|
-
}
|
|
302
|
-
const isV2ScopeKey = (k) => k.startsWith("user:") || k.startsWith("org:") || k.startsWith("proj:") || k.startsWith("userproj:");
|
|
303
|
-
const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
|
|
304
|
-
if (scope === undefined) {
|
|
305
|
-
throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
|
|
306
|
-
}
|
|
307
|
-
memorySync = { url: memorySyncUrl, token: memorySyncToken, scope, ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPushEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES) : undefined), ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPullEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES) : undefined) };
|
|
308
|
-
}
|
|
386
|
+
// P0.5 variant-2: the file-backed `local` backend's data root. SAME resolution main.ts:102 (localRoot =
|
|
387
|
+
// CONFIG_LOCAL_DIR ?? AGENT_DATA_DIR ?? ~/.ai-agent) + run-local.ts:198, so the HTTP service + a run-local on one
|
|
388
|
+
// box open ONE boot-locked data dir. `LOCAL_DATA_ROOT` is an additional highest-priority override (the explicit
|
|
389
|
+
// "the local backend's data lives HERE" knob). Only the `local` backend consumes it (createStoreBackend), but it's
|
|
390
|
+
// always resolved (cheap, env-deterministic).
|
|
309
391
|
const localDataRoot = process.env.LOCAL_DATA_ROOT || process.env.CONFIG_LOCAL_DIR || process.env.AGENT_DATA_DIR || join(homedir(), ".ai-agent");
|
|
392
|
+
// `auto` builds the DB coords only when a host is actually configured — otherwise it stays memory with no
|
|
393
|
+
// probe. (Explicit mysql always builds them so a missing host fails loudly.) `needsDb` is engine-agnostic;
|
|
394
|
+
// the connection coords are built for whichever engine `dbBackend` selects (MYSQL_* preferred, TIDB_* alias).
|
|
310
395
|
const dbHostSet = dbBackend === "pg" ? !!process.env.PG_HOST : !!(process.env.MYSQL_HOST || process.env.TIDB_HOST);
|
|
311
396
|
const needsTidb = sessionBackend === "tidb" || (sessionBackend === "auto" && dbHostSet);
|
|
312
397
|
const needsDb = needsTidb;
|
|
313
|
-
|
|
398
|
+
return {
|
|
399
|
+
sessionBackend,
|
|
400
|
+
sessionCacheTtlSec: Number(env("SESSION_CACHE_TTL_SEC", "300")),
|
|
401
|
+
rewindSnapshotMaxMb: optFinitePositiveEnv("REWIND_SNAPSHOT_MAX_MB"), // soft knob (S20: bad value warns + default)
|
|
402
|
+
dbBackend,
|
|
403
|
+
dbBackendExplicit: dbBackendSet,
|
|
404
|
+
localDataRoot,
|
|
405
|
+
tidb: needsDb && dbBackend === "mysql"
|
|
406
|
+
? {
|
|
407
|
+
host: env2("MYSQL_HOST", "TIDB_HOST"),
|
|
408
|
+
// default port follows the env family in use: MYSQL_HOST → 3306 (stock MySQL), TIDB_HOST → 4000 (TiDB)
|
|
409
|
+
port: Number(env2("MYSQL_PORT", "TIDB_PORT", process.env.MYSQL_HOST ? "3306" : "4000")),
|
|
410
|
+
user: env2("MYSQL_USER", "TIDB_USER"),
|
|
411
|
+
password: env2("MYSQL_PASSWORD", "TIDB_PASSWORD", ""),
|
|
412
|
+
database: env2("MYSQL_DATABASE", "TIDB_DATABASE"),
|
|
413
|
+
connectionLimit: process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE
|
|
414
|
+
? Number(process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE)
|
|
415
|
+
: undefined,
|
|
416
|
+
}
|
|
417
|
+
: undefined,
|
|
418
|
+
pg: needsDb && dbBackend === "pg"
|
|
419
|
+
? {
|
|
420
|
+
host: env("PG_HOST"),
|
|
421
|
+
port: Number(env("PG_PORT", "5432")),
|
|
422
|
+
user: env("PG_USER"),
|
|
423
|
+
password: env("PG_PASSWORD", ""),
|
|
424
|
+
database: env("PG_DATABASE"),
|
|
425
|
+
connectionLimit: process.env.PG_POOL_SIZE ? Number(process.env.PG_POOL_SIZE) : undefined,
|
|
426
|
+
}
|
|
427
|
+
: undefined,
|
|
428
|
+
// S9 deeper fix: per-query DB timeout (see the interface doc). Soft knob — a typo degrades to the default
|
|
429
|
+
// posture (pool-wide off / counter-family 30s) with an S20 boot warning, never NaN into a driver timer.
|
|
430
|
+
dbQueryTimeoutMs: optFinitePositiveEnv("DB_QUERY_TIMEOUT_MS"),
|
|
431
|
+
// E19/2c snapshot-BLOB object-store offload (clay 2026-06-26). Reuses the SAME adapter-held MinIO creds as the k8s
|
|
432
|
+
// workspace-snapshot lane (secret minio-agent-worker); all three keys required to enable, else the file-snapshot
|
|
433
|
+
// stores keep the SQL `snapshot_blob` default (byte-identical to today). Bucket = SESSION_SNAPSHOT_BUCKET (default
|
|
434
|
+
// "session-snapshots"); keyPrefix optional (default "blobs/" in MinioBlobBackend).
|
|
435
|
+
snapshotBlobStore: process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
436
|
+
? {
|
|
437
|
+
endpoint: process.env.MINIO_ENDPOINT,
|
|
438
|
+
bucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
|
|
439
|
+
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
440
|
+
secretKey: process.env.MINIO_SECRET_KEY,
|
|
441
|
+
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
442
|
+
...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: process.env.SESSION_SNAPSHOT_PREFIX } : {}),
|
|
443
|
+
// Fail-SAFE parse (finding 20): a non-numeric/non-positive value → omit (the MinioBlobBackend default 3600s
|
|
444
|
+
// applies), never NaN (which would sign every URL with X-Amz-Expires=NaN → total snapshot outage).
|
|
445
|
+
...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
|
|
446
|
+
}
|
|
447
|
+
: undefined,
|
|
448
|
+
// (a+c)(clay 2026-07-27)snapshot_blob 包墙修复两旋钮——语义见 ServiceConfig 字段注释。
|
|
449
|
+
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
450
|
+
snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
|
|
451
|
+
// SendUserFile 双轨公网直链(同三键开闸;S3_ENDPOINT 兼容位=外接 S3 场景内网/公网同址)。
|
|
452
|
+
// `||` 非 `??`(配置面自查 2026-07-14):env 惯例(env() helper 同款)空串=未设——`?? ` 会让
|
|
453
|
+
// 一个显式置空的 MINIO_ENDPOINT/S3_PUBLIC_ENDPOINT 把后备名吞掉(compose `${X:-}` 透传空串是常态)。
|
|
454
|
+
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
455
|
+
? {
|
|
456
|
+
endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
|
|
457
|
+
...(process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT
|
|
458
|
+
? { publicEndpoint: (process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT) }
|
|
459
|
+
: {}),
|
|
460
|
+
publicBucket: process.env.S3_PUBLIC_BUCKET ?? "sema-public",
|
|
461
|
+
privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
|
|
462
|
+
privateKeyPrefix: "sendfile/",
|
|
463
|
+
...(process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT ? { sandboxPutEndpoint: process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT } : {}),
|
|
464
|
+
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
465
|
+
secretKey: process.env.MINIO_SECRET_KEY,
|
|
466
|
+
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
467
|
+
// fail-SAFE:非法值回落 0(永久轨缺省),不 NaN 进签名。
|
|
468
|
+
defaultTtlSec: ((v) => (v !== undefined && Number.isInteger(v) && v >= 0 ? v : 0))(process.env.SEND_USER_FILE_URL_TTL !== undefined ? Number(process.env.SEND_USER_FILE_URL_TTL) : undefined),
|
|
469
|
+
}
|
|
470
|
+
: undefined,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/** 域:model(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
|
|
474
|
+
function parseModelDomain() {
|
|
475
|
+
// npm 卫生纪律:默认值不烤内网坐标 — localhost 占位(公开分发正确缺省);真网关一律显式 env。
|
|
476
|
+
const gatewayBaseUrl = env("MODEL_GATEWAY_BASEURL", "http://127.0.0.1:8000/v1");
|
|
477
|
+
const modelId = env("MODEL_ID", "Qwen3.5-35B");
|
|
478
|
+
// Model.api must reflect the brain that actually serves it (routing is by provider): an "anthropic"
|
|
479
|
+
// provider runs the Anthropic brain, whose usage reports `input` EXCLUDING cached tokens. core ≥1.22
|
|
480
|
+
// normalizes the cache-hit-rate denominator per api family — a wrong api makes promptTokens too small
|
|
481
|
+
// and cacheHitRate exceed 100%. (Brains select by provider, not api, so this only fixes accounting.)
|
|
482
|
+
//
|
|
483
|
+
// Default inference ([792]④, core ruling 2026-07-14): an EXPLICIT MODEL_PROVIDER always wins (unchanged).
|
|
484
|
+
// When it is unset but the operator explicitly configured an Anthropic-protocol base URL AND a matching
|
|
485
|
+
// credential (the EXACT same `firstSetEnv` predicates the `anthropic` route assembly below uses — F8:
|
|
486
|
+
// non-empty selection, so an EMPTY first alias can't shadow a set fallback), the default is "anthropic" +
|
|
487
|
+
// a one-line boot notice — a clean-machine user pointing ANTHROPIC_BASE_URL at an Anthropic-compatible
|
|
488
|
+
// upstream must not have the model silently routed to the (unconfigured) openai gateway. Base URL alone or
|
|
489
|
+
// credential alone does NOT infer (a bare ANTHROPIC_API_KEY historically only assembles the cloud route for
|
|
490
|
+
// registry-declared anthropic models); neither present ⇒ "gateway", byte-identical to before.
|
|
491
|
+
const explicitProvider = process.env.MODEL_PROVIDER; // env() semantics: "" = unset
|
|
314
492
|
const inferredAnthropicBaseUrl = firstSetEnv("ANTHROPIC_BASEURL", "ANTHROPIC_BASE_URL");
|
|
315
493
|
const inferredAnthropicCred = firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN");
|
|
316
494
|
const provider = (() => {
|
|
@@ -329,6 +507,12 @@ export function loadConfig() {
|
|
|
329
507
|
}
|
|
330
508
|
return "gateway";
|
|
331
509
|
})();
|
|
510
|
+
// Generation-tuning passthrough (core 1.60 `Model.extraBody`, merged via applyExtraBody — brain-owned
|
|
511
|
+
// keys like temperature/max_tokens ALWAYS win, core warns on collision; unset → request unchanged).
|
|
512
|
+
// frequency/presence penalty are the common case (suppress degenerate looping at the source — the
|
|
513
|
+
// prevention to core's degenerate-repetition safety net); MODEL_EXTRA_BODY is the JSON escape hatch
|
|
514
|
+
// (top_k, logit_bias, …). MUST be STATIC (stable key order per cache namespace, else the prefix cache
|
|
515
|
+
// breaks — design/9/31), so it's built once here from fixed env, never per-task.
|
|
332
516
|
const extraBody = buildExtraBody();
|
|
333
517
|
const thinkingDefault = (() => { const v = process.env.MODEL_DEFAULT_THINKING; return v && isThinkingLevel(v) && v !== "off" ? v : undefined; })();
|
|
334
518
|
const effortLevels = (process.env.MODEL_REASONING_EFFORT_LEVELS ?? "").split(",").map((s) => s.trim()).filter((s) => isThinkingLevel(s) && s !== "off");
|
|
@@ -337,9 +521,23 @@ export function loadConfig() {
|
|
|
337
521
|
name: modelId,
|
|
338
522
|
api: provider === "anthropic" ? "anthropic-messages" : "openai-completions",
|
|
339
523
|
provider,
|
|
524
|
+
// Empty so the URL is owned by the BRAIN layer: each composed brain (gateway / failover fallback /
|
|
525
|
+
// Anthropic route) supplies its own baseUrl, and the brain resolves `model.baseUrl || config.baseUrl`.
|
|
526
|
+
// A non-empty value here would override every brain to one URL — breaking failover AND the Anthropic
|
|
527
|
+
// route (it would POST to the vLLM gateway). See design/15.
|
|
340
528
|
baseUrl: "",
|
|
341
529
|
reasoning: env("MODEL_REASONING", "true") === "true",
|
|
530
|
+
// vision precheck: the modality list drives BOTH the `model_no_vision` 422 (main.ts resolveSpec,
|
|
531
|
+
// when body.images hits a text-only model) AND the vision flag on GET /v1/models. The env-lane default model
|
|
532
|
+
// declares vision via MODEL_VISION — DEFAULT "true" keeps the historical image-capable behavior, so set
|
|
533
|
+
// MODEL_VISION=false for a text-only model (e.g. deepseek) to make the precheck actually fire instead of letting
|
|
534
|
+
// images透传 to an opaque downstream-gateway 400. (The centralized sema-registry lane derives input from its own
|
|
535
|
+
// per-model vision flag — see sema-registry.ts.)
|
|
342
536
|
input: env("MODEL_VISION", "true") === "true" ? ["text", "image"] : ["text"],
|
|
537
|
+
// Per-1M-token USD pricing (core `modelCostToPricing`: input→inputPer1M, …). Default 0 keeps the
|
|
538
|
+
// historical behavior (spend reads $0) — set MODEL_COST_* on a deploy to make `model_cost_micro_usd`
|
|
539
|
+
// + the Fleet card's costUsd real. Numbers are the gateway's contract price (e.g. the review gateway's
|
|
540
|
+
// deepseek-pro), owned by the operator — the center never stores them (design/15: secrets+billing in env).
|
|
343
541
|
cost: {
|
|
344
542
|
input: Number(env("MODEL_COST_INPUT", "0")),
|
|
345
543
|
output: Number(env("MODEL_COST_OUTPUT", "0")),
|
|
@@ -347,129 +545,134 @@ export function loadConfig() {
|
|
|
347
545
|
cacheWrite: Number(env("MODEL_COST_CACHE_WRITE", "0")),
|
|
348
546
|
},
|
|
349
547
|
contextWindow: Number(env("MODEL_CONTEXT_WINDOW", "262144")),
|
|
548
|
+
// [854]③a: env 车道缺省 4096 → 32000。4096 对 reasoning 模型太小(一次完整思考+答案都放不下,长输出
|
|
549
|
+
// 任务被腰斩;上游同类缺省 32000)。per-model clamp:缺省值不越过本 entry 自己声明的 contextWindow
|
|
550
|
+
// (操作员配了小窗口模型时,缺省输出上限不得超过窗口本身 —— env 车道 model entry 就在此装配,唯一可用
|
|
551
|
+
// 约束即 MODEL_CONTEXT_WINDOW)。显式 MODEL_MAX_TOKENS 一如既往原样生效(显式配置=操作员意图,不 clamp);
|
|
552
|
+
// catalog(config.d/models.json)与 center 车道不动(那是显式配置,BL-8 继承链读的是这里的最终值)。
|
|
350
553
|
maxTokens: process.env.MODEL_MAX_TOKENS !== undefined && process.env.MODEL_MAX_TOKENS !== ""
|
|
351
554
|
? Number(env("MODEL_MAX_TOKENS"))
|
|
352
555
|
: Math.min(32000, Number(env("MODEL_CONTEXT_WINDOW", "262144"))),
|
|
556
|
+
// design/123 D2 (core 1.226): structural token-estimate coefficient (chars ≈ 1 token) feeding every
|
|
557
|
+
// non-usage-anchored estimate in core (compaction trigger fallback, clearStale/trim defenses, cut-point
|
|
558
|
+
// accounting). UNSET → omit = core default 4 (byte-compatible chars/4 heuristic). CJK-heavy models
|
|
559
|
+
// (deepseek on Chinese tasks) should set 2–3 — chars/4 underestimates Chinese by 60–75%, letting the
|
|
560
|
+
// request-layer defenses pass over-window requests. optFinitePositiveEnv: a typo'd value degrades to
|
|
561
|
+
// the safe core default rather than zeroing every structural estimate (soft knob, no fail-loud boot).
|
|
353
562
|
...(optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") } : {}),
|
|
563
|
+
// Cache family = the BRAIN's usage semantics (design/18): the Anthropic brain reports `input`
|
|
564
|
+
// EXCLUDING cached, the openai gateway brain INCLUDES it. Declare it EXPLICITLY (core 1.23
|
|
565
|
+
// `model.params.promptCacheFamily`) instead of leaving core to INFER it from `model.api` — cleaner,
|
|
566
|
+
// and `api` stays free to mean only "wire format". core warns + clamps if it's wrong (1.23).
|
|
354
567
|
params: { promptCacheFamily: provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
|
|
568
|
+
// design/111 L2②: defaultThinking = this model's DEFAULT tier (resolution chain `spec.thinking ?? role.thinking ??
|
|
569
|
+
// model.defaultThinking`, prepare-task). reasoningEffortLevels (openai endpoints only — Anthropic budget honors the
|
|
570
|
+
// full range) DECLARES which tiers the endpoint accepts so a higher requested tier clamps DOWN instead of 422'ing.
|
|
355
571
|
...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
|
|
356
572
|
...(effortLevels.length > 0 && provider !== "anthropic" ? { compat: { reasoningEffortLevels: effortLevels } } : {}),
|
|
357
573
|
...(extraBody ? { extraBody } : {}),
|
|
358
574
|
};
|
|
575
|
+
// 1.24 role map: a catalog + role→model so subsystems resolve a ROLE (cost tiering) instead of a hard
|
|
576
|
+
// model name. An optional cheap model on the SAME gateway (MODEL_CHEAP_ID — just a different id) serves
|
|
577
|
+
// the `subagent`+`summarize` roles, so council lenses & compaction run cheap while the lead/arbiter/team
|
|
578
|
+
// stay on the main model. Unset MODEL_CHEAP_ID → every role resolves to the main model (unchanged).
|
|
359
579
|
const cheapId = process.env.MODEL_CHEAP_ID;
|
|
360
580
|
const cheapModel = cheapId
|
|
361
581
|
? {
|
|
362
582
|
...model,
|
|
363
583
|
id: cheapId,
|
|
364
584
|
name: cheapId,
|
|
585
|
+
// vision precheck (adversarial-review finding): the cheap model must declare its OWN vision
|
|
586
|
+
// capability. Spreading `...model` inherits the MAIN model's `input` modality, so a vision-capable main +
|
|
587
|
+
// text-only cheap (e.g. MODEL_CHEAP_ID=deepseek for subagent/summarize) would advertise vision on /v1/models
|
|
588
|
+
// AND defeat the precheck (config.models[cheapId] is user-pickable via body.model) — the SAME dead-code-on-a-lane
|
|
589
|
+
// class clay caught on the env main model. MODEL_CHEAP_VISION declares it (strict "true"); UNSET → inherit the
|
|
590
|
+
// main model's input (prior behavior, no change — the documented "same gateway, just a different id" case).
|
|
365
591
|
input: process.env.MODEL_CHEAP_VISION === undefined
|
|
366
592
|
? model.input
|
|
367
593
|
: process.env.MODEL_CHEAP_VISION === "true"
|
|
368
594
|
? ["text", "image"]
|
|
369
595
|
: ["text"],
|
|
596
|
+
// Own pricing: pricing is keyed by model id (budget.buildPricing), so without this the cheap model
|
|
597
|
+
// inherits the MAIN model's `cost` and subagent/summarize spend (council lenses, compaction) is
|
|
598
|
+
// metered at the PRO per-token price → cost metric + per-principal quota over-count. Set
|
|
599
|
+
// MODEL_CHEAP_COST_* to the real cheap price; unset → inherit the main cost (prior behavior, no change).
|
|
370
600
|
cost: {
|
|
371
601
|
input: Number(process.env.MODEL_CHEAP_COST_INPUT ?? model.cost.input),
|
|
372
602
|
output: Number(process.env.MODEL_CHEAP_COST_OUTPUT ?? model.cost.output),
|
|
373
603
|
cacheRead: Number(process.env.MODEL_CHEAP_COST_CACHE_READ ?? model.cost.cacheRead),
|
|
374
604
|
cacheWrite: Number(process.env.MODEL_CHEAP_COST_CACHE_WRITE ?? model.cost.cacheWrite),
|
|
375
605
|
},
|
|
606
|
+
// systematic-audit: the cheap model also inherited the MAIN model's contextWindow/maxTokens/reasoning verbatim
|
|
607
|
+
// via the `...model` spread — wrong if MODEL_CHEAP_ID points at a model with different limits/capability. Declare
|
|
608
|
+
// them per cheap (MODEL_CHEAP_*); UNSET → inherit main (the documented "same gateway, just a different id" case).
|
|
376
609
|
contextWindow: process.env.MODEL_CHEAP_CONTEXT_WINDOW ? Number(process.env.MODEL_CHEAP_CONTEXT_WINDOW) : model.contextWindow,
|
|
377
610
|
maxTokens: process.env.MODEL_CHEAP_MAX_TOKENS ? Number(process.env.MODEL_CHEAP_MAX_TOKENS) : model.maxTokens,
|
|
611
|
+
// design/123 D2: charsPerToken per cheap slot — the spread inherits the MAIN model's coefficient,
|
|
612
|
+
// wrong when the cheap slot is a different tokenizer family (e.g. CJK-dense deepseek cheap under an
|
|
613
|
+
// anthropic main). MODEL_CHEAP_CHARS_PER_TOKEN overrides; UNSET → inherit main (same knob family).
|
|
378
614
|
...(optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") } : {}),
|
|
379
615
|
reasoning: process.env.MODEL_CHEAP_REASONING === undefined ? model.reasoning : process.env.MODEL_CHEAP_REASONING === "true",
|
|
380
616
|
}
|
|
381
617
|
: undefined;
|
|
618
|
+
// [818]① 1M dual-window (core 1.289 `Model.autoCompactTokens`, additive): a 1M-window model gets
|
|
619
|
+
// autoCompactTokens=967000 (CC's 1M special-case = the autocompact window is 1M-33000; core then triggers at
|
|
620
|
+
// 967k-33k=934k with the guard on the PHYSICAL window — the exact CC alignment core recommends embedders set).
|
|
621
|
+
// Static catalog audit for this batch: ZERO baked 1M entries exist (env lane defaults 262144; center lane
|
|
622
|
+
// inherits/declares per roster) — so this is a DERIVATION on the declared window, not a data edit: any
|
|
623
|
+
// deployment that sets MODEL_CONTEXT_WINDOW>=1e6 (or a center roster with a 1M contextWindow —
|
|
624
|
+
// sema-registry.ts applies the same helper) gets the field automatically. <1M ⇒ field ABSENT (core's plain
|
|
625
|
+
// W-33000/0.7W geometry already matches CC there).
|
|
382
626
|
applyAutoCompactWindow(model);
|
|
383
627
|
if (cheapModel)
|
|
384
|
-
applyAutoCompactWindow(cheapModel);
|
|
628
|
+
applyAutoCompactWindow(cheapModel); // the ...model spread must not leak the MAIN window's field onto a smaller cheap window
|
|
385
629
|
const models = { default: model, [model.id]: model };
|
|
386
630
|
if (cheapModel)
|
|
387
631
|
models[cheapModel.id] = cheapModel;
|
|
388
632
|
const cheapRef = cheapModel ? cheapModel.id : "default";
|
|
633
|
+
// M1(接入审计 2026-07-08):core verify 契约(verify.d.ts)把「verifier 必须异于 implementer」定为
|
|
634
|
+
// 🔴 部署契约——verifier 落 default(=implementer)时对抗验证者自评自批,去相关性名存实亡。
|
|
635
|
+
// `MODEL_VERIFIER=<catalog 模型 id>` 真兑现旧注释的 "operator can override"(此前只有注释没有机制);
|
|
636
|
+
// 值必须命中 catalog,typo 回落 default 并由 main.ts boot warn 曝光(连同"单模型部署未配 verifier"一起)。
|
|
389
637
|
const verifierEnv = process.env.MODEL_VERIFIER;
|
|
390
638
|
const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : "default";
|
|
639
|
+
// rosterMarkerRoles 收敛(0.7 worker.model 真删同车):role→tier 的名单语义
|
|
640
|
+
// 单一来源 = registry-core 的 roster 标记常量(0.6.3 契约:ROSTER_PRIMARY_ROLES=["default","team","synthesize"],
|
|
641
|
+
// ROSTER_CHEAP_ROLES=["subagent","summarize"])——service 不再自带第二份「哪些 role 吃 cheap/primary」清单
|
|
642
|
+
// (双源并存期空转结束)。值仍指我方 catalog ref(cheap 槽未配 ⇒ cheapRef="default" 全落主模型,行为不变);
|
|
643
|
+
// `verifier` 是 service 专有 role(core verify 契约),不在 roster 标记语义内,保留显式。
|
|
391
644
|
const roles = {
|
|
392
|
-
verifier: verifierRef,
|
|
645
|
+
verifier: verifierRef, // adversarial verification gate (1.44) — MODEL_VERIFIER 指异构模型,未设=default(见 boot warn)
|
|
393
646
|
};
|
|
394
647
|
for (const r of ROSTER_PRIMARY_ROLES)
|
|
395
648
|
roles[r] = "default";
|
|
396
649
|
for (const r of ROSTER_CHEAP_ROLES)
|
|
397
650
|
roles[r] = cheapRef;
|
|
651
|
+
// Developer-mode role preset (core 1.43, opt-in, default-neutral): attach the de-branded
|
|
652
|
+
// CODE_AGENT_PROMPT to the named roles via RoleSpec.systemPrompt — those roles run a coding prompt
|
|
653
|
+
// (when the task doesn't supply its own systemPrompt) while every other role keeps the neutral
|
|
654
|
+
// default. `MODEL_CODE_ROLES` unset → no change. e.g. MODEL_CODE_ROLES=default,subagent.
|
|
398
655
|
for (const r of csv("MODEL_CODE_ROLES")) {
|
|
399
656
|
const role = r;
|
|
400
657
|
const cur = roles[role];
|
|
401
658
|
if (cur === undefined)
|
|
402
|
-
continue;
|
|
659
|
+
continue; // only decorate roles we actually define
|
|
660
|
+
// our role targets are model-name strings; keep an object target's fields if config ever sets one.
|
|
403
661
|
roles[role] =
|
|
404
662
|
typeof cur === "string"
|
|
405
663
|
? { model: cur, systemPrompt: CODE_AGENT_PROMPT }
|
|
406
664
|
: { ...cur, systemPrompt: CODE_AGENT_PROMPT };
|
|
407
665
|
}
|
|
408
|
-
const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
|
|
409
|
-
const principalJwtPubkeys = parsePrincipalJwks(process.env.PRINCIPAL_JWT_PUBKEYS);
|
|
410
|
-
const principalJwtIss = process.env.PRINCIPAL_JWT_ISS || undefined;
|
|
411
|
-
const principalJwtAud = process.env.PRINCIPAL_JWT_AUD || undefined;
|
|
412
|
-
const principalJwtMaxTtlSec = Math.max(0, numEnv("PRINCIPAL_JWT_MAX_TTL_SEC", "120"));
|
|
413
|
-
const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS);
|
|
414
|
-
const dgOperators = csv("OPERATOR_PRINCIPALS");
|
|
415
|
-
const dgDurable = boolEnv("DURABLE_APPROVAL", false);
|
|
416
|
-
const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
|
|
417
|
-
if (directApprovalDoor) {
|
|
418
|
-
const missing = [];
|
|
419
|
-
if (!dgDurable)
|
|
420
|
-
missing.push("DURABLE_APPROVAL=true (the direct door's crypto gate only exists on the durable /decide path)");
|
|
421
|
-
if (dgHmacKeys.length === 0)
|
|
422
|
-
missing.push("APPROVAL_HMAC_KEYS");
|
|
423
|
-
if (principalJwtPubkeys.length === 0)
|
|
424
|
-
missing.push("PRINCIPAL_JWT_PUBKEYS");
|
|
425
|
-
if (!principalJwtIss)
|
|
426
|
-
missing.push("PRINCIPAL_JWT_ISS");
|
|
427
|
-
if (!principalJwtAud)
|
|
428
|
-
missing.push("PRINCIPAL_JWT_AUD");
|
|
429
|
-
if (dgOperators.length === 0)
|
|
430
|
-
missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
|
|
431
|
-
if (!requirePrincipal)
|
|
432
|
-
missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the spoofable header governs owner/quota checks)");
|
|
433
|
-
if (missing.length) {
|
|
434
|
-
throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
const authTokens = Object.fromEntries((process.env.SERVICE_AUTH_TOKENS ?? "")
|
|
438
|
-
.split(",")
|
|
439
|
-
.map((s) => s.trim())
|
|
440
|
-
.filter((s) => s.includes("="))
|
|
441
|
-
.map((s) => [s.slice(0, s.indexOf("=")).trim(), s.slice(s.indexOf("=") + 1).trim()])
|
|
442
|
-
.filter(([tok, sys]) => tok.length > 0 && sys.length > 0));
|
|
443
|
-
const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
|
|
444
|
-
if (boolEnv("IMAGE_BAKES_ENABLED", false) && dgOperators.length === 0) {
|
|
445
|
-
throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
|
|
446
|
-
}
|
|
447
|
-
if (boolEnv("IMAGE_BAKES_ENABLED", false) && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
|
|
448
|
-
throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
|
|
449
|
-
}
|
|
450
|
-
const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
|
|
451
|
-
configLkgEnabled();
|
|
452
|
-
hostBackgroundShellEnabled();
|
|
453
|
-
hostExecSpoolEnabled();
|
|
454
|
-
const lspHostEnabled = (() => {
|
|
455
|
-
const explicitHost = process.env.LSP_HOST_ENABLED;
|
|
456
|
-
if (explicitHost === undefined || explicitHost === "") {
|
|
457
|
-
if (process.env.LSP_ENABLED === "false") {
|
|
458
|
-
CONFIG_NOTICES.push({
|
|
459
|
-
event: "config_env_deprecated_lane_alias",
|
|
460
|
-
fields: { deprecated: "LSP_ENABLED=false", replacement: "LSP_HOST_ENABLED=false", lane: "host", effective: "false" },
|
|
461
|
-
});
|
|
462
|
-
registerKnob({ name: "LSP_HOST_ENABLED", polarity: "opt-out", value: false, source: "legacy-env", legacyName: "LSP_ENABLED" });
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return boolEnv("LSP_HOST_ENABLED", true);
|
|
467
|
-
})();
|
|
468
666
|
return {
|
|
469
|
-
port: Number(env("PORT", "8090")),
|
|
470
667
|
gatewayBaseUrl,
|
|
471
668
|
gatewayApiKey: process.env.MODEL_API_KEY,
|
|
472
669
|
gatewayFallbackUrls: csv("MODEL_GATEWAY_FALLBACK_URLS"),
|
|
670
|
+
// Anthropic-compatible-ecosystem env compat (2026-07-03): the route now assembles when EITHER credential
|
|
671
|
+
// is present — `ANTHROPIC_API_KEY` (x-api-key) or `ANTHROPIC_AUTH_TOKEN` (Authorization: Bearer; many such
|
|
672
|
+
// providers accept only this). AUTH_TOKEN wins when both are set. Base URL reads the widely-adopted
|
|
673
|
+
// standard name `ANTHROPIC_BASE_URL` too — the explicit sema name (no underscore) wins on a conflict.
|
|
674
|
+
// F8: alias selection via `firstSetEnv` ("" = unset) — the SAME helper the provider inference above
|
|
675
|
+
// consumes, so an empty first alias can neither suppress the route nor shadow the fallback base URL.
|
|
473
676
|
anthropic: firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
|
|
474
677
|
? {
|
|
475
678
|
apiKey: firstSetEnv("ANTHROPIC_API_KEY"),
|
|
@@ -481,8 +684,16 @@ export function loadConfig() {
|
|
|
481
684
|
}
|
|
482
685
|
: undefined,
|
|
483
686
|
resilience: {
|
|
687
|
+
// BEHAVIOR CHANGE ([880] stall-watchdog audit): connect/firstToken default ON. core's idle
|
|
688
|
+
// timer only arms AFTER the first token (bumpIdle lives in sawContentToken), so with these
|
|
689
|
+
// two off a stream that opens but never emits hangs unbounded — the one true blind spot the
|
|
690
|
+
// fleet audit found. Values follow the [880] recommendation; =0 restores off.
|
|
484
691
|
connectTimeoutMs: Number(env("MODEL_CONNECT_TIMEOUT_MS", "30000")),
|
|
485
692
|
firstTokenTimeoutMs: Number(env("MODEL_FIRST_TOKEN_TIMEOUT_MS", "120000")),
|
|
693
|
+
// BEHAVIOR CHANGE (CC 196 parity): mid-stream stall watchdog default ON at 5min —
|
|
694
|
+
// after output starts, 5min of zero deltas cancels the stream as a retryable [network] failure
|
|
695
|
+
// (retry/failover recovers; without it a hung gateway pins the turn until the user Escapes).
|
|
696
|
+
// MODEL_IDLE_TIMEOUT_MS=0 restores off.
|
|
486
697
|
idleTimeoutMs: Number(env("MODEL_IDLE_TIMEOUT_MS", "300000")),
|
|
487
698
|
circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", false),
|
|
488
699
|
failureThreshold: Number(env("MODEL_CB_FAILURE_THRESHOLD", "5")),
|
|
@@ -490,48 +701,279 @@ export function loadConfig() {
|
|
|
490
701
|
},
|
|
491
702
|
model,
|
|
492
703
|
models,
|
|
493
|
-
modelApiKeyEnv: {},
|
|
494
|
-
modelApiKeys: {},
|
|
495
|
-
modelQuotaWeights: {},
|
|
496
|
-
tiers: {},
|
|
497
|
-
projects: {},
|
|
704
|
+
modelApiKeyEnv: {}, // env path has a single gateway key; sema-registry populates per-model overrides
|
|
705
|
+
modelApiKeys: {}, // sealed-box 解封产物(内存-only 明文):env lane 恒空;applyEffective 解封填充
|
|
706
|
+
modelQuotaWeights: {}, // weight-at-burn 表:env lane 恒空(weight=1 兜底);registry apply 填充
|
|
707
|
+
tiers: {}, // 档位表:env lane 恒空(INERT);applyEffective 从 models.tierGroups 解析填充(保引用)
|
|
708
|
+
projects: {}, // 142-S4 项目登记簿:env lane 恒空(INERT);applyEffective 从 projects 域填充(保引用)
|
|
498
709
|
roles,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
710
|
+
cascadeLadder: csv("MODEL_CASCADE_LADDER"),
|
|
711
|
+
degrade: process.env.MODEL_DEGRADE_TO
|
|
712
|
+
? {
|
|
713
|
+
to: env("MODEL_DEGRADE_TO"),
|
|
714
|
+
atCostFraction: Number(env("MODEL_DEGRADE_AT_COST_FRACTION", "0.7")),
|
|
715
|
+
reactive: boolEnv("MODEL_DEGRADE_REACTIVE", false),
|
|
716
|
+
downgradeOn: csv("MODEL_DEGRADE_ON").length
|
|
717
|
+
? csv("MODEL_DEGRADE_ON")
|
|
718
|
+
: undefined,
|
|
719
|
+
// vision precheck (adversarial-review finding): the degrade TARGET is an external gateway
|
|
720
|
+
// model NOT in config.models, so resolveSpec's precheck (which only sees the picked model) can't see its
|
|
721
|
+
// vision capability — a vision-capable main + text-only degrade target would let images reach the text-only
|
|
722
|
+
// gateway as the opaque 400 the precheck prevents, AFTER admission, at runtime degrade. Declare it via
|
|
723
|
+
// MODEL_DEGRADE_TO_VISION (strict "true"). DEFAULT false = assume the cheaper degrade target is text-only
|
|
724
|
+
// (the common case, e.g. deepseek), so resolveSpec drops degrade for image-carrying tasks (main.ts).
|
|
725
|
+
toSupportsImages: boolEnv("MODEL_DEGRADE_TO_VISION", false),
|
|
726
|
+
}
|
|
727
|
+
: undefined,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/** 域:approval(审批/HITL 门)—— 审批名单与预算、durable 面、直连门开关与钥、敏感写门、elicitation。
|
|
731
|
+
* 注:直连门的**激活**(directDoorActive)与 D-G §3 半开门不变量是跨域合取,留在装配层。 */
|
|
732
|
+
function parseApprovalDomain(ctx) {
|
|
733
|
+
const { postureOn } = ctx; // 跨域入参②:posture 三态(single-user turnkey ⇒ HITL 面默认 ON)
|
|
734
|
+
// design/80 D-G: the direct-connect approval door anchors (all must be present to ACTIVATE; fail-closed).
|
|
735
|
+
const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
|
|
736
|
+
const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS);
|
|
737
|
+
// DURABLE_APPROVAL 既是本域字段,也是直连门的硬前置 —— 那条前置的理由与断言在装配层的跨域不变量段。
|
|
738
|
+
const durableApproval = boolEnv("DURABLE_APPROVAL", false);
|
|
739
|
+
return {
|
|
740
|
+
approvalRequire: csv("APPROVAL_REQUIRE"),
|
|
741
|
+
approvalDeny: csv("APPROVAL_DENY"),
|
|
742
|
+
approvalPollMs: Number(env("APPROVAL_POLL_MS", "1500")),
|
|
743
|
+
durableApproval,
|
|
744
|
+
resourceSuspend: boolEnv("RESOURCE_SUSPEND", false),
|
|
745
|
+
resourceSuspendTtlSec: Math.max(0, numEnv("RESOURCE_SUSPEND_TTL_SEC", "0")), // 0 ⇒ core default (30d)
|
|
746
|
+
approvalAutoBudget: Math.min(10000, Math.max(0, numEnv("APPROVAL_AUTO_BUDGET", "0"))), // clamp [0,10000]; 0 = off
|
|
747
|
+
approvalNeverAuto: csv("APPROVAL_NEVER_AUTO"),
|
|
748
|
+
approvalHmacKeys: dgHmacKeys, // D-G: WIRED (see the field's JSDoc); empty ⇒ inactive, not unimplemented
|
|
749
|
+
directApprovalDoor,
|
|
750
|
+
// ③ sensitive-path write deny set: unset = core's recommended set; explicit value = full replacement
|
|
751
|
+
// (comma-separated); "off"/empty = disabled. Set curation is core's (成文 core-side); server passes through.
|
|
752
|
+
sensitiveWritePatterns: ((raw) => {
|
|
753
|
+
if (raw === undefined)
|
|
754
|
+
return [...RECOMMENDED_SENSITIVE_PATTERNS];
|
|
755
|
+
const trimmed = raw.trim();
|
|
756
|
+
if (trimmed === "" || trimmed.toLowerCase() === "off")
|
|
757
|
+
return [];
|
|
758
|
+
return trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
759
|
+
})(process.env.SENSITIVE_WRITE_PATTERNS),
|
|
760
|
+
// [1557]§四: opt-in only — any value other than the two real gate shapes (incl. "off"/unset) stays undefined
|
|
761
|
+
// (no field on the config object at all), so the fs-write gate wiring never even considers it.
|
|
762
|
+
...(process.env.MANUAL_MODE_SHELL_GATE === "always" || process.env.MANUAL_MODE_SHELL_GATE === "classify"
|
|
763
|
+
? { manualModeShellGate: process.env.MANUAL_MODE_SHELL_GATE }
|
|
764
|
+
: {}),
|
|
765
|
+
mcpElicitation: {
|
|
766
|
+
enabled: boolEnv("MCP_ELICITATION_ENABLED", false), // E23 inbound elicitation; default OFF (fail-closed)
|
|
767
|
+
throttle: {
|
|
768
|
+
maxConcurrentPerRun: numEnvBounded("MCP_ELICITATION_MAX_CONCURRENT", String(DEFAULT_ELICITATION_THROTTLE.maxConcurrentPerRun), 1, 64),
|
|
769
|
+
maxTotalPerRun: numEnvBounded("MCP_ELICITATION_MAX_TOTAL", String(DEFAULT_ELICITATION_THROTTLE.maxTotalPerRun), 1, 10_000),
|
|
770
|
+
minIntervalMsPerServer: numEnvBounded("MCP_ELICITATION_MIN_INTERVAL_MS", String(DEFAULT_ELICITATION_THROTTLE.minIntervalMsPerServer), 0, 600_000),
|
|
771
|
+
ttlMs: numEnvBounded("MCP_ELICITATION_TTL_MS", String(DEFAULT_ELICITATION_THROTTLE.ttlMs), 1_000, 3_600_000),
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"), // §4④ AskUserQuestion live HITL; posture-gated (single-user → ON)
|
|
775
|
+
toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"), // [816]/[820]② live tool-approval HITL; posture-gated (single-user → ON), mirrors askQuestion
|
|
776
|
+
// [875]a 成文:0(缺省)= durable HITL 无限期等人,时间型 reapSuspended 不跑;file/memory lane 的回收
|
|
777
|
+
// 探针只在 DURABLE_APPROVAL=true 时注入(无 durable 的部署两只 suspended 回收器恒 NO-OP,parked 行的
|
|
778
|
+
// 恢复把手 = POST /v1/runs/:id/cancel,[868]①)。checkpoint 过期驱动的那只不受本旋钮门控。
|
|
779
|
+
approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
/** 域:memory(记忆面 + TOC 同步腿)—— 记忆引擎开关/方言/根目录、scope 缺省、同步 client 三键、项目记忆。 */
|
|
783
|
+
function parseMemoryDomain(ctx) {
|
|
784
|
+
const { requirePrincipal } = ctx; // 跨域入参①:多租户下记忆面 DARK(scope 缺省不铸)
|
|
785
|
+
// design/138 S1 (clay 2026-07-08): long-term memory = the injection-first file-based memory ENGINE
|
|
786
|
+
// (core RunnerDeps.memoryBackend), default ON with MEMORY_ENGINE=off as the explicit kill-switch.
|
|
787
|
+
// The legacy MEMORY_BACKEND/EMBEDDING_* store plane was dropped without migration (those env vars are
|
|
788
|
+
// no longer read). Single-user gating lives in main.ts (the engine is never wired multi-tenant).
|
|
789
|
+
const memoryEngineEnabled = process.env.MEMORY_ENGINE !== "off";
|
|
790
|
+
// S3-TOB(边界重切后的 backend 选择,设计 §1.3):记忆持久面方言。缺省 file=现状零变化;
|
|
791
|
+
// pg/tidb=DB 真身(零卷主档)——多租户点亮的唯一门(显式 opt-in,复审 F5:绝不静默翻转)。
|
|
792
|
+
const memoryEngineBackendRaw = process.env.MEMORY_ENGINE_BACKEND ?? "file";
|
|
793
|
+
if (!["file", "pg", "tidb"].includes(memoryEngineBackendRaw)) {
|
|
794
|
+
throw new Error(`MEMORY_ENGINE_BACKEND must be file|pg|tidb, got "${memoryEngineBackendRaw}"`);
|
|
795
|
+
}
|
|
796
|
+
const memoryEngineBackend = memoryEngineBackendRaw;
|
|
797
|
+
// Single-user memory scope (hoisted from the config literal so the sync-scope default below can consume it):
|
|
798
|
+
// explicit MEMORY_SCOPE always wins; default "local" only when the engine is on AND single-user (see the
|
|
799
|
+
// literal-site comment on why multi-tenant/engine-off must stay undefined = memory dark).
|
|
800
|
+
const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
|
|
801
|
+
// 142-S2.5-W1: TOC 同步 client 腿的三键(file memory 形态专用)。MEMORY_SYNC_URL 设了=开;
|
|
802
|
+
// 缺省 undefined=纯本地现状零变化。半配置 fail-loud(memoryEngineBackend 同款姿势,绝不静默降级):
|
|
803
|
+
// - URL 有 TOKEN 无 ⇒ throw(中心同步面要 Bearer,缺 token 的“开了但永远 401”是半配置陷阱);
|
|
804
|
+
// - TOKEN/SCOPE 有 URL 无 ⇒ throw(operator 显然想开同步,静默不跑=同款陷阱,对称拒启);
|
|
805
|
+
// - DB backend(pg/tidb)配了任意同步键 ⇒ throw(中心侧自己就是 POST /v1/memory/sync/:scope 的权威,
|
|
806
|
+
// 不该再跑 client 腿——两个半场叠在一个进程是拓扑错误)。
|
|
807
|
+
// scope 缺省 = formatUserScope(memoryScope)(中心侧 owner 门的 user 盘键铸法);memoryScope 本身已是
|
|
808
|
+
// v2 typed key(user:/org:/proj:/userproj:,只可能来自 operator 显式 MEMORY_SCOPE)则原样直通——
|
|
809
|
+
// 再包一层会铸出 `user:user%3A...` 双包键(诚实偏离,报告记档)。
|
|
810
|
+
const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
|
|
811
|
+
const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
|
|
812
|
+
const memorySyncScopeRaw = process.env.MEMORY_SYNC_SCOPE || undefined;
|
|
813
|
+
let memorySync;
|
|
814
|
+
if (memorySyncUrl === undefined) {
|
|
815
|
+
if (memorySyncToken !== undefined || memorySyncScopeRaw !== undefined) {
|
|
816
|
+
throw new Error("MEMORY_SYNC_TOKEN/MEMORY_SYNC_SCOPE are set but MEMORY_SYNC_URL is not — refusing to start half-configured (set MEMORY_SYNC_URL to enable memory sync, or unset the other MEMORY_SYNC_* keys)");
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
if (memoryEngineBackend !== "file") {
|
|
821
|
+
throw new Error(`MEMORY_SYNC_URL is a FILE-memory (TOC client) knob, but MEMORY_ENGINE_BACKEND=${memoryEngineBackend} — the DB memory plane IS the central sync authority (it serves POST /v1/memory/sync/:scope) and must not also run the client leg; unset MEMORY_SYNC_* here`);
|
|
822
|
+
}
|
|
823
|
+
if (memorySyncToken === undefined) {
|
|
824
|
+
throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
|
|
825
|
+
}
|
|
826
|
+
const isV2ScopeKey = (k) => k.startsWith("user:") || k.startsWith("org:") || k.startsWith("proj:") || k.startsWith("userproj:");
|
|
827
|
+
const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
|
|
828
|
+
if (scope === undefined) {
|
|
829
|
+
throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
|
|
830
|
+
}
|
|
831
|
+
memorySync = { url: memorySyncUrl, token: memorySyncToken, scope, ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPushEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES) : undefined), ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPullEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES) : undefined) };
|
|
832
|
+
}
|
|
833
|
+
// design/158 B4 ② 的 D 家族四枚里,唯一落在 ServiceConfig 上的就是这枚(其余三枚只在使用点懒读,
|
|
834
|
+
// 预登记留在 orchestration 域 —— 顺序与改前一致:本枚的弃用通知仍先于那三枚)。
|
|
835
|
+
const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
|
|
836
|
+
return {
|
|
502
837
|
memoryEngineEnabled,
|
|
503
838
|
memoryEngineDir: process.env.MEMORY_ENGINE_DIR || undefined,
|
|
504
|
-
memoryEngineRemoteLaneAllowed: process.env.MEMORY_ENGINE_REMOTE_LANE === "allow",
|
|
839
|
+
memoryEngineRemoteLaneAllowed: process.env.MEMORY_ENGINE_REMOTE_LANE === "allow", // N0: fail-closed default
|
|
840
|
+
// A single-user deployment has no principal, so `memoryScopeFor` falls back to `config.memoryScope` — and if
|
|
841
|
+
// THAT were unset the resolved scope would be undefined ⇒ resolveSpec builds NO `spec.memory` ⇒ the memory
|
|
842
|
+
// engine never materializes (the "wired-not-triggered" gap a §4 real test hit on the legacy plane).
|
|
843
|
+
// Default it to "local" for single-user (REQUIRE_PRINCIPAL !== "true") so TOC local memory works out-of-the-box
|
|
844
|
+
// (CC-parity). Multi-tenant memory is DARK by design (design/138 S1: the file basement has no tenant isolation —
|
|
845
|
+
// memoryScopeFor returns undefined there). An explicit MEMORY_SCOPE always wins.
|
|
505
846
|
memoryEngineBackend,
|
|
506
|
-
|
|
507
|
-
memoryScope,
|
|
847
|
+
memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
|
|
508
848
|
...(memorySync ? { memorySync } : {}),
|
|
849
|
+
projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (legacy PROJECT_MEMORY_DISABLED still honored)
|
|
850
|
+
syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/** 域:auth(身份/凭证/绑址)—— service token 面、CORS、principal 头与开关、JWT 锚、autonomy、绑址。 */
|
|
854
|
+
function parseAuthDomain(ctx) {
|
|
855
|
+
const { requirePrincipal } = ctx; // 装配层已解析(它是四个域的共同入参),本域只负责把它落到字段上
|
|
856
|
+
// design/80 D-G 直连门的 principal-JWT 锚:验签材料属 auth 域,门的激活与半开门不变量在装配层。
|
|
857
|
+
const principalJwtPubkeys = parsePrincipalJwks(process.env.PRINCIPAL_JWT_PUBKEYS);
|
|
858
|
+
const principalJwtIss = process.env.PRINCIPAL_JWT_ISS || undefined;
|
|
859
|
+
const principalJwtAud = process.env.PRINCIPAL_JWT_AUD || undefined;
|
|
860
|
+
const principalJwtMaxTtlSec = Math.max(0, numEnv("PRINCIPAL_JWT_MAX_TTL_SEC", "120")); // 0 = no cap; default ≤120s
|
|
861
|
+
// Per-system service credentials (token → system name). Parsed as a DOMAIN field (not inline in the return
|
|
862
|
+
// literal) so the bake boot invariant at the assembly layer can assert the runner-credential→runnerPrincipal
|
|
863
|
+
// mapping exists.
|
|
864
|
+
const authTokens = Object.fromEntries((process.env.SERVICE_AUTH_TOKENS ?? "")
|
|
865
|
+
.split(",")
|
|
866
|
+
.map((s) => s.trim())
|
|
867
|
+
.filter((s) => s.includes("="))
|
|
868
|
+
.map((s) => [s.slice(0, s.indexOf("=")).trim(), s.slice(s.indexOf("=") + 1).trim()])
|
|
869
|
+
.filter(([tok, sys]) => tok.length > 0 && sys.length > 0));
|
|
870
|
+
return {
|
|
871
|
+
// 绑址([1934]):BIND_HOST 优先,HOST 兼容(cli/桌面传的就是它);空串=未设。
|
|
872
|
+
bindHost: process.env.BIND_HOST || process.env.HOST || undefined,
|
|
873
|
+
authToken: process.env.SERVICE_AUTH_TOKEN,
|
|
874
|
+
authTokens,
|
|
875
|
+
allowUnauthedWrites: boolEnv("ALLOW_UNAUTHED_WRITES", false),
|
|
876
|
+
corsOrigins: (process.env.CORS_ORIGIN ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
877
|
+
principalHeader: headerNameEnv("PRINCIPAL_HEADER", "x-agent-principal"),
|
|
878
|
+
requirePrincipal,
|
|
879
|
+
// Runtime governance second baton (center §10): AUTONOMY env for the env/local path; sema-registry overlays
|
|
880
|
+
// it (applyRuntimeHot). commandPolicy is structured config — sema-registry-only (no flat env scalar form).
|
|
881
|
+
autonomy: parseAutonomy(process.env.AUTONOMY),
|
|
882
|
+
operatorPrincipals: csv("OPERATOR_PRINCIPALS"),
|
|
883
|
+
principalJwtPubkeys,
|
|
884
|
+
principalJwtIss,
|
|
885
|
+
principalJwtAud,
|
|
886
|
+
principalJwtMaxTtlSec,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/** 域:orchestration(编排 + 执行车道)—— remoteExec 五车道/worktree 隔离、leader/router/scheduler/fork、
|
|
890
|
+
* workflow 与后台 agent 的存留旋钮、沙箱面(pkg 源/env facts/LSP)、烤镜像门。 */
|
|
891
|
+
function parseOrchestrationDomain(ctx) {
|
|
892
|
+
const { postureOn } = ctx; // 跨域入参②:posture 三态(single-user turnkey ⇒ 自编排/fork/scheduler 默认 ON)
|
|
893
|
+
// workflowSizeGuideline: CC normalizes unknown→unrestricted (lenient), but an env knob typo
|
|
894
|
+
// silently degrading to "no guidance" is the half-config trap — fail-loud here (memoryEngineBackend
|
|
895
|
+
// posture; the ADVISORY nature is core's concern, the env spelling is ours).
|
|
896
|
+
const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
|
|
897
|
+
if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
|
|
898
|
+
throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
|
|
899
|
+
}
|
|
900
|
+
const workflowSizeGuideline = workflowSizeGuidelineRaw;
|
|
901
|
+
const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
|
|
902
|
+
// ── design/158 B4 ② — the four knobs whose canonical name went from negative to positive. Only the first
|
|
903
|
+
// lands on `ServiceConfig`; the other three are read lazily at their use sites (main.ts's LKG gate,
|
|
904
|
+
// remote-env-host's capability probe and exec path) but are resolved HERE too so that their polarity row and
|
|
905
|
+
// any legacy-alias deprecation notice are emitted once, at boot, with every other config diagnostic — an
|
|
906
|
+
// operator must not have to trigger a background shell before being told the name they set is retired.
|
|
907
|
+
// (家族的第一枚 PROJECT_MEMORY_ENABLED 是 memory 域的字段,已在那里解析 —— 通知顺序不变。)
|
|
908
|
+
configLkgEnabled();
|
|
909
|
+
hostBackgroundShellEnabled();
|
|
910
|
+
hostExecSpoolEnabled();
|
|
911
|
+
// ── design/158 B4 ③ — LSP: one knob per lane. `LSP_ENABLED` used to drive BOTH the sandbox lane (opt-in,
|
|
912
|
+
// default OFF — it needs a baked `sema-code-lsp` template) and the host lane (opt-out, default ON — it needs
|
|
913
|
+
// nothing and degrades to grep/read), i.e. one env name with two OPPOSITE defaults and no way to silence the
|
|
914
|
+
// host lane without also naming the sandbox knob. `LSP_HOST_ENABLED` now owns the host lane; an explicit
|
|
915
|
+
// `LSP_ENABLED=false` keeps working as a host opt-out (it was the only one operators ever had) with a notice
|
|
916
|
+
// naming the replacement, and `LSP_HOST_ENABLED` always wins when set.
|
|
917
|
+
const lspHostEnabled = (() => {
|
|
918
|
+
const explicitHost = process.env.LSP_HOST_ENABLED;
|
|
919
|
+
if (explicitHost === undefined || explicitHost === "") {
|
|
920
|
+
if (process.env.LSP_ENABLED === "false") {
|
|
921
|
+
CONFIG_NOTICES.push({
|
|
922
|
+
event: "config_env_deprecated_lane_alias",
|
|
923
|
+
fields: { deprecated: "LSP_ENABLED=false", replacement: "LSP_HOST_ENABLED=false", lane: "host", effective: "false" },
|
|
924
|
+
});
|
|
925
|
+
registerKnob({ name: "LSP_HOST_ENABLED", polarity: "opt-out", value: false, source: "legacy-env", legacyName: "LSP_ENABLED" });
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return boolEnv("LSP_HOST_ENABLED", true);
|
|
930
|
+
})();
|
|
931
|
+
return {
|
|
509
932
|
remoteExec: process.env.REMOTE_EXEC === "e2b" && process.env.E2B_API_KEY
|
|
510
933
|
? {
|
|
511
934
|
provider: "e2b",
|
|
512
935
|
apiKey: process.env.E2B_API_KEY,
|
|
513
936
|
...(process.env.E2B_TEMPLATE ? { template: process.env.E2B_TEMPLATE } : {}),
|
|
514
937
|
...(process.env.E2B_TIMEOUT_MS ? { timeoutMs: Number(process.env.E2B_TIMEOUT_MS) } : {}),
|
|
938
|
+
// Idle/liveness bound for a command (default 120s, the hang-fix). A long SILENT build —
|
|
939
|
+
// gradle's first dependency download produces no output for minutes (gate#2 P1 finding) — looks
|
|
940
|
+
// like a hang and gets killed; raise this for workers that run such tools. Agents should still
|
|
941
|
+
// prefer background+poll, but this is the operator escape hatch.
|
|
515
942
|
...(process.env.E2B_LIVENESS_MS ? { livenessMs: Number(process.env.E2B_LIVENESS_MS) } : {}),
|
|
516
943
|
...(process.env.E2B_ALLOW_NET ? { allowInternetAccess: boolEnv("E2B_ALLOW_NET", true) } : {}),
|
|
944
|
+
// Out-of-band secret/config env injected sandbox-wide (OA Phase-B, design/48 §5 gap 7):
|
|
945
|
+
// E2B_SANDBOX_ENV is a CSV of THIS worker's OWN env-var NAMES to forward into the sandbox under the
|
|
946
|
+
// same name (name-only config keeps the secret in the worker's env, never in the spec/center). e.g.
|
|
947
|
+
// `E2B_SANDBOX_ENV=GIT_TOKEN` → the agent's git can auth via $GIT_TOKEN (GIT_ASKPASS / credential
|
|
948
|
+
// helper) without the token ever entering the prompt, the command string, or the durable event log.
|
|
517
949
|
...(process.env.E2B_SANDBOX_ENV ? { sandboxEnv: resolveEnvForward(process.env.E2B_SANDBOX_ENV) } : {}),
|
|
950
|
+
// [1452]/[1454] CWD-A:workspace 根显式可配(其余四车道 *_MOUNT_PATH 早有;e2b 此前配置层
|
|
951
|
+
// 不可达,恒 /home/user)。exec/spawnBackground/fs 三腿同源消费(remote-env-e2b this.cwd)。
|
|
952
|
+
// ⚠️ 变更纪律(codex 终审 high):resume 契约只回传 snapshotId,恢复的沙箱以**当前**配置值为
|
|
953
|
+
// cwd——改此值前先 drain 挂起中的 e2b 任务,否则 resumed 任务的 exec 落新根而产物在旧根
|
|
954
|
+
// (根修=core resumeVM 契约带回 WorkspaceHandle.mountPath,已转 core)。
|
|
518
955
|
...(process.env.E2B_MOUNT_PATH ? { mountPath: process.env.E2B_MOUNT_PATH } : {}),
|
|
519
956
|
}
|
|
520
|
-
:
|
|
957
|
+
: // Kata pod sandbox on our k3s: image required; auth explicit (K8S_API_URL/K8S_TOKEN) or in-cluster SA.
|
|
521
958
|
process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE
|
|
522
959
|
? {
|
|
523
960
|
provider: "k8s",
|
|
524
961
|
image: process.env.K8S_SANDBOX_IMAGE,
|
|
525
962
|
...(process.env.K8S_API_URL ? { apiUrl: process.env.K8S_API_URL } : {}),
|
|
526
963
|
...(process.env.K8S_TOKEN ? { token: process.env.K8S_TOKEN } : {}),
|
|
964
|
+
// base64 to survive sema-registry's no-newline secrets API (same constraint as SSH_PRIVATE_KEY).
|
|
527
965
|
...(process.env.K8S_CA_CERT_B64 ? { caCert: Buffer.from(process.env.K8S_CA_CERT_B64, "base64").toString("utf8") } : {}),
|
|
528
966
|
...(boolEnv("K8S_INSECURE_TLS", false) ? { insecureTls: true } : {}),
|
|
529
967
|
...(process.env.K8S_NAMESPACE ? { namespace: process.env.K8S_NAMESPACE } : {}),
|
|
530
968
|
...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
|
|
531
969
|
...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
|
|
532
970
|
...(process.env.K8S_TIMEOUT_MS ? { timeoutMs: Number(process.env.K8S_TIMEOUT_MS) } : {}),
|
|
971
|
+
// Resource profile knob (resources are profile data, not mechanism — center/service
|
|
972
|
+
// config, never wire-hardcoded). Found live: 25-module gradle integration OOM'd the 2Gi default.
|
|
533
973
|
...(process.env.K8S_MEMORY ? { memory: process.env.K8S_MEMORY } : {}),
|
|
534
974
|
...(process.env.K8S_CPU ? { cpu: process.env.K8S_CPU } : {}),
|
|
975
|
+
// Workspace-durable suspend/resume: adapter-held MinIO creds (secret minio-agent-worker);
|
|
976
|
+
// the sandbox only ever sees a presigned URL. All three keys required to enable — else suspendable:false.
|
|
535
977
|
...(process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
536
978
|
? {
|
|
537
979
|
s3Snapshot: {
|
|
@@ -546,7 +988,7 @@ export function loadConfig() {
|
|
|
546
988
|
}
|
|
547
989
|
: {}),
|
|
548
990
|
}
|
|
549
|
-
:
|
|
991
|
+
: // design/61: SSH → a real host. Key from the adapter-held secret env SSH_PRIVATE_KEY (never the workspace).
|
|
550
992
|
process.env.REMOTE_EXEC === "ssh" && process.env.SSH_HOST && process.env.SSH_USER && process.env.SSH_PRIVATE_KEY
|
|
551
993
|
? {
|
|
552
994
|
provider: "ssh",
|
|
@@ -556,7 +998,7 @@ export function loadConfig() {
|
|
|
556
998
|
...(process.env.SSH_PORT ? { port: Number(process.env.SSH_PORT) } : {}),
|
|
557
999
|
...(process.env.SSH_MOUNT_PATH ? { mountPath: process.env.SSH_MOUNT_PATH } : {}),
|
|
558
1000
|
}
|
|
559
|
-
:
|
|
1001
|
+
: // design/61: ADB → a real device (serial or host:port). adb binary must be on PATH in the image.
|
|
560
1002
|
process.env.REMOTE_EXEC === "adb" && process.env.ADB_SERIAL
|
|
561
1003
|
? {
|
|
562
1004
|
provider: "adb",
|
|
@@ -564,14 +1006,18 @@ export function loadConfig() {
|
|
|
564
1006
|
...(process.env.ADB_PATH ? { adbPath: process.env.ADB_PATH } : {}),
|
|
565
1007
|
...(process.env.ADB_MOUNT_PATH ? { mountPath: process.env.ADB_MOUNT_PATH } : {}),
|
|
566
1008
|
}
|
|
567
|
-
:
|
|
1009
|
+
: // DUAL-MODE §5: `host` = run on THIS machine, no container (the TOC default). No required env.
|
|
568
1010
|
process.env.REMOTE_EXEC === "host"
|
|
569
1011
|
? {
|
|
570
1012
|
provider: "host",
|
|
571
1013
|
...(process.env.HOST_WORKSPACE_BASE ? { workspaceBase: process.env.HOST_WORKSPACE_BASE } : {}),
|
|
572
1014
|
...(process.env.HOST_COMMAND_TIMEOUT_MS ? { commandTimeoutMs: Number(process.env.HOST_COMMAND_TIMEOUT_MS) } : {}),
|
|
573
1015
|
}
|
|
574
|
-
:
|
|
1016
|
+
: // DUAL-MODE §5: `local-docker` = a per-task container on THIS machine's docker daemon (isolation:true,
|
|
1017
|
+
// suspendable:false). DOCKER_IMAGE is REQUIRED — no docker.io default (domestic-images iron rule); a
|
|
1018
|
+
// deployment points it at the docker.io/claybobby public pool or an in-network mirror (SWR
|
|
1019
|
+
// deprecated). Secrets are env-NAMEs the worker resolves from its
|
|
1020
|
+
// own process.env (DOCKER_SANDBOX_ENV, a CSV of NAMEs) — never the values in the spec/center.
|
|
575
1021
|
process.env.REMOTE_EXEC === "local-docker" && process.env.DOCKER_IMAGE
|
|
576
1022
|
? {
|
|
577
1023
|
provider: "local-docker",
|
|
@@ -589,10 +1035,17 @@ export function loadConfig() {
|
|
|
589
1035
|
...(process.env.DOCKER_COMMAND_TIMEOUT_MS ? { commandTimeoutMs: Number(process.env.DOCKER_COMMAND_TIMEOUT_MS) } : {}),
|
|
590
1036
|
...(process.env.DOCKER_SANDBOX_ENV ? { env: resolveEnvForward(process.env.DOCKER_SANDBOX_ENV) } : {}),
|
|
591
1037
|
}
|
|
592
|
-
:
|
|
1038
|
+
: // TOC first-run default: in LOCAL config mode (CONFIG_PROVIDER=local — the
|
|
1039
|
+
// single-machine source) with no explicit REMOTE_EXEC, default to the `host` lane so "fill .env
|
|
1040
|
+
// and run" works without the user setting REMOTE_EXEC. Fleet/remote (CONFIG_PROVIDER≠local) keeps
|
|
1041
|
+
// the in-process stub default (REMOTE_EXEC unset = OA/review posture) — unchanged.
|
|
593
1042
|
process.env.CONFIG_PROVIDER === "local"
|
|
594
1043
|
? { provider: "host" }
|
|
595
1044
|
: undefined,
|
|
1045
|
+
// SVC-3 git-worktree isolation: opt-in, operator-supplied ONLY. WORKTREE_REPO_ROOT is the trusted base
|
|
1046
|
+
// repo; WORKTREE_ALLOWED_ROOTS (CSV) is the defence-in-depth allowlist (default = [repoRoot]);
|
|
1047
|
+
// WORKTREE_BASE_COMMIT pins the worktree ref (default core HEAD). Enabled only when both the flag and the
|
|
1048
|
+
// repo root are present — a half-set config stays OFF (no silent default repo).
|
|
596
1049
|
worktreeIsolation: boolEnv("WORKTREE_ISOLATION_ENABLED", false) && process.env.WORKTREE_REPO_ROOT
|
|
597
1050
|
? {
|
|
598
1051
|
repoRoot: process.env.WORKTREE_REPO_ROOT,
|
|
@@ -602,158 +1055,42 @@ export function loadConfig() {
|
|
|
602
1055
|
...(process.env.WORKTREE_BASE_COMMIT ? { commit: process.env.WORKTREE_BASE_COMMIT } : {}),
|
|
603
1056
|
}
|
|
604
1057
|
: undefined,
|
|
605
|
-
dbBackend,
|
|
606
|
-
dbBackendExplicit: dbBackendSet,
|
|
607
|
-
localDataRoot,
|
|
608
|
-
tidb: needsDb && dbBackend === "mysql"
|
|
609
|
-
? {
|
|
610
|
-
host: env2("MYSQL_HOST", "TIDB_HOST"),
|
|
611
|
-
port: Number(env2("MYSQL_PORT", "TIDB_PORT", process.env.MYSQL_HOST ? "3306" : "4000")),
|
|
612
|
-
user: env2("MYSQL_USER", "TIDB_USER"),
|
|
613
|
-
password: env2("MYSQL_PASSWORD", "TIDB_PASSWORD", ""),
|
|
614
|
-
database: env2("MYSQL_DATABASE", "TIDB_DATABASE"),
|
|
615
|
-
connectionLimit: process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE
|
|
616
|
-
? Number(process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE)
|
|
617
|
-
: undefined,
|
|
618
|
-
}
|
|
619
|
-
: undefined,
|
|
620
|
-
pg: needsDb && dbBackend === "pg"
|
|
621
|
-
? {
|
|
622
|
-
host: env("PG_HOST"),
|
|
623
|
-
port: Number(env("PG_PORT", "5432")),
|
|
624
|
-
user: env("PG_USER"),
|
|
625
|
-
password: env("PG_PASSWORD", ""),
|
|
626
|
-
database: env("PG_DATABASE"),
|
|
627
|
-
connectionLimit: process.env.PG_POOL_SIZE ? Number(process.env.PG_POOL_SIZE) : undefined,
|
|
628
|
-
}
|
|
629
|
-
: undefined,
|
|
630
|
-
dbQueryTimeoutMs: optFinitePositiveEnv("DB_QUERY_TIMEOUT_MS"),
|
|
631
|
-
snapshotBlobStore: process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
632
|
-
? {
|
|
633
|
-
endpoint: process.env.MINIO_ENDPOINT,
|
|
634
|
-
bucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
|
|
635
|
-
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
636
|
-
secretKey: process.env.MINIO_SECRET_KEY,
|
|
637
|
-
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
638
|
-
...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: process.env.SESSION_SNAPSHOT_PREFIX } : {}),
|
|
639
|
-
...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
|
|
640
|
-
}
|
|
641
|
-
: undefined,
|
|
642
|
-
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
643
|
-
snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
|
|
644
|
-
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
|
|
645
|
-
bindHost: process.env.BIND_HOST || process.env.HOST || undefined,
|
|
646
|
-
attachmentOrphanGraceMs: ((v) => (v !== undefined && Number.isFinite(v) && v >= 0 ? v : 3_600_000))(process.env.ATTACHMENT_ORPHAN_GRACE_MS !== undefined ? Number(process.env.ATTACHMENT_ORPHAN_GRACE_MS) : undefined),
|
|
647
|
-
workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
|
|
648
|
-
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
649
|
-
? {
|
|
650
|
-
endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
|
|
651
|
-
...(process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT
|
|
652
|
-
? { publicEndpoint: (process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT) }
|
|
653
|
-
: {}),
|
|
654
|
-
publicBucket: process.env.S3_PUBLIC_BUCKET ?? "sema-public",
|
|
655
|
-
privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
|
|
656
|
-
privateKeyPrefix: "sendfile/",
|
|
657
|
-
...(process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT ? { sandboxPutEndpoint: process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT } : {}),
|
|
658
|
-
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
659
|
-
secretKey: process.env.MINIO_SECRET_KEY,
|
|
660
|
-
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
661
|
-
defaultTtlSec: ((v) => (v !== undefined && Number.isInteger(v) && v >= 0 ? v : 0))(process.env.SEND_USER_FILE_URL_TTL !== undefined ? Number(process.env.SEND_USER_FILE_URL_TTL) : undefined),
|
|
662
|
-
}
|
|
663
|
-
: undefined,
|
|
664
|
-
authToken: process.env.SERVICE_AUTH_TOKEN,
|
|
665
|
-
authTokens,
|
|
666
|
-
allowUnauthedWrites: boolEnv("ALLOW_UNAUTHED_WRITES", false),
|
|
667
|
-
metricsToken: process.env.METRICS_TOKEN || undefined,
|
|
668
|
-
traceToken: process.env.TRACE_TOKEN || undefined,
|
|
669
|
-
corsOrigins: (process.env.CORS_ORIGIN ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
670
|
-
attachmentMaxBytes: Math.max(1024, numEnv("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024))),
|
|
671
|
-
...(process.env.ATTACHMENT_MIME_ALLOWLIST
|
|
672
|
-
? { attachmentMimeAllowlist: process.env.ATTACHMENT_MIME_ALLOWLIST.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) }
|
|
673
|
-
: {}),
|
|
674
|
-
attachmentUnboundTtlMs: Math.max(60_000, numEnv("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000))),
|
|
675
|
-
principalHeader: headerNameEnv("PRINCIPAL_HEADER", "x-agent-principal"),
|
|
676
|
-
requirePrincipal,
|
|
677
|
-
autonomy: parseAutonomy(process.env.AUTONOMY),
|
|
678
|
-
approvalRequire: csv("APPROVAL_REQUIRE"),
|
|
679
|
-
operatorPrincipals: csv("OPERATOR_PRINCIPALS"),
|
|
680
|
-
cascadeLadder: csv("MODEL_CASCADE_LADDER"),
|
|
681
|
-
approvalDeny: csv("APPROVAL_DENY"),
|
|
682
|
-
approvalPollMs: Number(env("APPROVAL_POLL_MS", "1500")),
|
|
683
|
-
durableApproval: boolEnv("DURABLE_APPROVAL", false),
|
|
684
|
-
resourceSuspend: boolEnv("RESOURCE_SUSPEND", false),
|
|
685
|
-
resourceSuspendTtlSec: Math.max(0, numEnv("RESOURCE_SUSPEND_TTL_SEC", "0")),
|
|
686
|
-
approvalAutoBudget: Math.min(10000, Math.max(0, numEnv("APPROVAL_AUTO_BUDGET", "0"))),
|
|
687
|
-
approvalNeverAuto: csv("APPROVAL_NEVER_AUTO"),
|
|
688
|
-
approvalHmacKeys: dgHmacKeys,
|
|
689
|
-
directApprovalDoor,
|
|
690
|
-
principalJwtPubkeys,
|
|
691
|
-
principalJwtIss,
|
|
692
|
-
principalJwtAud,
|
|
693
|
-
principalJwtMaxTtlSec,
|
|
694
|
-
directDoorActive,
|
|
695
|
-
infraCostRates: {
|
|
696
|
-
toolCallMicroUsd: Math.max(0, numEnv("INFRA_COST_TOOL_CALL", "0")),
|
|
697
|
-
sandboxSecMicroUsd: Math.max(0, numEnv("INFRA_COST_SANDBOX_SEC", "0")),
|
|
698
|
-
egressGbMicroUsd: Math.max(0, numEnv("INFRA_COST_EGRESS_GB", "0")),
|
|
699
|
-
},
|
|
700
1058
|
leaderEnabled: boolEnv("LEADER_ENABLED", false),
|
|
701
1059
|
leaderFanoutEnabled: boolEnv("LEADER_FANOUT_ENABLED", true),
|
|
702
|
-
routerEnabled: boolEnv("ROUTER_ENABLED", false),
|
|
703
|
-
selfOrchestrationEnabled: postureOn("SELF_ORCHESTRATION_ENABLED"),
|
|
704
|
-
forkEnabled: postureOn("FORK_ENABLED"),
|
|
705
|
-
experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false),
|
|
706
|
-
schedulerEnabled: postureOn("SCHEDULER_ENABLED"),
|
|
707
|
-
|
|
708
|
-
planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true),
|
|
1060
|
+
routerEnabled: boolEnv("ROUTER_ENABLED", false), // A value router; default OFF (ship dark, enable after live validation)
|
|
1061
|
+
selfOrchestrationEnabled: postureOn("SELF_ORCHESTRATION_ENABLED"), // posture: single-user turnkey → ON (CC local trust; sandboxed vm); multi-tenant opt-in (RCE-ish, gated)
|
|
1062
|
+
forkEnabled: postureOn("FORK_ENABLED"), // §4 CC /fork posture: single-user turnkey → default-ON for the LLM; multi-tenant honored only with a per-principal entitlement resolver (core enforces allowFork, registry-core 0.1.47; enableForkFromBody)
|
|
1063
|
+
experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false), // observer 开闸线:显式 "true" 才开(实验面,core ships dark——非 postureOn);多租户只认 center caps,env 被忽略(applyObserverEnvOptIn boot warn)
|
|
1064
|
+
schedulerEnabled: postureOn("SCHEDULER_ENABLED"), // R7 自唤醒 posture: single-user turnkey → ON (CC self-wake); multi-tenant routes to center (opt-in)
|
|
1065
|
+
planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true), // EnterPlanMode (core 1.167) model-driven plan; 🆕 default ON (resident — clay), PLAN_MODE_ENABLED=false opts out
|
|
709
1066
|
...(process.env.SCHEDULER_STORE_PATH ? { schedulerStorePath: process.env.SCHEDULER_STORE_PATH } : {}),
|
|
710
|
-
|
|
711
|
-
schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true),
|
|
1067
|
+
schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true), // [1009]② host-signal: default ON, the spawning shell opts a daemon-less engine out (PLAN_MODE_ENABLED 同形解析)
|
|
712
1068
|
selfOrchestrationModels: csv("SELF_ORCHESTRATION_MODELS"),
|
|
713
1069
|
selfOrchestrationWorkerIsolation: boolEnv("SELF_ORCHESTRATION_WORKER_ISOLATION", false),
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1070
|
+
...(workflowSizeGuideline ? { workflowSizeGuideline } : {}),
|
|
1071
|
+
workflowRunStoreBackend: enumEnv("WORKFLOW_RUN_STORE", "auto", ["auto", "file", "memory"]), // SVC-1 + P1: auto = SQL backend when present (cross-replica), else File
|
|
1072
|
+
workflowOrphanGraceMs: Math.max(60_000, numEnv("WORKFLOW_ORPHAN_GRACE_MS", String(24 * 60 * 60 * 1000))), // SVC-1: 24h default, floor 1m
|
|
1073
|
+
workflowJournalRetentionMs: Math.max(60_000, numEnv("WORKFLOW_JOURNAL_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))), // SVC-2: 7d default, floor 1m
|
|
1074
|
+
workflowRunRetentionMs: Math.max(60_000, numEnv("WORKFLOW_RUN_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))), // 30d default, floor 1m; SQL twins only
|
|
1075
|
+
backgroundAgentRetentionMs: Math.max(60_000, numEnv("BG_AGENT_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))), // core 1.364: 7d default, floor 1m
|
|
1076
|
+
backgroundAgentStaleRunningMs: Math.max(10 * 60_000, numEnv("BG_AGENT_STALE_RUNNING_MS", String(15 * 60_000))), // core 契约 ≥10min 硬钳(60s 写者心跳)
|
|
1077
|
+
backgroundAgentParkClaimStaleMs: Math.max(60_000, numEnv("BG_AGENT_PARK_CLAIM_STALE_MS", String(60 * 60_000))), // [1575] F1 修:default 1h,floor 1m(零/负值都不该让整块清算失活)
|
|
1078
|
+
rosterRetentionMs: Math.max(60_000, numEnv("ROSTER_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))), // [1522] MED2: 30d default, floor 1m; SQL twins only
|
|
1079
|
+
// 修8: scratchpad sweep TTL — 0(or negative)= disabled; enabled values floor at 1m (a sub-minute TTL would
|
|
1080
|
+
// race in-flight tasks' scratchpad writes for no operational gain).
|
|
722
1081
|
scratchpadSweepTtlMs: ((n) => (n <= 0 ? 0 : Math.max(60_000, n)))(numEnv("SCRATCHPAD_SWEEP_TTL_MS", String(7 * 24 * 60 * 60 * 1000))),
|
|
1082
|
+
// RFC B5 region package-source selector — 🆕 default `global` (clay 拍 2026-07-13 海外为准:官方源 env-first
|
|
1083
|
+
// 对存量镜像即生效);国内部署显式 SANDBOX_PKG_SOURCE=cn;`none` = 完全不注入(旧 unset 行为,字节级不变)。
|
|
723
1084
|
...(process.env.SANDBOX_PKG_SOURCE !== "none"
|
|
724
1085
|
? { sandboxPkgSource: process.env.SANDBOX_PKG_SOURCE || "global" }
|
|
725
1086
|
: {}),
|
|
726
|
-
sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true),
|
|
727
|
-
selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true),
|
|
728
|
-
envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true),
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if (trimmed === "" || trimmed.toLowerCase() === "off")
|
|
734
|
-
return [];
|
|
735
|
-
return trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
736
|
-
})(process.env.SENSITIVE_WRITE_PATTERNS),
|
|
737
|
-
...(process.env.MANUAL_MODE_SHELL_GATE === "always" || process.env.MANUAL_MODE_SHELL_GATE === "classify"
|
|
738
|
-
? { manualModeShellGate: process.env.MANUAL_MODE_SHELL_GATE }
|
|
739
|
-
: {}),
|
|
740
|
-
toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
|
|
741
|
-
lspEnabled: boolEnv("LSP_ENABLED", false),
|
|
742
|
-
lspHostEnabled,
|
|
743
|
-
drainGraceMs: Math.max(10_000, numEnv("DRAIN_GRACE_MS", String(10 * 60 * 1000))),
|
|
744
|
-
sighupIdleGraceMs: Math.max(5_000, numEnv("SIGHUP_IDLE_GRACE_MS", String(120_000))),
|
|
745
|
-
mcpElicitation: {
|
|
746
|
-
enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
|
|
747
|
-
throttle: {
|
|
748
|
-
maxConcurrentPerRun: numEnvBounded("MCP_ELICITATION_MAX_CONCURRENT", String(DEFAULT_ELICITATION_THROTTLE.maxConcurrentPerRun), 1, 64),
|
|
749
|
-
maxTotalPerRun: numEnvBounded("MCP_ELICITATION_MAX_TOTAL", String(DEFAULT_ELICITATION_THROTTLE.maxTotalPerRun), 1, 10_000),
|
|
750
|
-
minIntervalMsPerServer: numEnvBounded("MCP_ELICITATION_MIN_INTERVAL_MS", String(DEFAULT_ELICITATION_THROTTLE.minIntervalMsPerServer), 0, 600_000),
|
|
751
|
-
ttlMs: numEnvBounded("MCP_ELICITATION_TTL_MS", String(DEFAULT_ELICITATION_THROTTLE.ttlMs), 1_000, 3_600_000),
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"),
|
|
755
|
-
toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"),
|
|
756
|
-
workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false),
|
|
1087
|
+
sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true), // session auto-title: default ON (one cheap call per session)
|
|
1088
|
+
selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true), // RFC A2: default ON (mount additionally gated on k8s+catalog)
|
|
1089
|
+
envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true), // RFC A1: default ON (core 1.240.0 TaskSpec.envFacts; no-facts = no field)
|
|
1090
|
+
toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false), // [803]④ defer face: EXPERIMENTAL, default OFF
|
|
1091
|
+
lspEnabled: boolEnv("LSP_ENABLED", false), // sandbox lane: opt-in (needs a baked sema-code-lsp template)
|
|
1092
|
+
lspHostEnabled, // host lane: LSP_HOST_ENABLED, DEFAULT ON (CC-parity, degrades gracefully) — see the derivation above
|
|
1093
|
+
workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false), // [824]① TOB 保守旋钮:默认 off = workflow 子 agent 同权(CC parity)
|
|
757
1094
|
imageBakes: {
|
|
758
1095
|
enabled: boolEnv("IMAGE_BAKES_ENABLED", false),
|
|
759
1096
|
runnerPrincipal: bakeRunnerPrincipal,
|
|
@@ -765,49 +1102,80 @@ export function loadConfig() {
|
|
|
765
1102
|
submitRateMax: Math.max(0, numEnv("BAKE_SUBMIT_RATE_MAX", "10")),
|
|
766
1103
|
submitRateWindowSec: Math.max(1, numEnv("BAKE_SUBMIT_RATE_WINDOW_SEC", "60")),
|
|
767
1104
|
},
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
/** 域:limits+http(限额与 HTTP 面)—— 端口、附件/工作区体积与 TTL、成本与速率天花板、排空窗、回收周期。 */
|
|
1108
|
+
function parseLimitsHttpDomain() {
|
|
1109
|
+
return {
|
|
1110
|
+
port: Number(env("PORT", "8090")),
|
|
1111
|
+
// D-1 孤儿对象 GC grace(复审 F10;0=关掉本腿,负/坏值走默认)。
|
|
1112
|
+
attachmentOrphanGraceMs: ((v) => (v !== undefined && Number.isFinite(v) && v >= 0 ? v : 3_600_000))(process.env.ATTACHMENT_ORPHAN_GRACE_MS !== undefined ? Number(process.env.ATTACHMENT_ORPHAN_GRACE_MS) : undefined),
|
|
1113
|
+
// workspace 浏览面(#3,[1894]①)单文件读上限。
|
|
1114
|
+
workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
|
|
1115
|
+
attachmentMaxBytes: Math.max(1024, numEnv("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024))),
|
|
1116
|
+
...(process.env.ATTACHMENT_MIME_ALLOWLIST
|
|
1117
|
+
? { attachmentMimeAllowlist: process.env.ATTACHMENT_MIME_ALLOWLIST.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) }
|
|
1118
|
+
: {}),
|
|
1119
|
+
attachmentUnboundTtlMs: Math.max(60_000, numEnv("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000))),
|
|
1120
|
+
infraCostRates: {
|
|
1121
|
+
toolCallMicroUsd: Math.max(0, numEnv("INFRA_COST_TOOL_CALL", "0")),
|
|
1122
|
+
sandboxSecMicroUsd: Math.max(0, numEnv("INFRA_COST_SANDBOX_SEC", "0")),
|
|
1123
|
+
egressGbMicroUsd: Math.max(0, numEnv("INFRA_COST_EGRESS_GB", "0")),
|
|
1124
|
+
},
|
|
1125
|
+
drainGraceMs: Math.max(10_000, numEnv("DRAIN_GRACE_MS", String(10 * 60 * 1000))), // SIGTERM drain window, default 10min, floor 10s
|
|
1126
|
+
sighupIdleGraceMs: Math.max(5_000, numEnv("SIGHUP_IDLE_GRACE_MS", String(120_000))), // B3: SIGHUP idle-shutdown window, default 2min, floor 5s
|
|
1127
|
+
// BL-9: numEnv (not bare Number) — a non-numeric typo FAILS at startup instead of becoming NaN, which would
|
|
1128
|
+
// silently DISABLE the operator cost/rate ceiling (every `> ceiling` comparison against NaN is false).
|
|
771
1129
|
rateLimitPerMin: numEnv("RATE_LIMIT_RPM", "0"),
|
|
772
1130
|
maxTaskCostUsd: numEnv("MAX_TASK_COST_USD", "0"),
|
|
773
1131
|
maxTaskTokens: numEnv("MAX_TASK_TOKENS", "0"),
|
|
774
1132
|
maxPrincipalCostUsd: numEnv("MAX_PRINCIPAL_COST_USD", "0"),
|
|
775
1133
|
costQuotaWindowSec: numEnv("COST_QUOTA_WINDOW_SEC", "86400"),
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1134
|
+
reapIntervalSec: numEnv("REAP_INTERVAL_SEC", "60"),
|
|
1135
|
+
runStaleSec: numEnv("REAP_RUN_STALE_SEC", "120"),
|
|
1136
|
+
toolResultTtlSec: Number(env("TOOL_RESULT_TTL_SEC", "86400")),
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
/** 域:observability(可观测)—— 指标/追踪令牌、工具与思考轨迹开关、日志档位、OTLP 出口。 */
|
|
1140
|
+
function parseObservabilityDomain() {
|
|
1141
|
+
return {
|
|
1142
|
+
metricsToken: process.env.METRICS_TOKEN || undefined,
|
|
1143
|
+
traceToken: process.env.TRACE_TOKEN || undefined,
|
|
1144
|
+
toolTrace: boolEnv("TOOL_TRACE", false),
|
|
1145
|
+
traceThinking: boolEnv("TRACE_THINKING", true), // default ON (clay)
|
|
1146
|
+
logLevel: env("LOG_LEVEL", "info"),
|
|
787
1147
|
otel: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
788
1148
|
? {
|
|
789
1149
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, ""),
|
|
790
1150
|
intervalMs: Number(env("OTEL_EXPORT_INTERVAL_MS", "15000")),
|
|
791
1151
|
serviceName: env("OTEL_SERVICE_NAME", "sema-server"),
|
|
1152
|
+
// `OTEL_EXPORTER_OTLP_HEADERS` = comma-separated `k=v` pairs (e.g. an auth header).
|
|
792
1153
|
headers: Object.fromEntries(csv("OTEL_EXPORTER_OTLP_HEADERS").map((kv) => {
|
|
793
1154
|
const i = kv.indexOf("=");
|
|
794
1155
|
return i === -1 ? [kv, ""] : [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
|
|
795
1156
|
})),
|
|
796
1157
|
}
|
|
797
1158
|
: undefined,
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
/** 域:misc-integration(外部集成)—— plugins clone 白名单、git/OA 面、场景与 skill 目录、sema-registry 接入。 */
|
|
1162
|
+
function parseIntegrationsDomain() {
|
|
1163
|
+
return {
|
|
1164
|
+
// plugins 域 clone host 白名单(契约 [1361]①;缺省 github.com,逗号扩 https 镜像域)。
|
|
1165
|
+
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
|
|
1166
|
+
configBootFetchBudgetMs: Math.max(0, numEnv("CONFIG_BOOT_FETCH_BUDGET_MS", "1500")), // boot 首拉抢答窗;过窗转后台补齐(clay 2026-07-17:本地 5s 启动=黑洞中心同步等待)
|
|
803
1167
|
gitApiBaseUrl: process.env.GIT_API_BASEURL,
|
|
804
1168
|
gitApiToken: process.env.GIT_API_TOKEN,
|
|
1169
|
+
// BEHAVIOR CHANGE([891] clay 硬裁定):出厂缺省场景 default→code(CC 编码 persona 蒸馏版)——
|
|
1170
|
+
// 编码产品出厂态对标 CC 出厂态,通用域中性 persona 用 DEFAULT_SCENARIO=default 显式选回。
|
|
805
1171
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
806
1172
|
skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
|
|
807
1173
|
oaApiBaseUrl: process.env.OA_API_BASEURL,
|
|
808
1174
|
oaServiceToken: process.env.OA_SERVICE_TOKEN,
|
|
1175
|
+
// 开 issue 工具:默认开到 OA 系统仓(AI-Only/oa-asset-system),写权限 token 走独立 env。
|
|
809
1176
|
oaIssue: process.env.OA_ISSUE_TOKEN
|
|
810
1177
|
? {
|
|
1178
|
+
// npm 卫生纪律:不再烤内网 git 主机缺省 — 开了 OA_ISSUE_TOKEN 就必须给 host(fail-loud)。
|
|
811
1179
|
baseUrl: (() => {
|
|
812
1180
|
const u = process.env.OA_ISSUE_BASEURL ?? process.env.GIT_API_BASEURL;
|
|
813
1181
|
if (!u)
|
|
@@ -820,6 +1188,11 @@ export function loadConfig() {
|
|
|
820
1188
|
defaultLabels: csv("OA_ISSUE_LABELS").map(Number).filter((n) => Number.isInteger(n)),
|
|
821
1189
|
}
|
|
822
1190
|
: undefined,
|
|
1191
|
+
// sema-registry (control plane) URL/token/worker. The orchestrator renamed the worker-injected env from
|
|
1192
|
+
// CONFIG_CENTER_* → SEMA_REGISTRY_* (the control-plane host was renamed to match); it dual-injects
|
|
1193
|
+
// both names at the SAME value during the transition, so we DUAL-READ here — new name preferred, old name
|
|
1194
|
+
// fallback — and keep working after the orchestrator drops the legacy CONFIG_CENTER_* names. Pure config
|
|
1195
|
+
// plumbing: no API/auth/task-logic change.
|
|
823
1196
|
configCenter: (process.env.SEMA_REGISTRY_URL || process.env.CONFIG_CENTER_URL)
|
|
824
1197
|
? {
|
|
825
1198
|
baseUrl: process.env.SEMA_REGISTRY_URL || env("CONFIG_CENTER_URL"),
|
|
@@ -832,6 +1205,206 @@ export function loadConfig() {
|
|
|
832
1205
|
configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
|
|
833
1206
|
};
|
|
834
1207
|
}
|
|
1208
|
+
const STORE_GROUP_KEYS = [
|
|
1209
|
+
"sessionBackend", "sessionCacheTtlSec", "rewindSnapshotMaxMb", "dbBackend", "dbBackendExplicit", "localDataRoot",
|
|
1210
|
+
"tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "sendUserFile",
|
|
1211
|
+
];
|
|
1212
|
+
const MODEL_PLANE_GROUP_KEYS = [
|
|
1213
|
+
"gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "anthropic", "resilience", "model", "models",
|
|
1214
|
+
"modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "tiers", "projects", "roles", "cascadeLadder", "degrade",
|
|
1215
|
+
];
|
|
1216
|
+
const APPROVAL_GROUP_KEYS = [
|
|
1217
|
+
"approvalRequire", "approvalDeny", "approvalPollMs", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
1218
|
+
"approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
1219
|
+
"resourceSuspendTtlSec", "askQuestionEnabled", "toolApprovalEnabled", "mcpElicitation", "sensitiveWritePatterns",
|
|
1220
|
+
"manualModeShellGate",
|
|
1221
|
+
];
|
|
1222
|
+
const MEMORY_GROUP_KEYS = [
|
|
1223
|
+
"memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
|
|
1224
|
+
"memorySync", "projectMemoryEnabled", "syncImportLeaseStaleSec",
|
|
1225
|
+
];
|
|
1226
|
+
const AUTH_GROUP_KEYS = [
|
|
1227
|
+
"authToken", "authTokens", "allowUnauthedWrites", "corsOrigins", "principalHeader", "requirePrincipal", "autonomy",
|
|
1228
|
+
"commandPolicy", "operatorPrincipals", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud",
|
|
1229
|
+
"principalJwtMaxTtlSec", "bindHost",
|
|
1230
|
+
];
|
|
1231
|
+
const ORCHESTRATION_GROUP_KEYS = [
|
|
1232
|
+
"remoteExec", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
|
|
1233
|
+
"selfOrchestrationModels", "selfOrchestrationWorkerIsolation", "forkEnabled", "experimentalObserverAgents",
|
|
1234
|
+
"schedulerEnabled", "schedulerSessionWakeup", "schedulerStorePath", "planModeEnabled", "workflowRunStoreBackend",
|
|
1235
|
+
"workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "workflowAgentsReadOnly",
|
|
1236
|
+
"workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
|
|
1237
|
+
"backgroundAgentParkClaimStaleMs", "rosterRetentionMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
|
|
1238
|
+
"sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
|
|
1239
|
+
"imageBakes",
|
|
1240
|
+
];
|
|
1241
|
+
const LIMITS_HTTP_GROUP_KEYS = [
|
|
1242
|
+
"port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
|
|
1243
|
+
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "sighupIdleGraceMs", "rateLimitPerMin", "maxTaskCostUsd",
|
|
1244
|
+
"maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec", "reapIntervalSec", "runStaleSec", "toolResultTtlSec",
|
|
1245
|
+
];
|
|
1246
|
+
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
1247
|
+
const INTEGRATIONS_GROUP_KEYS = [
|
|
1248
|
+
"pluginsAllowHosts", "mcpServers", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "defaultScenario",
|
|
1249
|
+
"skillsDir", "oaApiBaseUrl", "oaServiceToken", "oaIssue", "configCenter", "configProvider", "configLocalDir",
|
|
1250
|
+
];
|
|
1251
|
+
/** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
|
|
1252
|
+
export const CONFIG_GROUP_KEYS = {
|
|
1253
|
+
store: STORE_GROUP_KEYS,
|
|
1254
|
+
modelPlane: MODEL_PLANE_GROUP_KEYS,
|
|
1255
|
+
approval: APPROVAL_GROUP_KEYS,
|
|
1256
|
+
memory: MEMORY_GROUP_KEYS,
|
|
1257
|
+
auth: AUTH_GROUP_KEYS,
|
|
1258
|
+
orchestration: ORCHESTRATION_GROUP_KEYS,
|
|
1259
|
+
limitsHttp: LIMITS_HTTP_GROUP_KEYS,
|
|
1260
|
+
observability: OBSERVABILITY_GROUP_KEYS,
|
|
1261
|
+
integrations: INTEGRATIONS_GROUP_KEYS,
|
|
1262
|
+
};
|
|
1263
|
+
/** 一组的**取景器**:每个键一个 accessor,读的时候才去平铺面取值。
|
|
1264
|
+
* ⚠️ 刻意不是 `{...pick(flat, keys)}` 的快照——快照会在 sema-registry 热应用(`config.model = …`、
|
|
1265
|
+
* `mutateInPlace(config.models, …)`)之后变成一份**静默过期**的旧值;取景器没有第二份存储,故不存在
|
|
1266
|
+
* 过期这个状态。`create*` 而非 `build*`:返回的是带行为(accessor)的活对象,不是纯数据。 */
|
|
1267
|
+
/** ⚠️ 语义边界(复审 2026-07-30 F4):组视图给**每个注册键**都装访问器,不看平铺面是否真有值——
|
|
1268
|
+
* `"tidb" in config.store` 恒 true、`Object.keys(config.store).length` 恒为注册表长度。本仓的
|
|
1269
|
+
* presence-as-semantics 判定(`"k" in x` / Object.keys 计数)**只能打平铺面**,组视图仅供点号读值。 */
|
|
1270
|
+
function createGroupView(flat, keys) {
|
|
1271
|
+
const view = {};
|
|
1272
|
+
for (const key of keys) {
|
|
1273
|
+
Object.defineProperty(view, key, { get: () => flat[key], enumerable: true, configurable: true });
|
|
1274
|
+
}
|
|
1275
|
+
return view;
|
|
1276
|
+
}
|
|
1277
|
+
/** 把九个组视图挂到平铺配置上(**就地**,返回的就是同一个对象——视图必须看着这一个存储处)。
|
|
1278
|
+
*
|
|
1279
|
+
* 组键是**不可枚举**的:`Object.keys(config)` / `JSON.stringify(config)` / `{...config}` 的平铺面因此
|
|
1280
|
+
* 逐字节不变(既有消费点、既有 dump/审计面零影响),代价是 spread 克隆不带组视图——克隆后要用组形,
|
|
1281
|
+
* 再过一次本函数(读一个缺席的组是当场 TypeError,不会静默给旧值)。
|
|
1282
|
+
*
|
|
1283
|
+
* `loadConfig` 是主调用点;测试夹具(test/helpers/make-config.ts)构造 `ServiceConfigFlat` 后同样过它。 */
|
|
1284
|
+
export function attachConfigGroups(flat) {
|
|
1285
|
+
const groups = {
|
|
1286
|
+
store: createGroupView(flat, STORE_GROUP_KEYS),
|
|
1287
|
+
modelPlane: createGroupView(flat, MODEL_PLANE_GROUP_KEYS),
|
|
1288
|
+
approval: createGroupView(flat, APPROVAL_GROUP_KEYS),
|
|
1289
|
+
memory: createGroupView(flat, MEMORY_GROUP_KEYS),
|
|
1290
|
+
auth: createGroupView(flat, AUTH_GROUP_KEYS),
|
|
1291
|
+
orchestration: createGroupView(flat, ORCHESTRATION_GROUP_KEYS),
|
|
1292
|
+
limitsHttp: createGroupView(flat, LIMITS_HTTP_GROUP_KEYS),
|
|
1293
|
+
observability: createGroupView(flat, OBSERVABILITY_GROUP_KEYS),
|
|
1294
|
+
integrations: createGroupView(flat, INTEGRATIONS_GROUP_KEYS),
|
|
1295
|
+
};
|
|
1296
|
+
for (const [name, view] of Object.entries(groups)) {
|
|
1297
|
+
Object.defineProperty(flat, name, { value: view, enumerable: false, writable: false, configurable: true });
|
|
1298
|
+
}
|
|
1299
|
+
return flat;
|
|
1300
|
+
}
|
|
1301
|
+
export function loadConfig() {
|
|
1302
|
+
CONFIG_WARNINGS.length = 0; // repeated loadConfig() calls (test setup) must not accumulate stale warnings
|
|
1303
|
+
CONFIG_NOTICES.length = 0;
|
|
1304
|
+
CONFIG_KNOBS.clear(); // the polarity table describes THIS load, never a previous one's env
|
|
1305
|
+
KNOB_DEPRECATIONS_SEEN.clear();
|
|
1306
|
+
// Posture gate: single-user turnkey (`REQUIRE_PRINCIPAL !== "true"` = one trusted super-admin on their own
|
|
1307
|
+
// box/tenant, CC's trust model) ⇒ the CC capability set defaults ON, each composed with its infra prereq; multi-tenant
|
|
1308
|
+
// (requirePrincipal) stays opt-in (gated + secured). This is the SAME discriminator LSP's host-default-ON uses,
|
|
1309
|
+
// generalized into one posture switch (the PM's "not per-flag" ask). In `postureOn`, an explicit `X_ENABLED=true/false`
|
|
1310
|
+
// ALWAYS wins; else the default = single-user ∧ infra-ready. 🔒 Multi-tenant (prod) is UNAFFECTED (postureOn → false).
|
|
1311
|
+
// Parsed ONCE here (design/158 B4) instead of re-comparing `process.env.REQUIRE_PRINCIPAL` at the four sites
|
|
1312
|
+
// that needed it — two of which spelled it `!== "true"` and two `=== "true"`, i.e. the same knob read in both
|
|
1313
|
+
// polarities within one function. `boolEnv(x, false)` is value-identical to the old `=== "true"`.
|
|
1314
|
+
const requirePrincipal = boolEnv("REQUIRE_PRINCIPAL", false);
|
|
1315
|
+
const singleUserTurnkey = !requirePrincipal;
|
|
1316
|
+
// `postureOn` takes the NAME (not the pre-read value) so the knob can register itself in the polarity table —
|
|
1317
|
+
// the posture family is precisely the one whose default is NOT readable from its name, so leaving it out of the
|
|
1318
|
+
// self-report would omit the worst offenders. Its tri-state semantics are unchanged: an explicit literal wins,
|
|
1319
|
+
// anything else (including a typo — deliberately NOT warned here, unlike boolEnv) falls to the posture default.
|
|
1320
|
+
const postureOn = (name, infraReady = true) => {
|
|
1321
|
+
const envVal = process.env[name];
|
|
1322
|
+
const explicit = envVal === "true" ? true : envVal === "false" ? false : undefined;
|
|
1323
|
+
const value = explicit ?? (singleUserTurnkey && infraReady);
|
|
1324
|
+
registerKnob({ name, polarity: "posture", value, source: explicit === undefined ? "posture" : "env" });
|
|
1325
|
+
return value;
|
|
1326
|
+
};
|
|
1327
|
+
// ── 九域解析(纯解析)。调用次序贴合改前的 env 读序,故 CONFIG_NOTICES 的产出顺序逐条不变。
|
|
1328
|
+
const store = parseStoreDomain({ requirePrincipal });
|
|
1329
|
+
const modelPlane = parseModelDomain();
|
|
1330
|
+
const approval = parseApprovalDomain({ postureOn });
|
|
1331
|
+
const memory = parseMemoryDomain({ requirePrincipal });
|
|
1332
|
+
const auth = parseAuthDomain({ requirePrincipal });
|
|
1333
|
+
const orchestration = parseOrchestrationDomain({ postureOn });
|
|
1334
|
+
const limitsHttp = parseLimitsHttpDomain();
|
|
1335
|
+
const observability = parseObservabilityDomain();
|
|
1336
|
+
const integrations = parseIntegrationsDomain();
|
|
1337
|
+
// ── 跨域耦合与跨域不变量:两条门(D-G 直连门、bake 门)的判据各自横跨 approval/auth/orchestration 三域,
|
|
1338
|
+
// 任何单域都不足以裁定,故留在装配层(旧名保留,断言与消息逐字不变)。
|
|
1339
|
+
const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
|
|
1340
|
+
const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
|
|
1341
|
+
const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
|
|
1342
|
+
// 🔴 the direct-door crypto gate lives ONLY in the durable (checkpointStore) /decide branch — so DURABLE_APPROVAL
|
|
1343
|
+
// is a HARD precondition. Without it, checkpointStore is undefined, that branch is skipped, and the LEGACY
|
|
1344
|
+
// approvalStore /decide (trusted-header, NO crypto) becomes the live path = a full bypass (adversarial CRITICAL).
|
|
1345
|
+
const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
|
|
1346
|
+
// 🔴 boot invariant (D-G §3): a half-configured direct door is the WORST state (looks on, verifies nothing) — and
|
|
1347
|
+
// an empty OPERATOR_PRINCIPALS makes isOperator() true-for-all (= cross-tenant escalation). Refuse to start.
|
|
1348
|
+
if (directApprovalDoor) {
|
|
1349
|
+
const missing = [];
|
|
1350
|
+
if (!dgDurable)
|
|
1351
|
+
missing.push("DURABLE_APPROVAL=true (the direct door's crypto gate only exists on the durable /decide path)");
|
|
1352
|
+
if (dgHmacKeys.length === 0)
|
|
1353
|
+
missing.push("APPROVAL_HMAC_KEYS");
|
|
1354
|
+
if (principalJwtPubkeys.length === 0)
|
|
1355
|
+
missing.push("PRINCIPAL_JWT_PUBKEYS");
|
|
1356
|
+
if (!principalJwtIss)
|
|
1357
|
+
missing.push("PRINCIPAL_JWT_ISS");
|
|
1358
|
+
if (!principalJwtAud)
|
|
1359
|
+
missing.push("PRINCIPAL_JWT_AUD");
|
|
1360
|
+
if (dgOperators.length === 0)
|
|
1361
|
+
missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
|
|
1362
|
+
// 🔴 F-fix (dim-6): a direct door is inherently MULTI-TENANT (per-task verified JWT principal). Without
|
|
1363
|
+
// REQUIRE_PRINCIPAL=true an absent/invalid principal falls through as ANONYMOUS and the owner/quota checks
|
|
1364
|
+
// degrade to the spoofable header for null-owner rows — refuse a direct door that doesn't require a principal.
|
|
1365
|
+
if (!requirePrincipal)
|
|
1366
|
+
missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the spoofable header governs owner/quota checks)");
|
|
1367
|
+
if (missing.length) {
|
|
1368
|
+
throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.4a): the bake door is build-host-RCE-capable, and isOperator([],p)
|
|
1372
|
+
// is true-for-all — so enabling bakes with an empty OPERATOR_PRINCIPALS = a WORLD-WRITABLE RCE door. Refuse to
|
|
1373
|
+
// start (never a half-open bake door). The explicit-operator gate in server.ts is the runtime twin of this.
|
|
1374
|
+
if (imageBakesEnabled && dgOperators.length === 0) {
|
|
1375
|
+
throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
|
|
1376
|
+
}
|
|
1377
|
+
// 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.12 / H1): the bake-runner identity is DERIVED from its bearer
|
|
1378
|
+
// credential (token-derived `source`), never a self-asserted header — so a SERVICE_AUTH_TOKENS entry whose
|
|
1379
|
+
// system == BAKE_RUNNER_PRINCIPAL MUST exist, else the runner can never satisfy isRunner and the entire
|
|
1380
|
+
// claim/ingest/heartbeat pipeline is silently dead. Refuse to start a bake door with no runner credential.
|
|
1381
|
+
if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
|
|
1382
|
+
throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
|
|
1383
|
+
}
|
|
1384
|
+
// 平铺面**单点展开**九域产物(唯一存储处),再挂上九个组视图。组视图是对这同一个对象的取景器,
|
|
1385
|
+
// 不是第二份拷贝 —— 故 `config.limitsHttp.port === config.port` 恒成立,含热应用之后(见
|
|
1386
|
+
// attachConfigGroups / config-types.ts 分组段)。
|
|
1387
|
+
return attachConfigGroups({
|
|
1388
|
+
...store,
|
|
1389
|
+
...modelPlane,
|
|
1390
|
+
...approval,
|
|
1391
|
+
...memory,
|
|
1392
|
+
...auth,
|
|
1393
|
+
...orchestration,
|
|
1394
|
+
...limitsHttp,
|
|
1395
|
+
...observability,
|
|
1396
|
+
...integrations,
|
|
1397
|
+
directDoorActive, // 跨域合取(approval × auth × requirePrincipal),见上
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
/** 有效监听绑址([1934])。**显式 `BIND_HOST`/`HOST` 恒生效**(operator 保留在任何形下明示暴露的
|
|
1401
|
+
* 权利);未显式时:**无鉴权写面 ⇒ 127.0.0.1**,其余 ⇒ undefined(Node 默认全接口,既有部署零影响)。
|
|
1402
|
+
*
|
|
1403
|
+
* 为什么缺省要自己收窄、而不是只加旋钮:桌面形的安全结论原本写着「safe ONLY because HOST is
|
|
1404
|
+
* pinned to 127.0.0.1」,而那个前提在本仓从未成立(HOST 零消费)——把一个没人实现的假设当防线,
|
|
1405
|
+
* 是「沉默陷阱」。零凭证 + 全接口 + host lane(用户真机非沙箱)= 同网段任意执行,缺省必须安全。
|
|
1406
|
+
* "无鉴权写面" 的判据与 http 层 503 fail-closed 门同源:allowUnauthedWrites 且**完全没有**凭证
|
|
1407
|
+
* (半配=凭证必须使用,不算无鉴权)。 */
|
|
835
1408
|
export function resolveBindHost(config) {
|
|
836
1409
|
if (config.bindHost)
|
|
837
1410
|
return config.bindHost;
|