@sema-agent/server 1.322.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +59 -3
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -1,36 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVC-3 — git-WORKTREE ISOLATION wrapper for the deployment-level `executionEnvFactory` (design/97
|
|
3
|
+
* CORE-6). A higher-order factory that mints a per-AGENT isolated git worktree when (and ONLY
|
|
4
|
+
* when) core threads `ctx.isolation === "worktree"` through the TRUSTED `RunInternals` channel.
|
|
5
|
+
*
|
|
6
|
+
* WHY this lives in service, not core: core ships the env-AGNOSTIC primitive (`addWorktree` / `pruneWorktrees`
|
|
7
|
+
* on the base `ExecutionEnv` seam, plus the `ExecutionEnvFactoryContext.isolation` hint). The env-SPECIFIC
|
|
8
|
+
* bits — which concrete adapter roots an env at the worktree dir, which repo is the operator-trusted base,
|
|
9
|
+
* where the reaper runs — are deployment wiring, so they belong here (铁律 core-service-boundary). We consume
|
|
10
|
+
* the core export; we do NOT reimplement the git plumbing.
|
|
11
|
+
*
|
|
12
|
+
* SHAPE (per the task contract):
|
|
13
|
+
* const factory = withWorktreeIsolation(baseFactory, opts);
|
|
14
|
+
* - ctx.isolation === "worktree": `addWorktree(baseEnvForGit, { repoRoot, sessionId: ctx.sessionId,
|
|
15
|
+
* rootEnvAt, commit? })` → a worktree-rooted env WITH `destroy()` (the Runner calls it on task end →
|
|
16
|
+
* `git worktree remove --force`, which tears down ONLY that worktree, never the base repo).
|
|
17
|
+
* - otherwise: `baseFactory(ctx)` verbatim — the SHARED base env, NO `destroy` (the Runner must never tear
|
|
18
|
+
* the base down). Non-isolated tasks behave exactly as before this wrapper existed.
|
|
19
|
+
*
|
|
20
|
+
* 🔴 TRUST GATE (the whole point of SVC-3): the ONLY input that turns isolation ON is core's
|
|
21
|
+
* `ctx.isolation` — populated from `ctx.agent({ isolation })` via the TRUSTED `RunInternals`, which core
|
|
22
|
+
* guarantees is NEVER sourced from the untrusted `TaskSpec` (see core remote-env.ts §isolation). This wrapper
|
|
23
|
+
* reads NOTHING else from `ctx` to decide isolation, and `repoRoot` comes ENTIRELY from the operator-supplied
|
|
24
|
+
* closure (config), never from any task/`ctx` field — so an untrusted caller can neither flip isolation on a
|
|
25
|
+
* repo it shouldn't touch nor inject a `repoRoot` path to escape the allowed roots. We additionally assert the
|
|
26
|
+
* configured `repoRoot` is contained within an operator-trusted `allowedRoots` set at wrap time (defence in
|
|
27
|
+
* depth: a mis-wired deployment fails loud rather than git-worktree-ing an arbitrary path).
|
|
28
|
+
*
|
|
29
|
+
* 🔴 ISOLATE-ONLY (clay): the runtime NEVER merges a worktree back. The orchestrator script reads each
|
|
30
|
+
* worktree's result (via the worktree dir / the snapshot) and decides verify+merge in USERLAND. We only mint
|
|
31
|
+
* and reap; merge policy is not ours.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ BOUNDARY CAVEATS (from core's CORE-6 doc — kept here so a future wirer doesn't reintroduce
|
|
34
|
+
* a footgun):
|
|
35
|
+
* - durable-suspend is INCOMPATIBLE with a worktree env: core's prepare-task rejects a NON-remote factory env
|
|
36
|
+
* for a suspendable run (a host/NodeExecutionEnv-rooted worktree is not snapshotable). So an isolated agent
|
|
37
|
+
* MUST run to completion without parking — do NOT combine `isolation:"worktree"` with a suspend/HITL park
|
|
38
|
+
* on the host lane. (On an isolated lane like e2b/k8s the worktree would have to root a REMOTE env; that is
|
|
39
|
+
* a follow-on, see `rootEnvAt` below — host lane is v1.)
|
|
40
|
+
* - `git worktree` isolates the git WORKING COPY, not arbitrary shell access: bash inside the worktree can
|
|
41
|
+
* still reach the host filesystem/network. The worktree is a coordination boundary, NOT a security sandbox
|
|
42
|
+
* — pair it with a real sandbox lane (e2b/k8s/local-docker) when isolation from the host is required.
|
|
43
|
+
* - UNCOMMITTED changes in the base repo are ABSENT in the child worktree (core creates it `--detach` at
|
|
44
|
+
* `HEAD`/`commit`). An isolated agent sees the committed tree only — commit (or stash→apply, in userland)
|
|
45
|
+
* before fanning out if the base's dirty state must be visible.
|
|
46
|
+
* - a worktree orphaned by a process CRASH (the Runner never reached `destroy`) is reaped by
|
|
47
|
+
* {@link pruneWorktrees} — wired into the service reaper (best-effort, userland; core ships no post-kill
|
|
48
|
+
* Runner hook).
|
|
49
|
+
*/
|
|
1
50
|
import path from "node:path";
|
|
2
51
|
import { addWorktree, pruneWorktrees, } from "@sema-agent/core";
|
|
52
|
+
/** Normalize a path for containment checks: resolve to absolute + drop a trailing separator. */
|
|
3
53
|
function norm(p) {
|
|
4
54
|
const r = path.resolve(p);
|
|
5
55
|
return r.length > 1 && r.endsWith(path.sep) ? r.slice(0, -1) : r;
|
|
6
56
|
}
|
|
57
|
+
/** True when `child` is `root` itself or a descendant of it (segment-aware: `/a/b` is NOT under `/a/bc`). */
|
|
7
58
|
function isWithin(root, child) {
|
|
8
59
|
const r = norm(root);
|
|
9
60
|
const c = norm(child);
|
|
10
61
|
if (c === r)
|
|
11
62
|
return true;
|
|
12
63
|
const rel = path.relative(r, c);
|
|
64
|
+
// path.relative returns "" for equal, a non-"../"-leading relative for a descendant, and a "../"-leading
|
|
65
|
+
// (or absolute, on Windows drive change) path for an outside/sibling target.
|
|
13
66
|
return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
14
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Wrap a deployment's `executionEnvFactory` so an agent with `ctx.isolation === "worktree"` runs in its own
|
|
70
|
+
* detached git worktree. See the file header for the full contract + trust gate + caveats.
|
|
71
|
+
*
|
|
72
|
+
* Throws at WRAP time (fail-loud, not faked) if `repoRoot` is not contained within `allowedRoots` — a
|
|
73
|
+
* mis-wired deployment must not silently git-worktree an arbitrary path.
|
|
74
|
+
*/
|
|
15
75
|
export function withWorktreeIsolation(baseFactory, opts) {
|
|
16
76
|
const repoRoot = norm(opts.repoRoot);
|
|
17
77
|
const allowedRoots = (opts.allowedRoots?.length ? opts.allowedRoots : [repoRoot]).map(norm);
|
|
78
|
+
// Trust gate (defence in depth): the configured repoRoot must live inside an operator-trusted root. This is
|
|
79
|
+
// a CONFIG invariant (repoRoot is never a task input), so a violation = mis-wire → fail loud at boot.
|
|
18
80
|
if (!allowedRoots.some((root) => isWithin(root, repoRoot))) {
|
|
19
81
|
throw new Error(`withWorktreeIsolation: repoRoot ${repoRoot} is not within any allowedRoots [${allowedRoots.join(", ")}] — refusing to git-worktree an untrusted path`);
|
|
20
82
|
}
|
|
83
|
+
// SERIALIZE git worktree ADMIN ops on this shared repo. `git worktree add`/`remove` mutate the repo's
|
|
84
|
+
// `.git/worktrees` registry under a repo-level lock; running them CONCURRENTLY against one repo races (a
|
|
85
|
+
// parallel fan-out finishing together → the Runner calls several `destroy()` at once → some
|
|
86
|
+
// `git worktree remove` lose the lock and leave the worktree dir+registration behind — an orphan the
|
|
87
|
+
// dir-only `pruneWorktrees` can't reap). A per-wrapper (= per-repoRoot) promise-chain mutex chains every
|
|
88
|
+
// add/remove so they apply one at a time (git ops are fast + serial-safe; the agents themselves still run
|
|
89
|
+
// fully in parallel — only the ~ms git bookkeeping serializes).
|
|
21
90
|
let gitLock = Promise.resolve();
|
|
22
91
|
const serialize = (fn) => {
|
|
23
|
-
const run = gitLock.then(fn, fn);
|
|
92
|
+
const run = gitLock.then(fn, fn); // run after the prior op SETTLES (success OR failure — never deadlock)
|
|
24
93
|
gitLock = run.then(() => undefined, () => undefined);
|
|
25
94
|
return run;
|
|
26
95
|
};
|
|
27
96
|
return async (ctx) => {
|
|
97
|
+
// TRUST GATE: the ONLY trigger is core's RunInternals-sourced ctx.isolation. We read nothing else from ctx
|
|
98
|
+
// (and never anything from a TaskSpec) to decide isolation — see file header.
|
|
28
99
|
if (ctx.isolation !== "worktree") {
|
|
100
|
+
// Non-isolated: the SHARED base env, verbatim, NO destroy attached → the Runner never tears the base
|
|
101
|
+
// down (core's hasDestroy() returns false for a plain base env). Behaviour identical to no wrapper.
|
|
29
102
|
return baseFactory(ctx);
|
|
30
103
|
}
|
|
31
104
|
const { env, worktreeDir } = await serialize(() => addWorktree(opts.baseEnvForGit, {
|
|
32
105
|
repoRoot,
|
|
33
|
-
sessionId: ctx.sessionId,
|
|
106
|
+
sessionId: ctx.sessionId, // core sanitizes this into the worktree dir name (human-readable orphan diag)
|
|
34
107
|
rootEnvAt: opts.rootEnvAt,
|
|
35
108
|
...(opts.commit ? { commit: opts.commit } : {}),
|
|
36
109
|
}));
|
|
@@ -39,28 +112,54 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
39
112
|
...(ctx.taskId ? { taskId: ctx.taskId } : {}),
|
|
40
113
|
worktreeDir,
|
|
41
114
|
});
|
|
115
|
+
// Wrap core's `destroy()` (runs `git worktree remove`) through the SAME mutex so a concurrent teardown can't
|
|
116
|
+
// race the registry lock and orphan the worktree. The other env methods (exec/fs) keep their own binding —
|
|
117
|
+
// we only override `destroy`. Idempotent at the git level (remove of an already-gone worktree is a no-op).
|
|
118
|
+
//
|
|
119
|
+
// POST-MORTEM + RETRY (2026-07-26, the deterministic pin for the 30-round reaper-race intermittent):
|
|
120
|
+
// core's destroy is FAIL-OPEN on the remove — `git-worktree-env.js` neither checks the exec Result nor lets a
|
|
121
|
+
// rejection out (`.catch(() => undefined)`), so one transient failure (spawn EAGAIN/EMFILE under full-suite
|
|
122
|
+
// load) resolves destroy() normally while leaving dir+registration behind — and `git worktree prune` can NEVER
|
|
123
|
+
// reap a worktree whose dir still exists, so the orphan is permanent. Until core checks the result (reported),
|
|
124
|
+
// we verify the dir is actually gone after each destroy, retry ONCE on residue, and FAIL LOUD if it survives
|
|
125
|
+
// both — a thrown teardown is diagnosable; a silently-accumulating orphan tree is not.
|
|
42
126
|
const coreDestroy = env.destroy.bind(env);
|
|
43
127
|
const residue = async () => {
|
|
44
128
|
const r = await opts.baseEnvForGit.exists(worktreeDir);
|
|
45
|
-
return r.ok ? r.value : false;
|
|
129
|
+
return r.ok ? r.value : false; // can't verify (base env unhealthy) → don't invent a residue; destroy stays best-effort
|
|
46
130
|
};
|
|
131
|
+
// IDEMPOTENCY (core 1.423): the remove leg is now FAIL-LOUD ([1796]§二 → [1801] — the fail-open this wrapper's
|
|
132
|
+
// post-mortem was built against is fixed), so a REPEAT destroy would hit `git worktree remove` on an
|
|
133
|
+
// already-removed worktree and reject. A completed destroy + no residue ⇒ short-circuit (the goal state is
|
|
134
|
+
// already reached; double/concurrent destroy is a legal Runner shape). The first call always goes through
|
|
135
|
+
// coreDestroy (the inner env's own teardown must run).
|
|
47
136
|
let destroyedOnce = false;
|
|
48
137
|
env.destroy = () => serialize(async () => {
|
|
49
138
|
if (destroyedOnce && !(await residue()))
|
|
50
|
-
return;
|
|
139
|
+
return; // idempotent repeat — done is done
|
|
51
140
|
await coreDestroy();
|
|
52
141
|
destroyedOnce = true;
|
|
53
142
|
if (!(await residue()))
|
|
54
143
|
return;
|
|
55
144
|
opts.logger?.warn?.("worktree_destroy_residue_retry", { worktreeDir, sessionId: ctx.sessionId });
|
|
56
|
-
await coreDestroy();
|
|
145
|
+
await coreDestroy(); // second shot on a live registration/dir — safe; a missing one is short-circuited above
|
|
57
146
|
if (await residue()) {
|
|
58
147
|
throw new Error(`worktree destroy left residue at ${worktreeDir} (git worktree remove failed twice — see core git-worktree-env fail-open)`);
|
|
59
148
|
}
|
|
60
149
|
});
|
|
61
|
-
return env;
|
|
150
|
+
return env; // carries the serialized destroy() → core's Runner removes the worktree (ONLY this one) on task end.
|
|
62
151
|
};
|
|
63
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Best-effort reaper hook: deregister worktrees orphaned by a process crash (the Runner never reached
|
|
155
|
+
* `destroy`). Core's {@link pruneWorktrees} runs `git worktree prune` against the base repo — it cleans up
|
|
156
|
+
* registrations whose worktree dirs are already gone. Wired into the service reaper (userland; core ships no
|
|
157
|
+
* post-kill Runner hook). NEVER throws (a prune failure is logged, never fatal).
|
|
158
|
+
*
|
|
159
|
+
* NOTE: `git worktree prune` only deregisters worktrees whose DIRECTORY is missing. A crashed run that left
|
|
160
|
+
* the dir intact is NOT reaped by prune alone — that is acceptable for v1 (the worktree lives under the known
|
|
161
|
+
* `.sema-worktrees/` parent and is human-/script-reapable; a stricter age-based sweep is a follow-on).
|
|
162
|
+
*/
|
|
64
163
|
export async function reapOrphanWorktrees(baseEnvForGit, repoRoot, logger) {
|
|
65
164
|
try {
|
|
66
165
|
await pruneWorktrees(baseEnvForGit, norm(repoRoot));
|
|
@@ -1,10 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-replica write-behind aggregating counter — the shared mechanism behind the cost quota (P0-1)
|
|
3
|
+
* and the rate limiter (P0-2), on BOTH dialects. Both are additive per-key counters over an aligned fixed
|
|
4
|
+
* window, so the shared store uses an ATOMIC INCREMENT (`val = val + delta`), never last-writer-wins.
|
|
5
|
+
*
|
|
6
|
+
* design/158 A12 C1b: this file used to be MySQL-hardwired and the PG lane carried TWO inline copies of the
|
|
7
|
+
* same 150-line class (pg-cost-quota.ts's private `PgWriteBehindCounter` + pg-rate-limiter.ts's exported one).
|
|
8
|
+
* They are now ONE dialect-parameterized implementation; `WriteBehindCounter` (mysql2) and
|
|
9
|
+
* `PgWriteBehindCounter` (node-pg) remain as ctor subclasses so every call site and test keeps its shape.
|
|
10
|
+
*
|
|
11
|
+
* Hot path stays sync: `addLocal` accumulates a local pending delta, `used` reads (cached fleet total
|
|
12
|
+
* from the last refresh) + (this replica's unflushed delta). A periodic `flush` pushes deltas atomically
|
|
13
|
+
* and refreshes the current window's fleet totals into the cache.
|
|
14
|
+
*
|
|
15
|
+
* Consistency is **eventual** → this is **SOFT limiting**: a replica may briefly exceed the ceiling by
|
|
16
|
+
* up to (replicas × per-flush-interval spend/requests) at the boundary before peers' totals propagate.
|
|
17
|
+
* Right for cost/fairness ceilings (and backstopped, for cost, by the hard per-task `maxCostUsd` gate).
|
|
18
|
+
* A HARD compliance ceiling would instead need a linearizable CAS/atomic count, not write-behind — same
|
|
19
|
+
* trade as the cross-replica circuit breaker. Windows are aligned fixed buckets (`floor(now/windowMs)`),
|
|
20
|
+
* the standard distributed choice (a rolling per-key window is the hard part to coordinate cross-replica).
|
|
21
|
+
*
|
|
22
|
+
* ── Dialect deltas, kept EXPLICIT (the four the PG twins' file headers used to list) ──────────────────
|
|
23
|
+
* - placeholders `?` vs `$1..$n`
|
|
24
|
+
* - atomic increment: `ON DUPLICATE KEY UPDATE val = val + VALUES(val)` vs
|
|
25
|
+
* `ON CONFLICT (<keyCol>, window_bucket) DO UPDATE SET val = <table>.val + EXCLUDED.val`
|
|
26
|
+
* - affected rows: mysql2 `ResultSetHeader.affectedRows` vs node-pg `result.rowCount`
|
|
27
|
+
* - BIGINT/INT read back: node-pg returns them as STRINGS → always `Number(...)` on read (the read path
|
|
28
|
+
* already wraps every counter cell, so this is a no-op difference at the JS boundary)
|
|
29
|
+
* - per-query timeout ARG SHAPE: mysql2 takes `{sql, timeout}` as the first query arg; node-pg takes a
|
|
30
|
+
* `QueryConfig` with `query_timeout`. That is why this file keeps its own exec instead of using
|
|
31
|
+
* sql-driver.ts (which is on the plain `(sql, params)` form).
|
|
32
|
+
*/
|
|
1
33
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
34
|
import type { Pool as PgPool } from "pg";
|
|
3
35
|
export interface CounterTable {
|
|
36
|
+
/** Table name; must have columns (<keyCol>, window_bucket BIGINT, <valCol>, updated_at). */
|
|
4
37
|
table: string;
|
|
5
38
|
keyCol: string;
|
|
6
39
|
valCol: string;
|
|
7
40
|
}
|
|
41
|
+
/** S9 (SILENT-FALLBACK P0-e): a flush/refresh failure notification. `streak` = consecutive failures of that
|
|
42
|
+
* kind (0 = recovered); `prevStreak` lets the consumer detect the recovery edge. Shared by both dialects. */
|
|
8
43
|
export interface CounterDegradeInfo {
|
|
9
44
|
table: string;
|
|
10
45
|
kind: "flush" | "refresh" | "stalled";
|
|
@@ -13,8 +48,14 @@ export interface CounterDegradeInfo {
|
|
|
13
48
|
error?: string;
|
|
14
49
|
}
|
|
15
50
|
export type CounterDegradeHook = (info: CounterDegradeInfo) => void;
|
|
51
|
+
/** S9 deeper fix: default per-query timeout for the counter family's statements. Always ON (unlike the
|
|
52
|
+
* opt-in pool-wide DB_QUERY_TIMEOUT_MS): every statement here is a tiny single-row/single-bucket op, so
|
|
53
|
+
* 30s is ~1000× headroom — a query that slow IS the S9 pathology (a DB-side hang would otherwise pin the
|
|
54
|
+
* flush round forever; the stalled watchdog can then only report, not recover). The rejection lands in
|
|
55
|
+
* the existing flush/refresh catch → the S9 streak telemetry counts it; the round ENDS and retries. */
|
|
16
56
|
export declare const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30000;
|
|
17
57
|
type CounterDialect = "tidb" | "pg";
|
|
58
|
+
/** Dual-dialect write-behind aggregating counter. See the file header for the dialect-delta ledger. */
|
|
18
59
|
export declare class SqlWriteBehindCounter {
|
|
19
60
|
private readonly dialect;
|
|
20
61
|
private readonly pool;
|
|
@@ -29,24 +70,38 @@ export declare class SqlWriteBehindCounter {
|
|
|
29
70
|
private flushing;
|
|
30
71
|
private flushStartedAt;
|
|
31
72
|
private failStreak;
|
|
32
|
-
constructor(dialect: CounterDialect, pool: MySqlPool | PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined,
|
|
73
|
+
constructor(dialect: CounterDialect, pool: MySqlPool | PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined, // S9: observability tap — never affects counting
|
|
74
|
+
queryTimeoutMs?: number);
|
|
75
|
+
/** The ONLY place the two drivers' query shapes diverge. Returns {rows, affected}. */
|
|
33
76
|
private exec;
|
|
77
|
+
/** S9: track the consecutive-failure streak per kind and notify on every failure + on the recovery edge
|
|
78
|
+
* (streak>0 → 0). Steady healthy state stays silent. The hook must never break the counter. */
|
|
34
79
|
private bump;
|
|
35
80
|
bucket(): number;
|
|
36
81
|
private rk;
|
|
82
|
+
/** Cached fleet total (peers, from the last refresh) + this replica's unflushed delta, current window. */
|
|
37
83
|
used(key: string): number;
|
|
38
84
|
addLocal(key: string, n: number): void;
|
|
85
|
+
/** Seconds until the current aligned window rolls over. */
|
|
39
86
|
retryAfterSec(): number;
|
|
87
|
+
/** Atomically push pending deltas, then refresh the current window's fleet totals + reap old buckets.
|
|
88
|
+
* Reentrancy-guarded: two overlapping flushes would each apply `+delta` from the same snapshot and
|
|
89
|
+
* double-count into the shared table (inflating fleet totals → spurious rate-limit/quota rejection). */
|
|
40
90
|
flush(): Promise<void>;
|
|
41
91
|
private flushOnce;
|
|
92
|
+
/** Load the current window's fleet totals into the cache (pendingDelta is separate → never clobbered). */
|
|
42
93
|
refresh(): Promise<void>;
|
|
94
|
+
/** Delete expired window buckets to bound the table. */
|
|
43
95
|
reap(): Promise<number>;
|
|
44
96
|
startRefresh(intervalMs?: number): this;
|
|
45
97
|
stop(): void;
|
|
46
98
|
}
|
|
99
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
47
100
|
export declare class WriteBehindCounter extends SqlWriteBehindCounter {
|
|
48
101
|
constructor(pool: MySqlPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
|
|
49
102
|
}
|
|
103
|
+
/** PostgreSQL binding — the class pg-rate-limiter.ts used to define inline (and pg-cost-quota.ts duplicated
|
|
104
|
+
* privately, hardwired to the cost_quota columns). Same ctor shape as the mysql2 twin. */
|
|
50
105
|
export declare class PgWriteBehindCounter extends SqlWriteBehindCounter {
|
|
51
106
|
constructor(pool: PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
|
|
52
107
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
const SEP = "\u0000";
|
|
1
|
+
const SEP = "\u0000"; // NUL — can't appear in a principal / scope / IP key
|
|
2
|
+
/** S9 deeper fix: default per-query timeout for the counter family's statements. Always ON (unlike the
|
|
3
|
+
* opt-in pool-wide DB_QUERY_TIMEOUT_MS): every statement here is a tiny single-row/single-bucket op, so
|
|
4
|
+
* 30s is ~1000× headroom — a query that slow IS the S9 pathology (a DB-side hang would otherwise pin the
|
|
5
|
+
* flush round forever; the stalled watchdog can then only report, not recover). The rejection lands in
|
|
6
|
+
* the existing flush/refresh catch → the S9 streak telemetry counts it; the round ENDS and retries. */
|
|
2
7
|
export const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30_000;
|
|
8
|
+
/** Dual-dialect write-behind aggregating counter. See the file header for the dialect-delta ledger. */
|
|
3
9
|
export class SqlWriteBehindCounter {
|
|
4
10
|
dialect;
|
|
5
11
|
pool;
|
|
@@ -8,13 +14,18 @@ export class SqlWriteBehindCounter {
|
|
|
8
14
|
now;
|
|
9
15
|
onDegraded;
|
|
10
16
|
queryTimeoutMs;
|
|
11
|
-
pendingDelta = new Map();
|
|
12
|
-
fleetTotal = new Map();
|
|
17
|
+
pendingDelta = new Map(); // unflushed local, by rowKey (key + window bucket)
|
|
18
|
+
fleetTotal = new Map(); // peers' current-window view, refreshed periodically
|
|
13
19
|
timer;
|
|
14
20
|
flushing = false;
|
|
15
|
-
flushStartedAt = 0;
|
|
16
|
-
failStreak = new Map();
|
|
17
|
-
constructor(dialect, pool, t, windowMs, now = () => Date.now(), onDegraded,
|
|
21
|
+
flushStartedAt = 0; // S9 review MED-1: hang-watchdog anchor // reentrancy guard: a flush slower than the timer interval must not overlap
|
|
22
|
+
failStreak = new Map(); // S9: consecutive-failure runs, per kind
|
|
23
|
+
constructor(dialect, pool, t, windowMs, now = () => Date.now(), onDegraded, // S9: observability tap — never affects counting
|
|
24
|
+
// S9 deeper fix: per-query inactivity timeout. mysql2's is the client-side `{sql, timeout}` form (the
|
|
25
|
+
// connection is held until the hung command really ends, so a poisoned connection is never reused
|
|
26
|
+
// mid-query); node-pg's is `query_timeout` on the QueryConfig. Overridable via DB_QUERY_TIMEOUT_MS
|
|
27
|
+
// (threaded by store-backend.ts); the default keeps the counter face always bounded.
|
|
28
|
+
queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
18
29
|
this.dialect = dialect;
|
|
19
30
|
this.pool = pool;
|
|
20
31
|
this.t = t;
|
|
@@ -23,6 +34,7 @@ export class SqlWriteBehindCounter {
|
|
|
23
34
|
this.onDegraded = onDegraded;
|
|
24
35
|
this.queryTimeoutMs = queryTimeoutMs;
|
|
25
36
|
}
|
|
37
|
+
/** The ONLY place the two drivers' query shapes diverge. Returns {rows, affected}. */
|
|
26
38
|
async exec(sql, params) {
|
|
27
39
|
if (this.dialect === "tidb") {
|
|
28
40
|
const [r] = (await this.pool.query({ sql, timeout: this.queryTimeoutMs }, params));
|
|
@@ -34,6 +46,8 @@ export class SqlWriteBehindCounter {
|
|
|
34
46
|
const res = await this.pool.query(q);
|
|
35
47
|
return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
|
|
36
48
|
}
|
|
49
|
+
/** S9: track the consecutive-failure streak per kind and notify on every failure + on the recovery edge
|
|
50
|
+
* (streak>0 → 0). Steady healthy state stays silent. The hook must never break the counter. */
|
|
37
51
|
bump(kind, failed, error) {
|
|
38
52
|
if (!this.onDegraded)
|
|
39
53
|
return;
|
|
@@ -46,6 +60,7 @@ export class SqlWriteBehindCounter {
|
|
|
46
60
|
this.onDegraded({ table: this.t.table, kind, streak: next, prevStreak: prev, ...(failed && error !== undefined ? { error: error instanceof Error ? error.message : String(error) } : {}) });
|
|
47
61
|
}
|
|
48
62
|
catch {
|
|
63
|
+
/* observability must never break the counter */
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
bucket() {
|
|
@@ -54,6 +69,7 @@ export class SqlWriteBehindCounter {
|
|
|
54
69
|
rk(key) {
|
|
55
70
|
return `${key}${SEP}${this.bucket()}`;
|
|
56
71
|
}
|
|
72
|
+
/** Cached fleet total (peers, from the last refresh) + this replica's unflushed delta, current window. */
|
|
57
73
|
used(key) {
|
|
58
74
|
const rk = this.rk(key);
|
|
59
75
|
return (this.fleetTotal.get(rk) ?? 0) + (this.pendingDelta.get(rk) ?? 0);
|
|
@@ -64,11 +80,20 @@ export class SqlWriteBehindCounter {
|
|
|
64
80
|
const rk = this.rk(key);
|
|
65
81
|
this.pendingDelta.set(rk, (this.pendingDelta.get(rk) ?? 0) + n);
|
|
66
82
|
}
|
|
83
|
+
/** Seconds until the current aligned window rolls over. */
|
|
67
84
|
retryAfterSec() {
|
|
68
85
|
return Math.ceil(((this.bucket() + 1) * this.windowMs - this.now()) / 1000);
|
|
69
86
|
}
|
|
87
|
+
/** Atomically push pending deltas, then refresh the current window's fleet totals + reap old buckets.
|
|
88
|
+
* Reentrancy-guarded: two overlapping flushes would each apply `+delta` from the same snapshot and
|
|
89
|
+
* double-count into the shared table (inflating fleet totals → spurious rate-limit/quota rejection). */
|
|
70
90
|
async flush() {
|
|
71
91
|
if (this.flushing) {
|
|
92
|
+
// S9 review MED-1: a hung query holds this guard: every later tick returns HERE and bump() is never
|
|
93
|
+
// reached, so the reject-path telemetry goes silent exactly when the fleet is degraded. Surface the
|
|
94
|
+
// stall itself — >60s inside one flush round = a "stalled" failure signal per tick; its streak clears
|
|
95
|
+
// when the round finally settles. (The per-query timeout above is the deeper fix — a round now self-
|
|
96
|
+
// terminates at queryTimeoutMs; this watchdog stays as the backstop for anything it doesn't cover.)
|
|
72
97
|
if (this.now() - this.flushStartedAt > 60_000)
|
|
73
98
|
this.bump("stalled", true, "flush round stalled >60s (hung query?)");
|
|
74
99
|
return;
|
|
@@ -80,17 +105,18 @@ export class SqlWriteBehindCounter {
|
|
|
80
105
|
}
|
|
81
106
|
finally {
|
|
82
107
|
this.flushing = false;
|
|
83
|
-
this.bump("stalled", false);
|
|
108
|
+
this.bump("stalled", false); // watchdog recovery edge (no-op if it never fired)
|
|
84
109
|
}
|
|
85
110
|
}
|
|
86
111
|
async flushOnce() {
|
|
87
112
|
const { table, keyCol, valCol } = this.t;
|
|
113
|
+
// The atomic-increment upsert — the ONE statement whose dialect delta actually matters here.
|
|
88
114
|
const sql = this.dialect === "tidb"
|
|
89
115
|
? `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES (?,?,?,?) ` +
|
|
90
116
|
`ON DUPLICATE KEY UPDATE ${valCol} = ${valCol} + VALUES(${valCol}), updated_at = VALUES(updated_at)`
|
|
91
117
|
: `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES ($1,$2,$3,$4) ` +
|
|
92
118
|
`ON CONFLICT (${keyCol}, window_bucket) DO UPDATE SET ${valCol} = ${table}.${valCol} + EXCLUDED.${valCol}, updated_at = EXCLUDED.updated_at`;
|
|
93
|
-
let attempted = false;
|
|
119
|
+
let attempted = false; // S9: only score a flush round that actually tried a write
|
|
94
120
|
let failure;
|
|
95
121
|
let hadFailure = false;
|
|
96
122
|
for (const [rk, delta] of [...this.pendingDelta]) {
|
|
@@ -101,7 +127,8 @@ export class SqlWriteBehindCounter {
|
|
|
101
127
|
const windowBucket = Number(rk.slice(sep + 1));
|
|
102
128
|
attempted = true;
|
|
103
129
|
try {
|
|
104
|
-
await this.exec(sql, [key, windowBucket, delta, new Date()]);
|
|
130
|
+
await this.exec(sql, [key, windowBucket, delta, new Date()]); // S9 deeper fix: a hung INSERT rejects into the catch below (streak counts it)
|
|
131
|
+
// subtract what we flushed (not delete — a concurrent addLocal may have grown it meanwhile)
|
|
105
132
|
const remaining = (this.pendingDelta.get(rk) ?? 0) - delta;
|
|
106
133
|
if (remaining > 0)
|
|
107
134
|
this.pendingDelta.set(rk, remaining);
|
|
@@ -109,15 +136,22 @@ export class SqlWriteBehindCounter {
|
|
|
109
136
|
this.pendingDelta.delete(rk);
|
|
110
137
|
}
|
|
111
138
|
catch (err) {
|
|
112
|
-
hadFailure = true;
|
|
139
|
+
hadFailure = true; // S9: surfaced via bump() below — the retry/soft-limit semantics are unchanged
|
|
113
140
|
failure = err;
|
|
141
|
+
// keep the delta; retry next flush (advisory — never throw into the caller).
|
|
142
|
+
// KNOWN/ACCEPTED (audit B): an ambiguous failure (INSERT applied but the ack was lost) re-adds
|
|
143
|
+
// the delta next flush = double-count; conversely a reader between our INSERT and a peer's sees
|
|
144
|
+
// a momentary under-count. Both are bounded by one flush window and this counter backs SOFT
|
|
145
|
+
// limits only (quota/rate-limit, design: eventual consistency; hard caps use per-task budgets).
|
|
146
|
+
// Exactly-once would need a flush ledger/CAS — not worth it at this tier.
|
|
114
147
|
}
|
|
115
148
|
}
|
|
116
149
|
if (attempted)
|
|
117
|
-
this.bump("flush", hadFailure, failure);
|
|
150
|
+
this.bump("flush", hadFailure, failure); // S9: empty rounds carry no health signal
|
|
118
151
|
await this.refresh();
|
|
119
152
|
await this.reap();
|
|
120
153
|
}
|
|
154
|
+
/** Load the current window's fleet totals into the cache (pendingDelta is separate → never clobbered). */
|
|
121
155
|
async refresh() {
|
|
122
156
|
const { table, keyCol, valCol } = this.t;
|
|
123
157
|
const b = this.bucket();
|
|
@@ -127,15 +161,17 @@ export class SqlWriteBehindCounter {
|
|
|
127
161
|
: `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = $1`, [b]);
|
|
128
162
|
const next = new Map();
|
|
129
163
|
for (const row of rows) {
|
|
130
|
-
next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol]));
|
|
164
|
+
next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol])); // node-pg returns BIGINT/INT as STRING
|
|
131
165
|
}
|
|
132
166
|
this.fleetTotal = next;
|
|
133
|
-
this.bump("refresh", false);
|
|
167
|
+
this.bump("refresh", false); // S9: recovery edge (no-op while healthy)
|
|
134
168
|
}
|
|
135
169
|
catch (err) {
|
|
136
|
-
|
|
170
|
+
// keep last-known cache on a transient read failure
|
|
171
|
+
this.bump("refresh", true, err); // S9
|
|
137
172
|
}
|
|
138
173
|
}
|
|
174
|
+
/** Delete expired window buckets to bound the table. */
|
|
139
175
|
async reap() {
|
|
140
176
|
try {
|
|
141
177
|
const { affected } = await this.exec(this.dialect === "tidb"
|
|
@@ -158,11 +194,14 @@ export class SqlWriteBehindCounter {
|
|
|
158
194
|
this.timer = undefined;
|
|
159
195
|
}
|
|
160
196
|
}
|
|
197
|
+
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
161
198
|
export class WriteBehindCounter extends SqlWriteBehindCounter {
|
|
162
199
|
constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
163
200
|
super("tidb", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
|
|
164
201
|
}
|
|
165
202
|
}
|
|
203
|
+
/** PostgreSQL binding — the class pg-rate-limiter.ts used to define inline (and pg-cost-quota.ts duplicated
|
|
204
|
+
* privately, hardwired to the cost_quota columns). Same ctor shape as the mysql2 twin. */
|
|
166
205
|
export class PgWriteBehindCounter extends SqlWriteBehindCounter {
|
|
167
206
|
constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
168
207
|
super("pg", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
|
package/dist/principal-jwt.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
2
|
+
/**
|
|
3
|
+
* design/80 D-G: verify a compact JWS principal token. Returns the crypto-verified principal (`sub`) + the
|
|
4
|
+
* channel-binding (`bnd`) on success, or `null` on ANY failure (caller fail-closes — NEVER falls back to a header).
|
|
5
|
+
* 🔴 Hardening: the JWS header `alg` MUST equal the kid's PINNED alg (rejects `alg:none`, symmetric, downgrade);
|
|
6
|
+
* the kid MUST resolve to a configured key; iss/aud/exp/nbf (with a small skew) are enforced; `sub` MUST be present.
|
|
7
|
+
*/
|
|
2
8
|
export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[], expect: {
|
|
3
9
|
iss: string;
|
|
4
10
|
aud: string;
|
|
@@ -9,11 +15,29 @@ export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[
|
|
|
9
15
|
sub: string;
|
|
10
16
|
bnd?: string;
|
|
11
17
|
} | null;
|
|
18
|
+
/** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the
|
|
19
|
+
* CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken).
|
|
20
|
+
* Binds the identity assertion to THIS specific suspended action so a captured JWT cannot be lifted onto another. */
|
|
12
21
|
export declare function approvalBnd(env: {
|
|
13
22
|
sessionId: string;
|
|
14
23
|
boundCallId?: string | null;
|
|
15
24
|
boundInputHash?: string | null;
|
|
16
25
|
}): string;
|
|
26
|
+
/**
|
|
27
|
+
* design/80 D-G: verify the WHOLE direct-door proof for one /decide — the signed principal JWT (identity) + its
|
|
28
|
+
* cnf.bnd channel-binding + the HMAC envelope (integrity) — and return the CRYPTO-VERIFIED principal, or a typed
|
|
29
|
+
* reject. 🔴 Fail-closed: every failure rejects (the caller must NOT fall back to the trusted header). Order:
|
|
30
|
+
* verify principal → bind → mac-present → mac-valid (so a non-operator never gets a MAC oracle, an unverified
|
|
31
|
+
* caller never reaches the operator list).
|
|
32
|
+
*
|
|
33
|
+
* 🔴 `opts.actionBinding`: the boundCallId/boundInputHash action-binding is `policy_ask`-
|
|
34
|
+
* SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action`
|
|
35
|
+
* (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no
|
|
36
|
+
* action), so the proof correctly DEGRADES to "verify WHO": JWT identity + cnf.bnd channel-binding + the HMAC over
|
|
37
|
+
* {sessionId, decision} (bound fields null) — WITHOUT requiring a boundInputHash. Pass `actionBinding:false` for a
|
|
38
|
+
* non-action gate; the default (true) keeps the /decide tool-approval contract byte-identical. Binding the decision
|
|
39
|
+
* to a plan HASH was REJECTED (the plan artifact lives in a profile ref store; the worker has no TOCTOU original).
|
|
40
|
+
*/
|
|
17
41
|
export declare function verifyDirectDoorProof(proof: {
|
|
18
42
|
jwt?: string;
|
|
19
43
|
mac?: string;
|
package/dist/principal-jwt.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/80 D-G: principal crypto-binding (the direct-door IDENTITY gate) + the combined direct-door proof
|
|
3
|
+
* verifier. Split out of `security.ts` (design/158 A13, internal-lossless — the facade re-exports every symbol
|
|
4
|
+
* below unchanged; `verifyDirectDoorProof` imports {@link verifyApprovalHmac} from `./approval-hmac.js` for the
|
|
5
|
+
* integrity half of the combined proof; `security.ts`'s own `verifiedPrincipal` imports {@link verifyPrincipalJwt}
|
|
6
|
+
* from here for the direct-door identity branch).
|
|
7
|
+
*
|
|
8
|
+
* ✅ WIRED (was "🔴 SCAFFOLD — NOT YET WIRED" — STALE, corrected by the 2026-07-25 doc-rot sweep). Two live call
|
|
9
|
+
* sites, both traced: `verifiedPrincipal` (security.ts) takes the direct-door branch when `directDoorActive` and
|
|
10
|
+
* derives the principal from {@link verifyPrincipalJwt} instead of the header, and {@link verifyDirectDoorProof}
|
|
11
|
+
* verifies the same JWT for the decide routes. Still true, and the part worth keeping: `directDoorActive` is
|
|
12
|
+
* computed from the anchors (config.ts) and is OFF by default, so the direct branch never runs in a stock
|
|
13
|
+
* deployment — activation = provisioning the issuer JWKS + iss/aud, a deployment task, not a coding one.
|
|
14
|
+
* The direct-connect /decide door must NEVER trust a client-supplied principal header
|
|
15
|
+
* (invariant #1). Instead the client presents a SIGNED JWT (sub = principal), and the worker verifies the SIGNATURE
|
|
16
|
+
* against the issuer's PUBLIC key — so identity is crypto-DERIVED, not header-asserted. PUBLIC keys ⇒ non-secret
|
|
17
|
+
* (ride the config projection like OPERATOR_PRINCIPALS, not the HMAC secret path). The issuer's PRIVATE key lives
|
|
18
|
+
* ONLY with the trusted minter (the BFF/identity service), never on a worker. HMAC stays integrity; this is identity.
|
|
19
|
+
*/
|
|
1
20
|
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
2
21
|
import { verifyApprovalHmac } from "./approval-hmac.js";
|
|
3
22
|
function b64urlToBuf(s) {
|
|
4
23
|
return Buffer.from(s, "base64url");
|
|
5
24
|
}
|
|
25
|
+
/** Map a PINNED JWS alg → the node:crypto verify call. EdDSA(Ed25519): algorithm=null. RS256: RSA-PKCS1+sha256.
|
|
26
|
+
* ES256: ECDSA P-256 with the JWS raw r||s encoding (dsaEncoding ieee-p1363, NOT DER). Returns false on mismatch. */
|
|
6
27
|
function verifyJwsSignature(alg, signingInput, sig, pub) {
|
|
7
28
|
try {
|
|
8
29
|
if (alg === "EdDSA")
|
|
@@ -17,6 +38,12 @@ function verifyJwsSignature(alg, signingInput, sig, pub) {
|
|
|
17
38
|
return false;
|
|
18
39
|
}
|
|
19
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* design/80 D-G: verify a compact JWS principal token. Returns the crypto-verified principal (`sub`) + the
|
|
43
|
+
* channel-binding (`bnd`) on success, or `null` on ANY failure (caller fail-closes — NEVER falls back to a header).
|
|
44
|
+
* 🔴 Hardening: the JWS header `alg` MUST equal the kid's PINNED alg (rejects `alg:none`, symmetric, downgrade);
|
|
45
|
+
* the kid MUST resolve to a configured key; iss/aud/exp/nbf (with a small skew) are enforced; `sub` MUST be present.
|
|
46
|
+
*/
|
|
20
47
|
export function verifyPrincipalJwt(token, keys, expect) {
|
|
21
48
|
if (keys.length === 0)
|
|
22
49
|
return null;
|
|
@@ -35,13 +62,15 @@ export function verifyPrincipalJwt(token, keys, expect) {
|
|
|
35
62
|
catch {
|
|
36
63
|
return null;
|
|
37
64
|
}
|
|
65
|
+
// A payload of the JSON literal `null` (or any non-object) parses WITHOUT throwing — guard before any deref so a
|
|
66
|
+
// crafted `base64url("null").<payload>.<sig>` token fail-closes to a typed reject, not an uncaught TypeError → 500.
|
|
38
67
|
if (typeof header !== "object" || header === null || typeof payload !== "object" || payload === null)
|
|
39
68
|
return null;
|
|
40
69
|
if (typeof header.alg !== "string" || typeof header.kid !== "string" || sig.length === 0)
|
|
41
70
|
return null;
|
|
42
71
|
const key = keys.find((k) => k.kid === header.kid);
|
|
43
72
|
if (!key || key.alg !== header.alg)
|
|
44
|
-
return null;
|
|
73
|
+
return null; // 🔴 alg PINNED to the kid's key — no confusion/downgrade
|
|
45
74
|
let pub;
|
|
46
75
|
try {
|
|
47
76
|
pub = createPublicKey(key.key);
|
|
@@ -52,6 +81,7 @@ export function verifyPrincipalJwt(token, keys, expect) {
|
|
|
52
81
|
const signingInput = Buffer.from(`${h}.${p}`, "utf8");
|
|
53
82
|
if (!verifyJwsSignature(key.alg, signingInput, sig, pub))
|
|
54
83
|
return null;
|
|
84
|
+
// claims — signature verified, now enforce the envelope
|
|
55
85
|
if (payload.iss !== expect.iss)
|
|
56
86
|
return null;
|
|
57
87
|
const aud = payload.aud;
|
|
@@ -62,32 +92,57 @@ export function verifyPrincipalJwt(token, keys, expect) {
|
|
|
62
92
|
return null;
|
|
63
93
|
const now = expect.nowSec ?? Math.floor(Date.now() / 1000);
|
|
64
94
|
const skew = expect.skewSec ?? 30;
|
|
95
|
+
// 🔴 exp is MANDATORY (the design mandates a short-lived token, ≤120s) — a missing/non-numeric exp must FAIL, else a
|
|
96
|
+
// token minted without exp would never expire (anti-replay = single-use checkpointToken ONLY, with no time bound).
|
|
65
97
|
if (typeof payload.exp !== "number" || payload.exp + skew < now)
|
|
66
|
-
return null;
|
|
98
|
+
return null; // missing / non-numeric / expired
|
|
67
99
|
if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || payload.nbf - skew > now))
|
|
68
|
-
return null;
|
|
100
|
+
return null; // nbf, if present, must be numeric + reached
|
|
101
|
+
// Cap the TTL so a sloppy/over-permissive minter can't widen the replay window past the short-lived mandate.
|
|
69
102
|
if (expect.maxTtlSec && payload.exp - now > expect.maxTtlSec + skew)
|
|
70
103
|
return null;
|
|
71
104
|
const bnd = typeof payload.cnf?.bnd === "string" ? payload.cnf.bnd : undefined;
|
|
72
105
|
return { sub: payload.sub, bnd };
|
|
73
106
|
}
|
|
107
|
+
/** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the
|
|
108
|
+
* CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken).
|
|
109
|
+
* Binds the identity assertion to THIS specific suspended action so a captured JWT cannot be lifted onto another. */
|
|
74
110
|
export function approvalBnd(env) {
|
|
75
111
|
return createHash("sha256").update(JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null])).digest("hex");
|
|
76
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* design/80 D-G: verify the WHOLE direct-door proof for one /decide — the signed principal JWT (identity) + its
|
|
115
|
+
* cnf.bnd channel-binding + the HMAC envelope (integrity) — and return the CRYPTO-VERIFIED principal, or a typed
|
|
116
|
+
* reject. 🔴 Fail-closed: every failure rejects (the caller must NOT fall back to the trusted header). Order:
|
|
117
|
+
* verify principal → bind → mac-present → mac-valid (so a non-operator never gets a MAC oracle, an unverified
|
|
118
|
+
* caller never reaches the operator list).
|
|
119
|
+
*
|
|
120
|
+
* 🔴 `opts.actionBinding`: the boundCallId/boundInputHash action-binding is `policy_ask`-
|
|
121
|
+
* SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action`
|
|
122
|
+
* (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no
|
|
123
|
+
* action), so the proof correctly DEGRADES to "verify WHO": JWT identity + cnf.bnd channel-binding + the HMAC over
|
|
124
|
+
* {sessionId, decision} (bound fields null) — WITHOUT requiring a boundInputHash. Pass `actionBinding:false` for a
|
|
125
|
+
* non-action gate; the default (true) keeps the /decide tool-approval contract byte-identical. Binding the decision
|
|
126
|
+
* to a plan HASH was REJECTED (the plan artifact lives in a profile ref store; the worker has no TOCTOU original).
|
|
127
|
+
*/
|
|
77
128
|
export function verifyDirectDoorProof(proof, envelope, config, opts) {
|
|
78
|
-
const actionBinding = opts?.actionBinding ?? true;
|
|
129
|
+
const actionBinding = opts?.actionBinding ?? true; // default = the policy_ask tool-approval action-binding (unchanged)
|
|
79
130
|
if (!proof.jwt)
|
|
80
131
|
return { ok: false, status: 401, errorCode: "principal_unverified", error: "direct door requires a signed principal token (X-Approval-Principal-Token)" };
|
|
81
132
|
const verified = verifyPrincipalJwt(proof.jwt, config.principalJwtPubkeys, { iss: config.principalJwtIss ?? "\0", aud: config.principalJwtAud ?? "\0", maxTtlSec: config.principalJwtMaxTtlSec });
|
|
82
133
|
if (!verified)
|
|
83
134
|
return { ok: false, status: 401, errorCode: "principal_unverified", error: "principal token failed verification" };
|
|
135
|
+
// action-binding gates (policy_ask) MUST carry the bound action; a non-action gate (plan_review) skips this — the
|
|
136
|
+
// cnf.bnd + HMAC below still bind {sessionId, decision} (with null bound fields), so WHO + decision-integrity hold.
|
|
84
137
|
if (actionBinding && !envelope.boundInputHash)
|
|
85
138
|
return { ok: false, status: 400, errorCode: "approval_mac_required", error: "direct door requires boundInputHash (the bound action) in the body" };
|
|
86
139
|
if (verified.bnd !== approvalBnd(envelope))
|
|
87
140
|
return { ok: false, status: 401, errorCode: "principal_unbound", error: "principal token is not bound to this decision (cnf.bnd mismatch)" };
|
|
88
141
|
if (!proof.mac)
|
|
89
142
|
return { ok: false, status: 401, errorCode: "approval_mac_required", error: "direct door requires an approval MAC (X-Approval-Mac)" };
|
|
90
|
-
const ok = verifyApprovalHmac(
|
|
143
|
+
const ok = verifyApprovalHmac(
|
|
144
|
+
// reason 与两个绑定位同样归一到 null(缺席=null),使"服务端算的"与"签名器签的"在缺席形上逐字节一致。
|
|
145
|
+
{ sessionId: envelope.sessionId, boundCallId: envelope.boundCallId ?? null, boundInputHash: envelope.boundInputHash ?? null, decision: envelope.decision, reason: envelope.reason ?? null }, proof.mac, proof.kid, config.approvalHmacKeys);
|
|
91
146
|
if (!ok)
|
|
92
147
|
return { ok: false, status: 401, errorCode: "approval_mac_invalid", error: "approval envelope MAC invalid" };
|
|
93
148
|
return { ok: true, principal: verified.sub };
|