@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
|
@@ -4,9 +4,13 @@ import { redactedPreview } from "../../trace/redact.js";
|
|
|
4
4
|
import { sleep } from "../sse-log.js";
|
|
5
5
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
6
6
|
import { gatedPrincipal, explicitOperatorOk, isOperator, explicitOperator } from "../principal-gate.js";
|
|
7
|
+
// design/80 seam #2 (assistant-scheduler): graceful preempt (durable yield) + resource_limit resume of one task.
|
|
7
8
|
export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
|
|
8
9
|
export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
|
|
9
10
|
export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
|
|
11
|
+
/** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
|
|
12
|
+
* load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
|
|
13
|
+
* so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
|
|
10
14
|
export function isQuestionAnswer(v) {
|
|
11
15
|
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
12
16
|
return false;
|
|
@@ -29,15 +33,28 @@ export async function handleApprovalsAssistant(req, res, url, ctx) {
|
|
|
29
33
|
await handleApprovalsAssistantBody(req, res, url, ctx, miss);
|
|
30
34
|
return !miss.fell;
|
|
31
35
|
}
|
|
36
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
37
|
+
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
32
38
|
async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
33
39
|
const { deps } = ctx;
|
|
34
40
|
const { preemptableRuns } = ctx.registry;
|
|
35
41
|
const { readJson, rateLimited, quotaExceeded } = ctx.helpers;
|
|
36
42
|
const { resumeCheckpoint, resumePreempted, resumePlanReview } = ctx.legs;
|
|
43
|
+
// Durable F4 (design/45): the approval queue over the CHECKPOINT store. Operators list pending (non-secret)
|
|
44
|
+
// and decide by the session handle → the service maps session→token INTERNALLY and resumes (token never in
|
|
45
|
+
// a URL/body/log). Takes precedence over the poll gate when a checkpointStore is wired.
|
|
37
46
|
if (deps.checkpointStore && (url.startsWith("/v1/approvals") || url.startsWith("/v1/assistant"))) {
|
|
38
47
|
const cs = deps.checkpointStore;
|
|
39
|
-
|
|
48
|
+
// 🔴 D-G §6: a direct-mode worker NEVER trusts x-agent-principal — NOT EVEN for the READ paths (else a forged
|
|
49
|
+
// operator header leaks every tenant's pending tool-call args via GET/stream, adversarial HIGH). For reads there
|
|
50
|
+
// is no per-decision checkpointToken, so identity comes from a verified principal JWT WITHOUT a cnf.bnd; an
|
|
51
|
+
// absent/invalid token ⇒ undefined principal ⇒ scope "__none__" + operator=false (fail-closed). The POST /decide
|
|
52
|
+
// path below re-verifies the SAME token WITH cnf.bnd (channel-bound to the action).
|
|
53
|
+
const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
|
|
40
54
|
const operator = isOperator(principal, deps.config.operatorPrincipals);
|
|
55
|
+
// design/80 (native push): SSE stream of pending-approval deltas so the portal SUBSCRIBES ONCE instead of
|
|
56
|
+
// polling GET /v1/approvals every ~10s. Same operator/owner scope as the list. Must precede the list/decide
|
|
57
|
+
// matches (the decide regex would otherwise treat "stream" as a sessionId on a POST — but this is GET-only).
|
|
41
58
|
if (req.method === "GET" && url === "/v1/approvals/stream") {
|
|
42
59
|
const scope = operator
|
|
43
60
|
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
|
|
@@ -47,11 +64,15 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
47
64
|
}
|
|
48
65
|
if (req.method === "GET" && url === "/v1/approvals") {
|
|
49
66
|
const scope = operator
|
|
50
|
-
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
|
|
51
|
-
: (principal ?? "__none__");
|
|
67
|
+
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined) // operator: all (or ?owner)
|
|
68
|
+
: (principal ?? "__none__"); // non-operator: only its own scope (never others' pending)
|
|
52
69
|
sendJson(res, 200, { pending: await cs.listPending(scope) });
|
|
53
70
|
return;
|
|
54
71
|
}
|
|
72
|
+
// exemptions surface — the UI's "本会话不再询问" state (list) + revoke. Same authz shape as
|
|
73
|
+
// /decide: an explicit operator may inspect any session; otherwise the caller must OWN the session's scope
|
|
74
|
+
// (peekPendingScope only knows PENDING sessions, so ownership here reads the session store's owner — the
|
|
75
|
+
// durable authority). Non-owner ⇒ 404 (no existence oracle, decide-route parity).
|
|
55
76
|
{
|
|
56
77
|
const em = /^\/v1\/approvals\/([^/]+)\/exemptions(?:\/([^/]+))?$/.exec(url);
|
|
57
78
|
if (em && (req.method === "GET" || req.method === "DELETE")) {
|
|
@@ -72,6 +93,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
72
93
|
const operators = deps.config.operatorPrincipals;
|
|
73
94
|
const isExplicitOperator = operators.length > 0 && principal !== undefined && operators.includes(principal);
|
|
74
95
|
if (!isExplicitOperator) {
|
|
96
|
+
// 🔴 fail-CLOSED owner gate (both reviewers, 2026-07-13): mirror the sibling owner-gated routes
|
|
97
|
+
// (session policy/delete) — ownerOf REQUIRED (absent ⇒ 501, never open), a store error PROPAGATES
|
|
98
|
+
// (500, never open; the old `.catch(() => undefined)` flipped a DB blip into authorization), and an
|
|
99
|
+
// UNKNOWN session (owner === undefined) 404s with no existence oracle. owner === null (ownerless
|
|
100
|
+
// anonymous/dev session) stays open — decide-route parity. No uuid shape-gate: this population is
|
|
101
|
+
// the checkpoint sessionId space (decide-route parity), not the sessions-API surface.
|
|
75
102
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
76
103
|
if (!ownerOf) {
|
|
77
104
|
sendError(res, 501, "capability.session_store_required", "approval exemptions require a session-store backend");
|
|
@@ -85,7 +112,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
85
112
|
}
|
|
86
113
|
if (req.method === "GET") {
|
|
87
114
|
if (exToolName !== undefined) {
|
|
88
|
-
sendError(res, 404, "request.route_unsupported", "not found");
|
|
115
|
+
sendError(res, 404, "request.route_unsupported", "not found"); // GET is list-only (no per-tool read)
|
|
89
116
|
return;
|
|
90
117
|
}
|
|
91
118
|
sendJson(res, 200, { exemptions: await deps.approvalExemptionStore.list(exSessionId) });
|
|
@@ -100,30 +127,67 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
100
127
|
return;
|
|
101
128
|
}
|
|
102
129
|
}
|
|
130
|
+
// design/80 #2 (assistant-scheduler, design/75 §3): the SINGLE INBOX — one prioritized view of the HITL gates
|
|
131
|
+
// across ALL of a user's N concurrent durable tasks. = listPending(scope) (already severity-sorted with the
|
|
132
|
+
// riskDescriptor) ENRICHED with each task's objective so the user knows WHICH of their tasks each gate belongs
|
|
133
|
+
// to (the missing cross-task aggregation). Scheduling priority = the severity sort; preemption is
|
|
134
|
+
// a later policy layer. Same owner-scoping as listPending (a non-operator sees only its own tasks' gates).
|
|
103
135
|
if (req.method === "GET" && url === "/v1/assistant/inbox") {
|
|
136
|
+
// SECURITY (review w16yqkkxv): the assistant inbox is a PER-USER view — ALWAYS scope to a CONCRETE owner,
|
|
137
|
+
// never a system-wide dump. A non-operator sees its own principal; an operator may inspect ONE owner via
|
|
138
|
+
// ?owner, else its own. (≠ /v1/approvals, the admin queue, which intentionally lists all.) This removes the
|
|
139
|
+
// scope=undefined / empty-?owner path that made listPending() return every tenant's pendings unbounded.
|
|
104
140
|
const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
|
|
105
141
|
const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
|
|
142
|
+
// 🔴 (BUG FIX) the inbox is the documented CheckpointSummary view (gateKind + severity SCALAR +
|
|
143
|
+
// spentMicroUsd + deadline), NOT the rich operator-queue PendingCheckpoint. It USED to spread listPending()'s
|
|
144
|
+
// raw rich rows — which carried `riskDescriptor.severity` (an object, not the scalar), the tool input + the
|
|
145
|
+
// decide binding, and NO gateKind — diverging from the contract + the tasks view (which already projects via
|
|
146
|
+
// summarizeCheckpoint). Switched to listByScope (the SAME summarizeCheckpoint projection tasks uses) so the
|
|
147
|
+
// inbox + tasks are consistent; the rich decide-binding lives on /v1/approvals (the operator queue), not here.
|
|
106
148
|
const summaries = await cs.listByScope(scope);
|
|
107
149
|
const inbox = (await Promise.all(summaries.map(async (s) => {
|
|
150
|
+
// `createdAt` + `toolInput` are now PROJECTED into CheckpointSummary by core's summarizeCheckpoint
|
|
151
|
+
// (core 1.116.0): listByScope returns them in
|
|
152
|
+
// ONE query, so the per-entry `cs.get(token)` N+1 is GONE. `objective` lives in the task body
|
|
153
|
+
// (checkpoint_ctx), not the summary, so it still needs getCtx (best-effort; missing ⇒ null).
|
|
108
154
|
const ctx = await cs.getCtx(s.sessionId).catch(() => null);
|
|
155
|
+
// 🔴 STRIP the resume `token` (the unexposed secret capability — resume is by sessionId, never the
|
|
156
|
+
// token) AND the RAW `toolInput` (core bounds but does NOT redact it — redaction is
|
|
157
|
+
// the consumer's job). Re-emit the REDACTED `input` instead.
|
|
109
158
|
const { token: _token, toolInput: _rawToolInput, ...safe } = s;
|
|
159
|
+
// `input` = a REDACTED preview of the projected (bounded, RAW) tool_input. Only `tool_approval` gates
|
|
160
|
+
// carry it (resource_limit / plan_review have none ⇒ toolInput undefined ⇒ input null). NOT the old
|
|
161
|
+
// bug (raw rich row + decide-binding); the decide-binding still lives only on /v1/approvals.
|
|
110
162
|
const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
|
|
111
163
|
return { ...safe, objective: ctx?.body?.objective ?? null, input };
|
|
112
|
-
}))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0));
|
|
113
|
-
sendJson(res, 200, { inbox });
|
|
164
|
+
}))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0)); // severity DESC (listByScope is created_at ASC; stable → oldest-first within a tier)
|
|
165
|
+
sendJson(res, 200, { inbox }); // CheckpointSummary (token-stripped) + objective, severity-prioritized — the assistant single inbox
|
|
114
166
|
return;
|
|
115
167
|
}
|
|
168
|
+
// design/80 #2 (assistant-scheduler): the N-TASKS overview = the cross-task SCHEDULING view. A user's active
|
|
169
|
+
// durable agent tasks, each joined (when suspended on a HITL gate) with the core scheduler seam
|
|
170
|
+
// listByScope(scope) → {gateKind, severity, spentMicroUsd, deadline} (core 1.105, ONE query, no N+1). The
|
|
171
|
+
// result is ordered for triage: blocked-on-a-gate first, then by risk severity DESC, then by budget spent
|
|
172
|
+
// DESC — i.e. the order a scheduler would advance/escalate them. Same owner-scoping. Needs the run-store.
|
|
116
173
|
if (req.method === "GET" && url === "/v1/assistant/tasks") {
|
|
174
|
+
// SECURITY (review w16yqkkxv): per-user view — ALWAYS a CONCRETE owner (operator inspects ONE via ?owner,
|
|
175
|
+
// else its own; non-operator = its own). No scope=undefined → no unbounded cross-tenant listPending()/listRuns.
|
|
117
176
|
const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
|
|
118
177
|
const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
|
|
119
178
|
if (!deps.runStore) {
|
|
120
179
|
sendJson(res, 200, { tasks: [] });
|
|
121
180
|
return;
|
|
122
181
|
}
|
|
182
|
+
// Rich per-gate summary via the core seam (severity + budget) — ONE scoped query, bounded (LIMIT 500).
|
|
123
183
|
const summaries = await cs.listByScope(scope);
|
|
124
184
|
const byGate = new Map(summaries.map((s) => [s.sessionId, s]));
|
|
125
185
|
const runs = await deps.runStore.listRuns({ owner: scope, limit: 100 });
|
|
126
186
|
const tasks = runs
|
|
187
|
+
// 🔴 wlzq75oac HIGH: a plan_review / dry-run review park persists as status='needs_review' (NOT 'suspended'),
|
|
188
|
+
// so a running/suspended-only filter DROPPED exactly the HITL-gated tasks §3 promises to surface "needs-
|
|
189
|
+
// attention first" — the operator could never discover the very plan_review tasks §4c says to resolve.
|
|
190
|
+
// Same class as the inbox leak (handler vs contract drift). needs_review is a non-terminal park → include it.
|
|
127
191
|
.filter((r) => r.status === "running" || r.status === "suspended" || r.status === "needs_review")
|
|
128
192
|
.map((r) => {
|
|
129
193
|
const s = byGate.get(r.sessionId);
|
|
@@ -134,18 +198,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
134
198
|
sendJson(res, 200, { tasks });
|
|
135
199
|
return;
|
|
136
200
|
}
|
|
201
|
+
// design/80 seam #2: POST /v1/assistant/tasks/:id/preempt — the scheduler's graceful "yield this task"
|
|
202
|
+
// (durable + resumable; the OPPOSITE of /v1/runs/:id/cancel's kill). Durable cross-replica preempt flag
|
|
203
|
+
// (the owning instance's heartbeat honors it) + a same-replica preempt-controller fast path. On a direct
|
|
204
|
+
// worker the caller is the crypto-VERIFIED JWT principal (above) — owner-scoped, no trusted-header bypass.
|
|
137
205
|
const preemptMatch = req.method === "POST" ? ASSISTANT_PREEMPT_RE.exec(url) : null;
|
|
138
206
|
if (preemptMatch) {
|
|
139
207
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
140
|
-
return;
|
|
208
|
+
return; // 🔴 复审 C3:preempt is NON-billable
|
|
209
|
+
// (yields a task, runs no model — see the billable-route classifier) → NO lease gate, else an exhausted
|
|
210
|
+
// tenant could not stop its own spending.
|
|
141
211
|
if (!deps.runStore) {
|
|
142
212
|
sendError(res, 501, "capability.run_store_required", "preemption requires the TiDB run store");
|
|
143
213
|
return;
|
|
144
214
|
}
|
|
215
|
+
// requirePrincipal parity with the sibling mutating endpoints (runOwnerOk / cancel / runs): 401 before the
|
|
216
|
+
// lookup when configured (uses the crypto-verified `principal` — never the trusted header on a direct door).
|
|
145
217
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
146
218
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
147
219
|
return;
|
|
148
220
|
}
|
|
221
|
+
// Precondition: a preempt only durably suspends when the worker opts tasks into resourceSuspend
|
|
222
|
+
// (RESOURCE_SUSPEND=true) AND has the durable-suspend substrate (a checkpoint store — resolveSpec wires
|
|
223
|
+
// resourceSuspend ONLY inside the durableEnabled block, so without a checkpoint store the preemptSignal is
|
|
224
|
+
// never armed) AND core deems the task eligible (remote env + durable tool-results). Reject honestly when the
|
|
225
|
+
// worker can't preempt at all (e.g. the local in-memory backend has no checkpoint store), rather than 202
|
|
226
|
+
// "preempting" a signal nothing honors.
|
|
149
227
|
if (!deps.config.resourceSuspend || !deps.checkpointStore) {
|
|
150
228
|
sendError(res, 501, "feature.preemption_disabled", "preemption not enabled on this worker (needs RESOURCE_SUSPEND=true + a durable checkpoint store)");
|
|
151
229
|
return;
|
|
@@ -156,6 +234,10 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
156
234
|
sendError(res, 404, "not_found.run", "task not found");
|
|
157
235
|
return;
|
|
158
236
|
}
|
|
237
|
+
// Owner-gate: an EXPLICIT operator may preempt any tenant's task (preemption is a scheduler action); a
|
|
238
|
+
// normal caller only its own. 404 (not 403) for a non-owner — no existence oracle (parity with runOwnerOk
|
|
239
|
+
// + the cancel/decide precedent). NOT the bare isOperator (its empty-list "true-for-all" would let any
|
|
240
|
+
// caller preempt anyone — same trap the /decide owner-gate avoids).
|
|
159
241
|
const operators = deps.config.operatorPrincipals;
|
|
160
242
|
const explicitOperator = operators.length > 0 && principal !== undefined && operators.includes(principal);
|
|
161
243
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
@@ -163,18 +245,25 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
163
245
|
return;
|
|
164
246
|
}
|
|
165
247
|
if (run.status === "running") {
|
|
248
|
+
// Honesty guard (review wtyc7ist3): if THIS replica owns the run (instance match) yet it is NOT in
|
|
249
|
+
// preemptableRuns, it is a verify/cascade leg — those durably suspend as a FAILED result, so they are
|
|
250
|
+
// deliberately never registered + never honored (runs.ts). Don't set a dead durable flag or claim
|
|
251
|
+
// "preempting"; report the honest no-op. (Owned-elsewhere → instanceId mismatch → fall through to the
|
|
252
|
+
// durable flag so the owning replica's heartbeat honors it iff that leg is eligible — best-effort.)
|
|
166
253
|
const ownedHere = run.instanceId != null && run.instanceId === deps.instanceId;
|
|
167
254
|
if (ownedHere && !preemptableRuns.has(taskId)) {
|
|
168
255
|
sendJson(res, 202, { taskId, status: run.status, note: "task is not preempt-eligible (verify/cascade leg) — preempt is a no-op" });
|
|
169
256
|
return;
|
|
170
257
|
}
|
|
171
|
-
|
|
258
|
+
// Durable flag FIRST (only flips a still-running row), abort after — if the run suspended/terminal'd in
|
|
259
|
+
// the getRun→here window the flag affects 0 rows, so re-read and answer by the ACTUAL state.
|
|
260
|
+
const flagged = await deps.runStore.requestPreempt(taskId, run.owner); // owner guard: single-DB defense-in-depth (owner-gate already enforced above)
|
|
172
261
|
if (!flagged) {
|
|
173
262
|
const now = await deps.runStore.getRun(taskId);
|
|
174
263
|
sendJson(res, 202, { taskId, status: now?.status ?? "failed", note: "task no longer running — preempt is a no-op" });
|
|
175
264
|
return;
|
|
176
265
|
}
|
|
177
|
-
preemptableRuns.get(taskId)?.abort();
|
|
266
|
+
preemptableRuns.get(taskId)?.abort(); // fast path: preempt landed on the running instance → suspend now
|
|
178
267
|
sendJson(res, 202, { taskId, status: "preempting", note: "graceful durable yield — the task suspends at the next clean turn boundary if preempt-eligible; resume via POST /v1/assistant/tasks/:id/resume" });
|
|
179
268
|
}
|
|
180
269
|
else if (run.status === "suspended") {
|
|
@@ -185,10 +274,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
185
274
|
}
|
|
186
275
|
return;
|
|
187
276
|
}
|
|
277
|
+
// design/80 seam #2: POST /v1/assistant/tasks/:id/resume — resume a task durably SUSPENDED on a
|
|
278
|
+
// resource_limit gate (a scheduler preempt OR a budget/turns/walltime slice, design/74). NOT an approval
|
|
279
|
+
// gate (resumePreempted's gate-kind guard 409s a policy_ask — that goes through POST /v1/approvals/:id/decide).
|
|
188
280
|
const resumeMatch = req.method === "POST" ? ASSISTANT_RESUME_RE.exec(url) : null;
|
|
189
281
|
if (resumeMatch) {
|
|
190
282
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
191
|
-
return;
|
|
283
|
+
return; // 🔴 复审 C2:lease admitted INSIDE driveResumeIntoRunLog on the CHECKPOINT-OWNER principal (the billed tenant), not the request principal — a cross-tenant operator resume must charge the owner's lease, not the operator's. resume hits TiDB + runs the model
|
|
192
284
|
if (!deps.runStore) {
|
|
193
285
|
sendError(res, 501, "capability.run_store_required", "resume requires the TiDB run store");
|
|
194
286
|
return;
|
|
@@ -196,7 +288,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
196
288
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
197
289
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
198
290
|
return;
|
|
199
|
-
}
|
|
291
|
+
} // parity with sibling mutating endpoints
|
|
200
292
|
const taskId = resumeMatch[1];
|
|
201
293
|
const run = await deps.runStore.getRun(taskId);
|
|
202
294
|
if (!run) {
|
|
@@ -209,14 +301,23 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
209
301
|
sendError(res, 404, "not_found.run", "task not found");
|
|
210
302
|
return;
|
|
211
303
|
}
|
|
212
|
-
const out = await resumePreempted(run.sessionId, req);
|
|
304
|
+
const out = await resumePreempted(run.sessionId, req); // gate-kind guard + drive into the run log (shared)
|
|
213
305
|
sendJson(res, out.status, out.body);
|
|
214
306
|
return;
|
|
215
307
|
}
|
|
308
|
+
// design/80 D-B: POST /v1/assistant/tasks/:id/plan_review — resolve a PRE-ACTION `plan_review` gate, 3-state
|
|
309
|
+
// (approve | edit | reject). A SIBLING of preempt/resume (a scheduler-resolvable gate with NO bound tool
|
|
310
|
+
// action — the human reviews the PLAN, not a tool call), NOT the /decide wire (a tool-approval allow/deny +
|
|
311
|
+
// action binding) and NOT the needs_review wire (never reuse it). `edit` carries the
|
|
312
|
+
// operator's REVISED plan (core threads `editedPlan` into the resumed run). 🔴 D-G ruling:
|
|
313
|
+
// a plan_review binds NO action, so on a direct-door worker the self-asserted x-agent-principal is NEVER
|
|
314
|
+
// trusted — it carries the PRINCIPAL-ONLY proof (verifyDirectDoorProof actionBinding:false = JWT identity +
|
|
315
|
+
// cnf.bnd + HMAC over {sessionId,decision}, no boundInputHash), then the SAME operator-or-owner gate on the
|
|
316
|
+
// CRYPTO-VERIFIED principal. On a BFF worker the trusted-header principal owner-gate applies (parity with resume).
|
|
216
317
|
const planReviewMatch = req.method === "POST" ? ASSISTANT_PLAN_REVIEW_RE.exec(url) : null;
|
|
217
318
|
if (planReviewMatch) {
|
|
218
319
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
219
|
-
return;
|
|
320
|
+
return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB + runs the model (approve/edit)
|
|
220
321
|
if (!deps.runStore) {
|
|
221
322
|
sendError(res, 501, "capability.run_store_required", "plan_review requires the TiDB run store");
|
|
222
323
|
return;
|
|
@@ -224,8 +325,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
224
325
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
225
326
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
226
327
|
return;
|
|
227
|
-
}
|
|
328
|
+
} // parity with sibling mutating endpoints
|
|
228
329
|
const taskId = planReviewMatch[1];
|
|
330
|
+
// 3-state decision; `edit` MUST carry the revised plan, `approve`/`reject` must NOT (a stray editedPlan
|
|
331
|
+
// signals a wrong-gate call — reject, don't drop it). Parsed + validated BEFORE the task is resolved so
|
|
332
|
+
// every bad-body 400 is existence-INDEPENDENT (depends only on the body). 🔴 wq64gmm5e LOW: getRun's 404
|
|
333
|
+
// used to fire BEFORE the body/proof — a task-existence oracle (404-vs-401) on a direct-door worker,
|
|
334
|
+
// divergent from /decide. Now the existence branch comes AFTER and is status-uniform with a bad proof.
|
|
229
335
|
const body = (await readJson(req));
|
|
230
336
|
const decision = body?.decision === "approve" ? "approve" : body?.decision === "edit" ? "edit" : body?.decision === "reject" ? "reject" : undefined;
|
|
231
337
|
if (!decision) {
|
|
@@ -244,12 +350,16 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
244
350
|
sendError(res, 400, "request.field_invalid", "reason must be a string");
|
|
245
351
|
return;
|
|
246
352
|
}
|
|
353
|
+
// 与 /decide 腿同一上限:reason 入签之后,超长只能**拒**(截断产出的字节签名器复现不了 ⇒ 恒 401)。
|
|
247
354
|
if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
|
|
248
355
|
sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
|
|
249
356
|
return;
|
|
250
357
|
}
|
|
251
358
|
const run = await deps.runStore.getRun(taskId);
|
|
252
359
|
if (!run) {
|
|
360
|
+
// No such task. On a DIRECT-DOOR worker return the SAME 401 a bad proof gets (parity with /decide): a
|
|
361
|
+
// missing task is indistinguishable from a bad proof — the caller cannot mint a valid proof for an
|
|
362
|
+
// unknown sessionId anyway — so existence never leaks. On a BFF worker the trusted-header owner posture 404s.
|
|
253
363
|
if (deps.config.directDoorActive) {
|
|
254
364
|
sendError(res, 401, "principal_unverified", "principal proof required");
|
|
255
365
|
return;
|
|
@@ -257,10 +367,15 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
257
367
|
sendError(res, 404, "not_found.run", "task not found");
|
|
258
368
|
return;
|
|
259
369
|
}
|
|
370
|
+
// ── D-G principal-only proof (non-action gate): on a direct-door worker derive the CRYPTO-VERIFIED principal
|
|
371
|
+
// from the signed proof, never the self-asserted header. The envelope binds {sessionId, decision} so a forged
|
|
372
|
+
// or replayed plan decision is rejected; no boundInputHash (there is no tool action). fail-closed.
|
|
260
373
|
let deciderPrincipal = principal;
|
|
261
374
|
if (deps.config.directDoorActive) {
|
|
262
375
|
const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
|
|
263
|
-
const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") },
|
|
376
|
+
const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") },
|
|
377
|
+
// reason 入签:同一条规则也适用于身份绑定腿(两个绑定位为 null,第五位照常参与)。
|
|
378
|
+
{ sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
|
|
264
379
|
if (!proof.ok) {
|
|
265
380
|
sendError(res, proof.status, proof.errorCode, proof.error);
|
|
266
381
|
return;
|
|
@@ -272,7 +387,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
272
387
|
if (!explicitOperator && run.owner !== null && run.owner !== deciderPrincipal) {
|
|
273
388
|
sendError(res, 404, "not_found.run", "task not found");
|
|
274
389
|
return;
|
|
275
|
-
}
|
|
390
|
+
} // no existence oracle (verified principal on a direct worker)
|
|
276
391
|
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req);
|
|
277
392
|
sendJson(res, out.status, out.body);
|
|
278
393
|
return;
|
|
@@ -280,10 +395,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
280
395
|
const m = /^\/v1\/approvals\/([^/]+?)(?:\/decide)?$/.exec(url);
|
|
281
396
|
if (m && req.method === "POST") {
|
|
282
397
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
283
|
-
return;
|
|
398
|
+
return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB — rate-limit (council)
|
|
399
|
+
// NOTE: the operator gate is checked AFTER the body parse below — D-G's direct door needs the parsed
|
|
400
|
+
// envelope (checkpointToken + decision) to verify the HMAC before it can derive the deciding principal.
|
|
284
401
|
let sessionId;
|
|
285
402
|
try {
|
|
286
|
-
sessionId = decodeURIComponent(m[1]);
|
|
403
|
+
sessionId = decodeURIComponent(m[1]); // a malformed %-sequence is a client error (400), not a 500
|
|
287
404
|
}
|
|
288
405
|
catch {
|
|
289
406
|
sendError(res, 400, "request.path_malformed", "malformed approval id (invalid percent-encoding)");
|
|
@@ -295,6 +412,11 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
295
412
|
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember? }");
|
|
296
413
|
return;
|
|
297
414
|
}
|
|
415
|
+
// `reason` 现在**在签名载荷里**(三方裁 (a),见 security.ts `approvalHmacMessage` 顶注)⇒ 它的形必须先钉死:
|
|
416
|
+
// · 非字符串会让规范化编码产出一个签名器不可能复现的消息(对象会被内联进 JSON)—— 此前这条腿
|
|
417
|
+
// 根本没校验 reason 的类型(plan_review 腿有),入签之后这就成了硬前置。
|
|
418
|
+
// · 超长**拒而不截**:服务端截过的字节与签名方签的字节必然不同 ⇒ MAC 恒不匹配,截断只会把一个
|
|
419
|
+
// 可诊断的 413 变成费解的 401(与 steer 入参上限同一判据:诚实拒绝优于自作主张改写)。
|
|
298
420
|
if (body.reason !== undefined && typeof body.reason !== "string") {
|
|
299
421
|
sendError(res, 400, "request.field_invalid", "reason must be a string");
|
|
300
422
|
return;
|
|
@@ -303,6 +425,11 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
303
425
|
sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
|
|
304
426
|
return;
|
|
305
427
|
}
|
|
428
|
+
// `remember:"session"` — record a per-session per-toolName approval exemption alongside an
|
|
429
|
+
// approve ("本会话不再询问"). Closed enum (extensible later, e.g. "principal"); deny+remember = 400 (a
|
|
430
|
+
// "remembered deny" is a different semantic — not smuggled in here); direct-door v1 = 400 (remember is
|
|
431
|
+
// NOT in the HMAC envelope's signed payload — accepting it unsigned would be a tamper hole; extending
|
|
432
|
+
// the MAC payload is a v2 with the BFF, see docs/design/APPROVAL-SESSION-EXEMPTION.md).
|
|
306
433
|
let remember = false;
|
|
307
434
|
if (body.remember !== undefined) {
|
|
308
435
|
if (body.remember !== "session") {
|
|
@@ -321,12 +448,20 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
321
448
|
sendError(res, 501, "capability.store_required", "approval exemptions need a store backend (DB_BACKEND / LOCAL lane)");
|
|
322
449
|
return;
|
|
323
450
|
}
|
|
451
|
+
// 🔴 remember REQUIRES the D-1 decision-binding echo (workflow audit 2026-07-13, my override of the
|
|
452
|
+
// verify-reject): an unbound decide can consume a gate that was SWAPPED after the operator's GET (see
|
|
453
|
+
// gate A, approve lands on gate B) — tolerable as a one-shot legacy quirk, NOT as the mint of a
|
|
454
|
+
// SESSION-PERSISTENT auto-approval for a tool the operator never saw. remember is a NEW contract, so
|
|
455
|
+
// it can demand the strict calling shape (the BFF already receives boundCallId/checkpointToken on the
|
|
456
|
+
// pending row). Plain decide without remember keeps the legacy unbound fallback untouched.
|
|
324
457
|
if (!body.checkpointToken && !body.boundCallId) {
|
|
325
458
|
sendError(res, 400, "remember_requires_binding", "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)");
|
|
326
459
|
return;
|
|
327
460
|
}
|
|
328
461
|
remember = true;
|
|
329
462
|
}
|
|
463
|
+
// Durable ask (TC-5.4): the operator's out-of-band QuestionAnswer rides the approve. Trusted
|
|
464
|
+
// control-plane input (the model never produces it); malformed = client error, absent = unchanged.
|
|
330
465
|
let answer;
|
|
331
466
|
if (body.answer !== undefined) {
|
|
332
467
|
if (!isQuestionAnswer(body.answer)) {
|
|
@@ -335,12 +470,27 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
335
470
|
}
|
|
336
471
|
answer = body.answer;
|
|
337
472
|
}
|
|
473
|
+
// D-1 (design/80) decision-action binding: the operator echoes back the checkpointToken + boundCallId +
|
|
474
|
+
// boundInputHash they SAW in GET /v1/approvals. The service binds the resume to that exact action (core
|
|
475
|
+
// verifies opaque equality, fail-closed PRE-CAS) and rejects a stale view distinctly (approval_stale).
|
|
476
|
+
// All OPTIONAL — a legacy BFF caller that omits them falls back to the current-pending values (deprecated).
|
|
338
477
|
for (const f of ["checkpointToken", "boundCallId", "boundInputHash"]) {
|
|
339
478
|
if (body[f] !== undefined && typeof body[f] !== "string") {
|
|
340
479
|
sendError(res, 400, "request.field_invalid", `${f} must be a string (the value surfaced on the pending approval record)`);
|
|
341
480
|
return;
|
|
342
481
|
}
|
|
343
482
|
}
|
|
483
|
+
// 🔴 直连门上拒 `updatedInput`(2026-07-26 修),判据与上面拒 `remember` **逐字同一条**:
|
|
484
|
+
// 「不在决策证明的签名载荷里」。此前只 `remember` 有这条门,而 `updatedInput` 改写的是
|
|
485
|
+
// **将被执行的工具实参**(core `runtask.js` 在哈希比对**之后**用 `outcome.updatedInput ?? pendingAction.args`)
|
|
486
|
+
// ⇒ 同一份 JWT+MAC 对「原 body」与「加了 updatedInput 的 body」**都放行**,而 `boundInputHash` 仍是
|
|
487
|
+
// 原动作的哈希。直连门的威胁模型就是「线上字节不可信」(否则不需要 HMAC),能改字节的一方
|
|
488
|
+
// (反代 / 被劫持的客户端)可在操作员批准 `ls` 的同一次决定里换掉实参。
|
|
489
|
+
// ⚠️ 已亲验的缓解:core 会把改写后的实参**重新过一遍 base tool policy**,所以越权面被限制在策略仍允许的
|
|
490
|
+
// 范围内、不是任意命令 —— 但那是"减轻",不是"这个字段被签名覆盖了"。
|
|
491
|
+
// 修法刻意**不是**把它塞进 HMAC 载荷(那要求所有直连门客户端改签名 = 跨仓 wire 变更),而是把**既有的、
|
|
492
|
+
// 代码自己写下的**判据施加到它漏掉的那个字段上:fail-closed、server 单方面可做、与 `remember` 对称。
|
|
493
|
+
// BFF/gated 门(头已由上游验过)**不受影响** —— 这条只在 `directDoorActive` 生效。
|
|
344
494
|
if (deps.config.directDoorActive && body.updatedInput !== undefined) {
|
|
345
495
|
sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
|
|
346
496
|
return;
|
|
@@ -351,38 +501,69 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
351
501
|
boundInputHash: body.boundInputHash,
|
|
352
502
|
updatedInput: body.updatedInput,
|
|
353
503
|
};
|
|
504
|
+
// ── design/80 D-G: the authorization gate. On a DIRECT-MODE worker the trusted-header path is DISABLED —
|
|
505
|
+
// every /decide MUST carry the crypto proof (signed principal JWT + cnf.bnd + HMAC envelope), and the
|
|
506
|
+
// self-asserted x-agent-principal is NEVER consulted. On a normal (BFF) worker, the existing trusted-header
|
|
507
|
+
// operator gate is byte-identical. Fail-closed: a direct-door failure rejects, never falls back to the header.
|
|
354
508
|
let deciderPrincipal = principal;
|
|
355
509
|
if (deps.config.directDoorActive) {
|
|
356
510
|
const hdr = (n) => {
|
|
357
511
|
const h = req.headers[n];
|
|
358
512
|
return Array.isArray(h) ? h[0] : h;
|
|
359
513
|
};
|
|
360
|
-
const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") },
|
|
514
|
+
const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") },
|
|
515
|
+
// reason 入签(三方裁 (a) + clay 干净切):落账理由属于决定内容,与 decision 同受完整性保护。
|
|
516
|
+
{ sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
|
|
361
517
|
if (!proof.ok) {
|
|
362
518
|
sendError(res, proof.status, proof.errorCode, proof.error);
|
|
363
519
|
return;
|
|
364
520
|
}
|
|
365
|
-
deciderPrincipal = proof.principal;
|
|
521
|
+
deciderPrincipal = proof.principal; // the CRYPTO-VERIFIED principal replaces the self-asserted header
|
|
366
522
|
}
|
|
523
|
+
// Authorization (council finding): an EXPLICIT operator may decide ANY approval; otherwise the
|
|
524
|
+
// caller must OWN the checkpoint's scope. 🔴 NOT the bare isOperator — its empty-OPERATOR_PRINCIPALS
|
|
525
|
+
// "true-for-all" would let ANY authenticated caller decide ANOTHER user's checkpoint via a leaked, NON-
|
|
526
|
+
// credential sessionId+boundInputHash (from a log/screenshot/ticket). A non-owner-non-operator gets 404
|
|
527
|
+
// (no existence oracle — parity with runOwnerOk + the cancel P0 precedent), never 403.
|
|
367
528
|
const operators = deps.config.operatorPrincipals;
|
|
368
529
|
const explicitOperator = operators.length > 0 && deciderPrincipal !== undefined && operators.includes(deciderPrincipal);
|
|
369
530
|
if (!explicitOperator) {
|
|
370
531
|
const cpScope = await cs.peekPendingScope(sessionId);
|
|
371
|
-
|
|
532
|
+
// "_" = the anonymous-submit scope SENTINEL (main.ts writes auth?.principal ?? "_" at suspend) — treat it
|
|
533
|
+
// like null (open), as the sibling sites already do (the 5040/5599/5653 normalizations). Without this an
|
|
534
|
+
// anonymous single-user durable worker DEADLOCKS its own approvals (decide → 404 forever; workflow audit
|
|
535
|
+
// 2026-07-13, confirmed by both verify lenses).
|
|
536
|
+
const ownsIt = cpScope == null || cpScope === "_" || cpScope === deciderPrincipal; // null/"_" = anonymous/dev (open)
|
|
372
537
|
if (!ownsIt) {
|
|
373
538
|
sendError(res, 404, "not_found.approval", "approval not found");
|
|
374
539
|
return;
|
|
375
540
|
}
|
|
376
541
|
}
|
|
542
|
+
// remember: capture the pending gate's toolName BEFORE the resume consumes the row. AskUserQuestion
|
|
543
|
+
// gates are excluded (the durable-question policy owns them — an exemption row would be dead weight the
|
|
544
|
+
// ask-gate probe explicitly skips). null toolName (legacy row) ⇒ nothing to remember.
|
|
377
545
|
let rememberToolName = null;
|
|
378
546
|
if (remember) {
|
|
547
|
+
// [1.202 live 复验抓获] 旧姿势 `listPending(deciderPrincipal ?? "__none__")` 在匿名单用户 lane
|
|
548
|
+
// (decider 无 principal、checkpoint scope="_" 哨兵)永远滤空 ⇒ remember 结构性失灵(rememberApplied
|
|
549
|
+
// 恒 false,豁免从不落店)。授权已由上方 owner gate 把过(cpScope null/"_"/相符,同 5040/5599/5653
|
|
550
|
+
// 的 "_" 归一先例)——这里直接读该 session 的 pending 行拿 toolName,不再按 decider 二次过滤。
|
|
379
551
|
const tok = await cs.findPendingTokenBySession(sessionId);
|
|
380
552
|
const cp0 = tok ? await cs.get(tok) : null;
|
|
381
553
|
const pa0 = cp0?.pendingAction;
|
|
382
554
|
const tn = pa0 && pa0.kind === "tool_approval" ? pa0.toolName : null;
|
|
383
555
|
rememberToolName = tn && tn !== "AskUserQuestion" ? tn : null;
|
|
384
556
|
}
|
|
557
|
+
// grant timing(codex M2,1.294 isExempt 同腿生效):grant 挪进 onResumeCommitted——markResuming CAS
|
|
558
|
+
// 赢了(decide 已被接受、并发 sibling 已输)之后、模型腿驱动**之前**落店,所以 resumed leg 自己的下一个
|
|
559
|
+
// ask 就能命中豁免(gate isExempt + ask 政策层探针都实时读店);旧姿势 await 整条 resume 腿(可跑数分钟)
|
|
560
|
+
// 之后才 grant,同腿第二次同工具 ask 必再 park 一轮。输掉 CAS(409)/stale(404)⇒ 钩不触发,绝不留
|
|
561
|
+
// 豁免;CAS 赢后模型腿再失败 ⇒ 豁免保留(operator 的 approve+remember 意图在 accept 时已成立,与
|
|
562
|
+
// 「grant 失败不回滚 approve」同向)。grant 失败只 warn 不阻断 resume,响应带 rememberApplied 如实。
|
|
385
563
|
let rememberApplied = false;
|
|
564
|
+
// overrideSessionId([1591] 候裁② server 修):parked 赎回腿把 grant 锚改到行的 root/host 会话
|
|
565
|
+
// (部署侧豁免探针闭包统一捕获 host 会话键——落子代会话则键永不相交,rememberApplied 会是假话)。
|
|
566
|
+
// legacy 任务腿不传 override,行为逐字不变。
|
|
386
567
|
const grantOnCommit = remember && deps.approvalExemptionStore && rememberToolName
|
|
387
568
|
? async (overrideSessionId) => {
|
|
388
569
|
const grantSessionId = overrideSessionId ?? sessionId;
|
|
@@ -408,8 +589,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
408
589
|
sendError(res, 404, "not_found.route", "not found");
|
|
409
590
|
return;
|
|
410
591
|
}
|
|
592
|
+
// Human-in-the-loop approval gate (F4). Operators list/decide; the running task polls the row.
|
|
411
593
|
if (deps.approvalStore && url.startsWith("/v1/approvals")) {
|
|
412
594
|
const store = deps.approvalStore;
|
|
595
|
+
// 🔴 D-G belt (defense-in-depth): this LEGACY block authorizes /approvals via the trusted x-agent-principal
|
|
596
|
+
// header (no crypto). On a direct-door worker that path must NEVER be live — the boot invariant already forces
|
|
597
|
+
// DURABLE_APPROVAL (so checkpointStore owns /approvals above and we never reach here), but if a direct-door
|
|
598
|
+
// worker ever fell through to here, fail closed rather than expose the header-trusting path.
|
|
413
599
|
if (deps.config.directApprovalDoor) {
|
|
414
600
|
sendError(res, 503, "approval_door_unconfigured", "approvals are served by the direct door on this worker");
|
|
415
601
|
return;
|
|
@@ -417,10 +603,14 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
417
603
|
if (req.method === "GET" && url === "/v1/approvals") {
|
|
418
604
|
const principal = principalFrom(req, deps.config);
|
|
419
605
|
if (isOperator(principal, deps.config.operatorPrincipals)) {
|
|
606
|
+
// operator: may filter by any ?owner (or list all). scope is owner-sourced, so the operator's ?owner
|
|
607
|
+
// filter IS the scope filter; an unfiltered operator list is cross-tenant BY DESIGN (operator queue).
|
|
420
608
|
const owner = new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined;
|
|
421
609
|
sendJson(res, 200, { pending: owner ? await store.listPending(owner, owner) : await store.listPendingAll() });
|
|
422
610
|
}
|
|
423
611
|
else {
|
|
612
|
+
// non-operator: only ITS OWN pending (ignore any ?owner spoof) — never others' tool-call args. scope =
|
|
613
|
+
// the caller's principal (owner-sourced), so the single-DB read guard isolates it at the SQL layer too.
|
|
424
614
|
sendJson(res, 200, { pending: principal ? await store.listPending(principal, principal) : [] });
|
|
425
615
|
}
|
|
426
616
|
return;
|
|
@@ -429,12 +619,19 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
429
619
|
if (m) {
|
|
430
620
|
const id = m[1];
|
|
431
621
|
if (req.method === "GET") {
|
|
622
|
+
// The row carries the full tool-call args (commands/paths/contents of a high-risk op). The list
|
|
623
|
+
// endpoint deliberately scopes non-operators to their OWN pending ("never others' tool-call args");
|
|
624
|
+
// a by-id GET must enforce the same boundary, else any token-holder who learns an id reads another
|
|
625
|
+
// tenant's args. Operators see all; everyone else only their own. 404 (not 403) to non-owners so
|
|
626
|
+
// existence isn't revealed — symmetric with runOwnerOk.
|
|
432
627
|
const principal = principalFrom(req, deps.config);
|
|
433
628
|
if (deps.config.requirePrincipal && !principal) {
|
|
434
629
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
435
630
|
return;
|
|
436
631
|
}
|
|
437
632
|
const operator = isOperator(principal, deps.config.operatorPrincipals);
|
|
633
|
+
// Operators read across tenants (scope-agnostic getById); a non-operator reads ONLY its own scope so the
|
|
634
|
+
// single-DB read guard fires at the SQL layer (no cross-tenant args even if it learns an id).
|
|
438
635
|
const row = operator ? await store.getById(id) : principal != null ? await store.get(id, principal) : undefined;
|
|
439
636
|
if (!row || (!operator && row.owner !== null && row.owner !== principal)) {
|
|
440
637
|
sendError(res, 404, "not_found.approval", "approval not found");
|
|
@@ -444,6 +641,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
444
641
|
return;
|
|
445
642
|
}
|
|
446
643
|
if (req.method === "POST") {
|
|
644
|
+
// Deciding (approve/deny) is OPERATOR-ONLY — a requester approving its own high-risk op would
|
|
645
|
+
// defeat the F4 gate. (Legacy/unset OPERATOR_PRINCIPALS → the service token is the boundary — but
|
|
646
|
+
// ONLY on a single-tenant deployment: under REQUIRE_PRINCIPAL the empty-list true-for-all would let
|
|
647
|
+
// ANY tenant decide ANOTHER tenant's approval [the durable /decide got
|
|
648
|
+
// this hardening earlier and this legacy leg never did] — multi-tenant demands an EXPLICIT
|
|
649
|
+
// operator list, same posture as the durable twin.)
|
|
447
650
|
const principal = principalFrom(req, deps.config);
|
|
448
651
|
const operatorOk = deps.config.requirePrincipal
|
|
449
652
|
? explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals)
|
|
@@ -458,6 +661,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
458
661
|
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason? }");
|
|
459
662
|
return;
|
|
460
663
|
}
|
|
664
|
+
// The operator decides ACROSS tenants by design (the F4 operator queue), so we bind the decide CAS to the
|
|
665
|
+
// row's OWN scope (read scope-agnostically first) rather than the operator's principal — the scope<=>?
|
|
666
|
+
// clause then becomes an exact, consistent guard instead of a cross-tenant bypass. Unknown id ⇒ 409.
|
|
461
667
|
const target = await store.getById(id);
|
|
462
668
|
if (!target) {
|
|
463
669
|
sendError(res, 409, "conflict.approval_settled", "approval already decided or not found");
|
|
@@ -476,10 +682,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
476
682
|
}
|
|
477
683
|
miss.fell = true;
|
|
478
684
|
}
|
|
685
|
+
/** Server-side poll cadence for the native-push approvals stream. The CLIENT subscribes ONCE and never polls;
|
|
686
|
+
* the server diffs at this cadence (TiDB has no LISTEN/NOTIFY) and pushes deltas. 3s < the ~10s client poll it
|
|
687
|
+
* replaces, so the operator sees a new suspend faster AND the portal loses its polling loop. (Each SSE
|
|
688
|
+
* connection runs its OWN poll loop — fine for a handful of operators; a shared fan-out poll is a future
|
|
689
|
+
* optimization if the operator count grows.) */
|
|
479
690
|
const APPROVALS_STREAM_POLL_MS = 3000;
|
|
691
|
+
/**
|
|
692
|
+
* GET /v1/approvals/stream (design/80 native push): SSE — pushes pending-approval deltas so the portal
|
|
693
|
+
* SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
|
|
694
|
+
* loop). Polls listPending(scope) and diffs against the prior snapshot: `pending` for a new suspend, `resolved`
|
|
695
|
+
* for a decided/expired/gone one. Cross-replica BY CONSTRUCTION — the poll reads the SHARED checkpoint table,
|
|
696
|
+
* so a suspend on ANY replica is seen by an operator streaming on a DIFFERENT replica. A transient DB blip
|
|
697
|
+
* heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
|
|
698
|
+
* Poll-granularity caveat: a pending that resolves AND re-suspends on the same (session,toolCallId) within one
|
|
699
|
+
* interval shows no delta — acceptable (the live snapshot is always eventually correct; no decision is missed).
|
|
700
|
+
*/
|
|
480
701
|
export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_STREAM_POLL_MS) {
|
|
702
|
+
// JSON-encode the (sessionId, toolCallId) pair so distinct pendings can NEVER collide into one Map key,
|
|
703
|
+
// regardless of what a caller-supplied sessionId contains (any single-char delimiter — space OR even NUL — is
|
|
704
|
+
// injectable by an adversarial id, masking/wrong-removing an approval card; adversarial finding). JSON escaping
|
|
705
|
+
// makes every distinct (sessionId, toolCallId) pair a distinct key.
|
|
481
706
|
const keyOf = (p) => JSON.stringify([p.sessionId, p.toolCallId ?? null]);
|
|
482
707
|
sseHeaders(res);
|
|
708
|
+
// Each data payload ALSO carries a `type` field mirroring the SSE event name — so a client can dispatch on
|
|
709
|
+
// `data.type` without relying on the SSE `event:` line (some proxies strip event names; `onmessage` ignores
|
|
710
|
+
// named events). The SSE event name stays for native EventSource addEventListener.
|
|
483
711
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "approvals-delta", pollMs: APPROVALS_STREAM_POLL_MS })}\n\n`);
|
|
484
712
|
let closed = false;
|
|
485
713
|
req.on("close", () => { closed = true; });
|
|
@@ -494,6 +722,7 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
494
722
|
pending = await cs.listPending(scope);
|
|
495
723
|
}
|
|
496
724
|
catch {
|
|
725
|
+
// a transient TiDB blip must NOT kill the subscription — heartbeat + retry next tick (fail-soft)
|
|
497
726
|
if (!res.writableEnded)
|
|
498
727
|
res.write(`event: heartbeat\ndata: ${JSON.stringify({ type: "heartbeat" })}\n\n`);
|
|
499
728
|
await sleep(pollMs);
|
|
@@ -501,6 +730,7 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
501
730
|
}
|
|
502
731
|
const cur = new Map(pending.map((p) => [keyOf(p), p]));
|
|
503
732
|
if (first) {
|
|
733
|
+
// initial snapshot so a freshly-subscribed portal renders the current queue immediately (no first-poll gap)
|
|
504
734
|
for (const p of pending)
|
|
505
735
|
res.write(`event: pending\ndata: ${JSON.stringify({ type: "pending", ...p })}\n\n`);
|
|
506
736
|
res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
|
|
@@ -509,10 +739,10 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
509
739
|
else {
|
|
510
740
|
for (const [k, p] of cur)
|
|
511
741
|
if (!prev.has(k))
|
|
512
|
-
res.write(`event: pending\ndata: ${JSON.stringify({ type: "pending", ...p })}\n\n`);
|
|
742
|
+
res.write(`event: pending\ndata: ${JSON.stringify({ type: "pending", ...p })}\n\n`); // a new suspend
|
|
513
743
|
for (const [k, p] of prev)
|
|
514
744
|
if (!cur.has(k))
|
|
515
|
-
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
|
|
745
|
+
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`); // decided/expired/gone
|
|
516
746
|
}
|
|
517
747
|
prev = cur;
|
|
518
748
|
if (Date.now() - start > MAX_MS) {
|