@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.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/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
- package/dist/activities/execute-deep-agent/hitl.js +5 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +370 -56
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
- package/dist/activities/execute-deep-agent/status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +15 -1
- package/src/activities/execute-deep-agent/index.ts +434 -64
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +19 -7
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime verification: does a SUB-AGENT approval interrupt propagate to the
|
|
3
|
+
* PARENT checkpoint, and does Command(resume=…) route back into the nested
|
|
4
|
+
* interrupt?
|
|
5
|
+
*
|
|
6
|
+
* This is the make-or-break question behind installing the approval gate on
|
|
7
|
+
* sub-agent graphs (HITL Phase 2, Slice B). The resume path in `hitl.ts` /
|
|
8
|
+
* `index.ts` reads pending interrupts from the PARENT's top-level
|
|
9
|
+
* `graphState.tasks[].interrupts` and resumes via `Command(resume={taskId: …})`.
|
|
10
|
+
* If a sub-agent interrupt does NOT surface there (or cannot be resumed),
|
|
11
|
+
* gating sub-agent tools would hang executions — strictly worse than the current
|
|
12
|
+
* bypass. So we prove the mechanism end-to-end against the real deepagents +
|
|
13
|
+
* LangGraph runtime before wiring it.
|
|
14
|
+
*
|
|
15
|
+
* Shape under test (faithful to the real pipeline): a deepagents PARENT with a
|
|
16
|
+
* checkpointer and `subagents: [worker]`; the worker is a deepagents agent
|
|
17
|
+
* carrying our real ApprovalGate middleware and NO checkpointer (exactly how
|
|
18
|
+
* compileSubagents builds it). The parent's `task` tool invokes the worker via
|
|
19
|
+
* `worker.invoke(state, {...parentConfig})` — so a GraphInterrupt thrown inside
|
|
20
|
+
* the worker (no checkpointer → re-thrown) must bubble to the parent (has
|
|
21
|
+
* checkpointer → records it).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from "vitest";
|
|
25
|
+
import { HumanMessage } from "@langchain/core/messages";
|
|
26
|
+
import { tool } from "@langchain/core/tools";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { Command, MemorySaver } from "@langchain/langgraph";
|
|
29
|
+
import { createDeepAgent, StateBackend } from "deepagents";
|
|
30
|
+
import { createApprovalGateMiddleware } from "../../../middleware/approval-gate.js";
|
|
31
|
+
import {
|
|
32
|
+
ScriptedModel,
|
|
33
|
+
readPendingInterrupts,
|
|
34
|
+
type ScriptSelector,
|
|
35
|
+
} from "../__test-utils__/scripted-model.js";
|
|
36
|
+
|
|
37
|
+
// Role script for this test: the WORKER (its bound tools include the gated
|
|
38
|
+
// `overwrite_file`) writes a file; the PARENT delegates via `task`. createDeepAgent
|
|
39
|
+
// injects `task` into both, so the role is keyed on the worker's unique tool.
|
|
40
|
+
const roleScript: ScriptSelector = (toolNames) =>
|
|
41
|
+
toolNames.includes("overwrite_file")
|
|
42
|
+
? {
|
|
43
|
+
toolCalls: [
|
|
44
|
+
{ name: "overwrite_file", args: { path: "/out.txt", content: "hello" }, id: "write_1" },
|
|
45
|
+
],
|
|
46
|
+
done: "worker done",
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
toolCalls: [
|
|
50
|
+
{ name: "task", args: { description: "write the file", subagent_type: "worker" }, id: "task_1" },
|
|
51
|
+
],
|
|
52
|
+
done: "parent done",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("sub-agent approval interrupt propagation", () => {
|
|
56
|
+
it("surfaces a sub-agent interrupt at the parent checkpoint AND resumes it", async () => {
|
|
57
|
+
let executed = 0;
|
|
58
|
+
const overwriteFile = tool(
|
|
59
|
+
async () => {
|
|
60
|
+
executed += 1;
|
|
61
|
+
return "wrote file";
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "overwrite_file",
|
|
65
|
+
description: "overwrite a file at a path",
|
|
66
|
+
schema: z.object({ path: z.string(), content: z.string() }),
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Worker: real ApprovalGate middleware, NO checkpointer — exactly how
|
|
71
|
+
// compileSubagents builds a sub-agent. overwrite_file classifies as
|
|
72
|
+
// FILE_WRITE, so the gate must interrupt before it runs.
|
|
73
|
+
const worker = await createDeepAgent({
|
|
74
|
+
model: new ScriptedModel(roleScript),
|
|
75
|
+
tools: [overwriteFile],
|
|
76
|
+
middleware: [
|
|
77
|
+
createApprovalGateMiddleware({ policies: new Map(), toolServerMap: new Map() }),
|
|
78
|
+
],
|
|
79
|
+
backend: new StateBackend(),
|
|
80
|
+
generalPurposeAgent: false,
|
|
81
|
+
} as unknown as Parameters<typeof createDeepAgent>[0]);
|
|
82
|
+
|
|
83
|
+
const checkpointer = new MemorySaver();
|
|
84
|
+
const parent = await createDeepAgent({
|
|
85
|
+
model: new ScriptedModel(roleScript),
|
|
86
|
+
checkpointer: checkpointer as never,
|
|
87
|
+
backend: new StateBackend(),
|
|
88
|
+
subagents: [{ name: "worker", description: "writes files", runnable: worker as never }],
|
|
89
|
+
} as unknown as Parameters<typeof createDeepAgent>[0]);
|
|
90
|
+
|
|
91
|
+
const config = { configurable: { thread_id: "thread-1" }, recursionLimit: 50 };
|
|
92
|
+
|
|
93
|
+
// Drive to the first pause. The worker's gated tool call must NOT have run.
|
|
94
|
+
await parent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
|
|
95
|
+
|
|
96
|
+
const afterFirst = await parent.getState(config);
|
|
97
|
+
const pending = readPendingInterrupts(afterFirst as never);
|
|
98
|
+
|
|
99
|
+
expect(pending.length).toBeGreaterThanOrEqual(1);
|
|
100
|
+
expect(pending[0].toolCallId).toBe("write_1");
|
|
101
|
+
expect(executed).toBe(0);
|
|
102
|
+
|
|
103
|
+
// Resume with APPROVE, keyed by the actual interrupt id (NOT task.id — for a
|
|
104
|
+
// nested sub-agent interrupt the two differ, and only the interrupt id routes
|
|
105
|
+
// the resume value into the worker's interrupt()).
|
|
106
|
+
const resumeDict: Record<string, { action: string }> = {};
|
|
107
|
+
for (const p of pending) resumeDict[p.interruptId] = { action: "approve" };
|
|
108
|
+
|
|
109
|
+
await parent.invoke(new Command({ resume: resumeDict }), config);
|
|
110
|
+
|
|
111
|
+
const afterResume = await parent.getState(config);
|
|
112
|
+
const stillPending = readPendingInterrupts(afterResume as never);
|
|
113
|
+
|
|
114
|
+
expect(stillPending.length).toBe(0);
|
|
115
|
+
expect(executed).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("PARENT-level gated tool: interrupt-id keyed resume also works (fix is safe)", async () => {
|
|
119
|
+
// Guards the hitl.ts change: keying resume by interrupts[].id must also drive
|
|
120
|
+
// a top-level (non-sub-agent) approval, so switching off task.id is safe.
|
|
121
|
+
let executed = 0;
|
|
122
|
+
const overwriteFile = tool(
|
|
123
|
+
async () => {
|
|
124
|
+
executed += 1;
|
|
125
|
+
return "wrote file";
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "overwrite_file",
|
|
129
|
+
description: "overwrite a file at a path",
|
|
130
|
+
schema: z.object({ path: z.string(), content: z.string() }),
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const checkpointer = new MemorySaver();
|
|
135
|
+
const parent = await createDeepAgent({
|
|
136
|
+
model: new ScriptedModel(roleScript),
|
|
137
|
+
checkpointer: checkpointer as never,
|
|
138
|
+
backend: new StateBackend(),
|
|
139
|
+
tools: [overwriteFile],
|
|
140
|
+
middleware: [
|
|
141
|
+
createApprovalGateMiddleware({ policies: new Map(), toolServerMap: new Map() }),
|
|
142
|
+
],
|
|
143
|
+
} as unknown as Parameters<typeof createDeepAgent>[0]);
|
|
144
|
+
|
|
145
|
+
const config = { configurable: { thread_id: "thread-parent" }, recursionLimit: 50 };
|
|
146
|
+
await parent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
|
|
147
|
+
|
|
148
|
+
const afterFirst = await parent.getState(config);
|
|
149
|
+
const pending = readPendingInterrupts(afterFirst as never);
|
|
150
|
+
expect(pending.length).toBeGreaterThanOrEqual(1);
|
|
151
|
+
expect(executed).toBe(0);
|
|
152
|
+
|
|
153
|
+
const resumeDict: Record<string, { action: string }> = {};
|
|
154
|
+
for (const p of pending) resumeDict[p.interruptId] = { action: "approve" };
|
|
155
|
+
await parent.invoke(new Command({ resume: resumeDict }), config);
|
|
156
|
+
|
|
157
|
+
expect(readPendingInterrupts((await parent.getState(config)) as never).length).toBe(0);
|
|
158
|
+
expect(executed).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that a
|
|
3
|
+
* SUB-AGENT's gitignored file writes are captured for CAS review at parity with
|
|
4
|
+
* the parent (Session 26, DD-19).
|
|
5
|
+
*
|
|
6
|
+
* These tests exercise the ACTUAL wiring under change — `compileSubagents` — which
|
|
7
|
+
* installs a CAS-observing backend on each sub-agent and flips its gate to flow
|
|
8
|
+
* gitignored writes (both derived from the shared observer, so "unobserved bytes"
|
|
9
|
+
* is impossible by construction). A non-secret gitignored write flows and is
|
|
10
|
+
* observed; a secret-like one is hard-blocked with nothing written; a git-tracked
|
|
11
|
+
* write is left to the boundary git diff (not observed).
|
|
12
|
+
*
|
|
13
|
+
* Scope note: the turn-boundary CANDIDATE authoring, `changeSetId`/`turnSeq`, and
|
|
14
|
+
* the Temporal resume loop are parent/activity-level machinery unchanged by this
|
|
15
|
+
* work (covered by the offline/corpus suites). These tests assert the observer
|
|
16
|
+
* capture surface (before-bytes + blocked-secret paths + on-disk result) that
|
|
17
|
+
* those consumers read.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
21
|
+
import { mkdtemp, mkdir, rm, readFile, access } from "node:fs/promises";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { HumanMessage } from "@langchain/core/messages";
|
|
25
|
+
import { tool } from "@langchain/core/tools";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { Command, MemorySaver } from "@langchain/langgraph";
|
|
28
|
+
import { createDeepAgent, StateBackend } from "deepagents";
|
|
29
|
+
import type { ApprovalGateConfig } from "../../../middleware/approval-gate.js";
|
|
30
|
+
import { partitionIgnoredPathsBySecret } from "../../../shared/filereview/secret-paths.js";
|
|
31
|
+
import { resolveWorkspacePath } from "../../../shared/file-change.js";
|
|
32
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
33
|
+
import { compileSubagents, type TransformedSubagent } from "../subagent-transformer.js";
|
|
34
|
+
import { ScriptedModel, readPendingInterrupts, type ScriptSelector } from "../__test-utils__/scripted-model.js";
|
|
35
|
+
|
|
36
|
+
/** Only `src/**` is git-tracked (capturable); everything else is gitignored. */
|
|
37
|
+
const isTracked = (relPath: string): boolean => relPath.startsWith("src/");
|
|
38
|
+
|
|
39
|
+
describe("sub-agent gitignored capture (DD-19)", () => {
|
|
40
|
+
let root: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
root = await mkdtemp(join(tmpdir(), "sa-cas-"));
|
|
44
|
+
await mkdir(join(root, "dist"), { recursive: true });
|
|
45
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
46
|
+
await mkdir(join(root, "cache"), { recursive: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await rm(root, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function makeObserver(): CasCaptureObserver {
|
|
54
|
+
return new CasCaptureObserver({
|
|
55
|
+
rootDir: root,
|
|
56
|
+
isIgnored: async (relPath) => !isTracked(relPath),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A capture-mode gate wired to `observer`, exactly as setup.ts builds the parent
|
|
62
|
+
* gate and buildSubAgentMiddleware inherits it when a CAS observer backs the
|
|
63
|
+
* sub-agent (captureIgnored true).
|
|
64
|
+
*/
|
|
65
|
+
function captureGate(observer: CasCaptureObserver): ApprovalGateConfig {
|
|
66
|
+
return {
|
|
67
|
+
policies: new Map(),
|
|
68
|
+
toolServerMap: new Map(),
|
|
69
|
+
fileCaptureMode: true,
|
|
70
|
+
isCapturablePath: async (raw: string) =>
|
|
71
|
+
isTracked(resolveWorkspacePath(raw, root, true).path),
|
|
72
|
+
captureIgnored: true,
|
|
73
|
+
recordBlockedSecret: (raw: string) => observer.recordBlockedSecret(raw),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compile a sub-agent through the production path (the code under change), with
|
|
79
|
+
* the shared CAS observer so its backend + gate are the real capture wiring.
|
|
80
|
+
*/
|
|
81
|
+
async function compileWorker(
|
|
82
|
+
observer: CasCaptureObserver,
|
|
83
|
+
script: ScriptSelector,
|
|
84
|
+
tools: TransformedSubagent["tools"] = [],
|
|
85
|
+
) {
|
|
86
|
+
const compiled = await compileSubagents(
|
|
87
|
+
[{ name: "worker", description: "test worker", systemPrompt: "do the work", tools }],
|
|
88
|
+
{
|
|
89
|
+
approvalGate: captureGate(observer),
|
|
90
|
+
parentModelName: "test-model",
|
|
91
|
+
workspaceRootDir: root,
|
|
92
|
+
casObserver: observer,
|
|
93
|
+
modelFactory: async () => new ScriptedModel(script),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
expect(compiled).toHaveLength(1);
|
|
97
|
+
return compiled[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it("captures a sub-agent's non-secret gitignored write, hard-blocks a secret one, and leaves git-tracked to the git diff", async () => {
|
|
101
|
+
const observer = makeObserver();
|
|
102
|
+
|
|
103
|
+
// A single worker; script it to write three files in one turn (no `task`, so
|
|
104
|
+
// no sub-agent delegation ambiguity — we invoke the compiled sub-agent, which
|
|
105
|
+
// IS what the parent dispatches, directly).
|
|
106
|
+
const script: ScriptSelector = () => ({
|
|
107
|
+
toolCalls: [
|
|
108
|
+
{ name: "write_file", args: { file_path: "dist/app.js", content: "console.log(1)" }, id: "w_ignored" },
|
|
109
|
+
{ name: "write_file", args: { file_path: ".env", content: "SECRET=shh" }, id: "w_secret" },
|
|
110
|
+
{ name: "write_file", args: { file_path: "src/app.ts", content: "export const x = 1;" }, id: "w_tracked" },
|
|
111
|
+
],
|
|
112
|
+
done: "worker done",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const worker = await compileWorker(observer, script);
|
|
116
|
+
await worker.runnable.invoke(
|
|
117
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
118
|
+
{ configurable: { thread_id: "t1" }, recursionLimit: 50 },
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Non-secret gitignored write: flowed (on disk) AND observed (before=null ADD).
|
|
122
|
+
expect(await readFile(join(root, "dist/app.js"), "utf8")).toBe("console.log(1)");
|
|
123
|
+
expect(observer.before.has("dist/app.js")).toBe(true);
|
|
124
|
+
expect(observer.before.get("dist/app.js")).toBeNull();
|
|
125
|
+
|
|
126
|
+
// Secret-like gitignored write: hard-blocked — nothing written, path recorded.
|
|
127
|
+
await expect(access(join(root, ".env"))).rejects.toThrow();
|
|
128
|
+
expect([...observer.blockedSecretPaths]).toContain(".env");
|
|
129
|
+
|
|
130
|
+
// Git-tracked write: flowed to disk but NOT observed (the git diff owns it).
|
|
131
|
+
expect(await readFile(join(root, "src/app.ts"), "utf8")).toBe("export const x = 1;");
|
|
132
|
+
expect(observer.before.has("src/app.ts")).toBe(false);
|
|
133
|
+
|
|
134
|
+
// Bridge to the boundary: the observer state partitions into a
|
|
135
|
+
// GIT_IGNORED_CAPTURED (dist/app.js) and a DIFF_UNREVIEWABLE (.env), exactly
|
|
136
|
+
// as buildCasTurnCaptures does at the turn boundary.
|
|
137
|
+
const { capturablePaths, unreviewablePaths } = partitionIgnoredPathsBySecret(
|
|
138
|
+
observer.before.keys(),
|
|
139
|
+
observer.blockedSecretPaths,
|
|
140
|
+
);
|
|
141
|
+
expect([...capturablePaths]).toEqual(["dist/app.js"]);
|
|
142
|
+
expect([...unreviewablePaths]).toEqual([".env"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("mixed turn: a sub-agent's gitignored write is captured even when the same turn pauses on a gated tool, and resume completes", async () => {
|
|
146
|
+
const observer = makeObserver();
|
|
147
|
+
let shellRuns = 0;
|
|
148
|
+
// `shell` classifies as the SHELL category, so capture mode never bypasses it —
|
|
149
|
+
// it interrupts (parity with the parent). `cas_marker` is a role marker unique
|
|
150
|
+
// to the worker (the parent lacks it), used only to select the worker's script
|
|
151
|
+
// branch — deepagents auto-injects `task` into every agent, so `task` cannot
|
|
152
|
+
// distinguish the roles.
|
|
153
|
+
const shell = tool(async () => { shellRuns += 1; return "ran"; }, {
|
|
154
|
+
name: "shell",
|
|
155
|
+
description: "run a command",
|
|
156
|
+
schema: z.object({ command: z.string() }),
|
|
157
|
+
});
|
|
158
|
+
const marker = tool(async () => "noop", {
|
|
159
|
+
name: "cas_marker",
|
|
160
|
+
description: "worker role marker",
|
|
161
|
+
schema: z.object({}),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const script: ScriptSelector = (toolNames) =>
|
|
165
|
+
toolNames.includes("cas_marker")
|
|
166
|
+
? {
|
|
167
|
+
toolCalls: [
|
|
168
|
+
{ name: "write_file", args: { file_path: "cache/data.txt", content: "cached" }, id: "w_ignored" },
|
|
169
|
+
{ name: "shell", args: { command: "echo hi" }, id: "sh_1" },
|
|
170
|
+
],
|
|
171
|
+
done: "worker done",
|
|
172
|
+
}
|
|
173
|
+
: {
|
|
174
|
+
toolCalls: [
|
|
175
|
+
{ name: "task", args: { description: "work", subagent_type: "worker" }, id: "task_1" },
|
|
176
|
+
],
|
|
177
|
+
done: "parent done",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const worker = await compileWorker(observer, script, [shell, marker] as TransformedSubagent["tools"]);
|
|
181
|
+
|
|
182
|
+
const parent = await createDeepAgent({
|
|
183
|
+
model: new ScriptedModel(script),
|
|
184
|
+
checkpointer: new MemorySaver() as never,
|
|
185
|
+
backend: new StateBackend(),
|
|
186
|
+
subagents: [{ name: worker.name, description: worker.description, runnable: worker.runnable as never }],
|
|
187
|
+
} as unknown as Parameters<typeof createDeepAgent>[0]);
|
|
188
|
+
|
|
189
|
+
const config = { configurable: { thread_id: "t2" }, recursionLimit: 50 };
|
|
190
|
+
|
|
191
|
+
// Invocation 1: the gitignored write flows and is captured BEFORE the turn
|
|
192
|
+
// pauses on the gated shell tool (the shell must NOT have run yet).
|
|
193
|
+
await parent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
|
|
194
|
+
|
|
195
|
+
const pending = readPendingInterrupts((await parent.getState(config)) as never);
|
|
196
|
+
expect(pending.some((p) => p.toolCallId === "sh_1")).toBe(true);
|
|
197
|
+
expect(shellRuns).toBe(0);
|
|
198
|
+
expect(await readFile(join(root, "cache/data.txt"), "utf8")).toBe("cached");
|
|
199
|
+
expect(observer.before.get("cache/data.txt")).toBeNull();
|
|
200
|
+
|
|
201
|
+
// Resume with APPROVE: the worker re-runs (no checkpointer) and the shell runs
|
|
202
|
+
// once. First-touch-wins keeps the single pre-turn baseline for the re-written
|
|
203
|
+
// gitignored file (no duplicate, no post-write baseline).
|
|
204
|
+
const resumeDict: Record<string, { action: string }> = {};
|
|
205
|
+
for (const p of pending) resumeDict[p.interruptId] = { action: "approve" };
|
|
206
|
+
await parent.invoke(new Command({ resume: resumeDict }), config);
|
|
207
|
+
|
|
208
|
+
expect(readPendingInterrupts((await parent.getState(config)) as never).length).toBe(0);
|
|
209
|
+
expect(shellRuns).toBe(1);
|
|
210
|
+
expect(observer.before.size).toBe(1);
|
|
211
|
+
expect(observer.before.get("cache/data.txt")).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -428,8 +428,7 @@ describe("transformAndCompileSubagents", () => {
|
|
|
428
428
|
parentMcpUsages: [] as McpServerUsage[],
|
|
429
429
|
skillClient: {} as unknown,
|
|
430
430
|
workspaceBackend: mockWorkspaceBackend(),
|
|
431
|
-
|
|
432
|
-
autoApproveAll: false,
|
|
431
|
+
approvalGate: null,
|
|
433
432
|
parentModelName: "claude-sonnet-4-6",
|
|
434
433
|
parentHasNativeThinking: true,
|
|
435
434
|
costCap: undefined,
|
|
@@ -659,8 +658,7 @@ describe("subagent-transformer edge cases", () => {
|
|
|
659
658
|
parentMcpUsages: [],
|
|
660
659
|
skillClient: {} as unknown,
|
|
661
660
|
workspaceBackend: mockWorkspaceBackend({ rootDir: "" }),
|
|
662
|
-
|
|
663
|
-
autoApproveAll: false,
|
|
661
|
+
approvalGate: null,
|
|
664
662
|
parentModelName: "claude-sonnet-4-6",
|
|
665
663
|
parentHasNativeThinking: true,
|
|
666
664
|
costCap: undefined,
|
|
@@ -683,8 +681,7 @@ describe("subagent-transformer edge cases", () => {
|
|
|
683
681
|
parentMcpUsages: [],
|
|
684
682
|
skillClient: {} as unknown,
|
|
685
683
|
workspaceBackend: mockWorkspaceBackend(),
|
|
686
|
-
|
|
687
|
-
autoApproveAll: false,
|
|
684
|
+
approvalGate: null,
|
|
688
685
|
parentModelName: "claude-sonnet-4-6",
|
|
689
686
|
parentHasNativeThinking: true,
|
|
690
687
|
costCap: undefined,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { buildSubAgentMiddleware } from "../subagent-wiring.js";
|
|
3
3
|
import { createCostCapMiddleware } from "../../../middleware/cost-cap.js";
|
|
4
|
+
import * as approvalGateModule from "../../../middleware/approval-gate.js";
|
|
4
5
|
|
|
5
6
|
describe("buildSubAgentMiddleware", () => {
|
|
6
7
|
it("returns the correct middleware order without cost cap", () => {
|
|
@@ -68,4 +69,86 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
68
69
|
|
|
69
70
|
expect(stack[2].name).toBe("ToolTruncationMiddleware");
|
|
70
71
|
});
|
|
72
|
+
|
|
73
|
+
it("installs the approval gate when an approvalGate config is provided", () => {
|
|
74
|
+
const stack = buildSubAgentMiddleware({
|
|
75
|
+
approvalGate: { policies: new Map(), toolServerMap: new Map() },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// loop, budget, truncation, approval gate
|
|
79
|
+
expect(stack).toHaveLength(4);
|
|
80
|
+
expect(stack[3].name).toBe("ApprovalGateMiddleware");
|
|
81
|
+
expect(stack[3].wrapToolCall).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("omits the approval gate when approvalGate is null (auto-approve-all parity)", () => {
|
|
85
|
+
const stack = buildSubAgentMiddleware({ approvalGate: null });
|
|
86
|
+
|
|
87
|
+
expect(stack).toHaveLength(3);
|
|
88
|
+
expect(stack.some((m) => m.name === "ApprovalGateMiddleware")).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("orders the gate before the cost-cap view", () => {
|
|
92
|
+
const parentCostCap = createCostCapMiddleware({
|
|
93
|
+
maxCostUsd: 10,
|
|
94
|
+
inputPricePerMillion: 3,
|
|
95
|
+
outputPricePerMillion: 15,
|
|
96
|
+
cacheReadPricePerMillion: 0.3,
|
|
97
|
+
warningPct: 80,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const stack = buildSubAgentMiddleware({
|
|
101
|
+
costCap: parentCostCap,
|
|
102
|
+
approvalGate: { policies: new Map(), toolServerMap: new Map() },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// loop, budget, truncation, approval gate, cost cap view
|
|
106
|
+
expect(stack).toHaveLength(5);
|
|
107
|
+
expect(stack[3].name).toBe("ApprovalGateMiddleware");
|
|
108
|
+
expect(stack[4].name).toBe("CostCapSubAgentView");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// captureIgnored is the structural coupling that makes sub-agent gitignored
|
|
112
|
+
// capture safe (DD-19): the gate flows gitignored writes into CAS iff a CAS
|
|
113
|
+
// observer backs the sub-agent's backend (compileSubagents passes !!casObserver).
|
|
114
|
+
describe("captureIgnored (sub-agent CAS routing)", () => {
|
|
115
|
+
it("captureIgnored:true inherits the parent gate verbatim (CAS routing + secret sink preserved)", () => {
|
|
116
|
+
const recordBlockedSecret = vi.fn();
|
|
117
|
+
const parentGate = {
|
|
118
|
+
policies: new Map(),
|
|
119
|
+
toolServerMap: new Map(),
|
|
120
|
+
captureIgnored: true,
|
|
121
|
+
recordBlockedSecret,
|
|
122
|
+
};
|
|
123
|
+
const spy = vi.spyOn(approvalGateModule, "createApprovalGateMiddleware");
|
|
124
|
+
|
|
125
|
+
buildSubAgentMiddleware({ approvalGate: parentGate, captureIgnored: true });
|
|
126
|
+
|
|
127
|
+
const cfg = spy.mock.calls[0]![0];
|
|
128
|
+
expect(cfg.captureIgnored).toBe(true);
|
|
129
|
+
expect(cfg.recordBlockedSecret).toBe(recordBlockedSecret);
|
|
130
|
+
spy.mockRestore();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("regression lock: without captureIgnored, CAS routing is forced OFF and the secret sink dropped even if the parent gate had them", () => {
|
|
134
|
+
const recordBlockedSecret = vi.fn();
|
|
135
|
+
// A parent gate that DOES route gitignored writes into CAS — the sub-agent
|
|
136
|
+
// must not inherit that unless a CAS observer explicitly backs it, else it
|
|
137
|
+
// would apply unobserved, unreviewable bytes.
|
|
138
|
+
const parentGate = {
|
|
139
|
+
policies: new Map(),
|
|
140
|
+
toolServerMap: new Map(),
|
|
141
|
+
captureIgnored: true,
|
|
142
|
+
recordBlockedSecret,
|
|
143
|
+
};
|
|
144
|
+
const spy = vi.spyOn(approvalGateModule, "createApprovalGateMiddleware");
|
|
145
|
+
|
|
146
|
+
buildSubAgentMiddleware({ approvalGate: parentGate });
|
|
147
|
+
|
|
148
|
+
const cfg = spy.mock.calls[0]![0];
|
|
149
|
+
expect(cfg.captureIgnored).toBe(false);
|
|
150
|
+
expect(cfg.recordBlockedSecret).toBeUndefined();
|
|
151
|
+
spy.mockRestore();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
71
154
|
});
|
|
@@ -271,12 +271,15 @@ describe("V3StatusBuilder", () => {
|
|
|
271
271
|
sb.setApprovalProvider({
|
|
272
272
|
policies: new Map([
|
|
273
273
|
["my-server/dangerous_tool", {
|
|
274
|
+
toolName: "dangerous_tool",
|
|
275
|
+
mcpServerSlug: "my-server",
|
|
274
276
|
requiresApproval: true,
|
|
275
277
|
approvalMessage: "This tool will modify {{args.path}}",
|
|
278
|
+
source: "classifier_default",
|
|
276
279
|
}],
|
|
277
280
|
]),
|
|
278
281
|
toolServerMap: new Map([["dangerous_tool", "my-server"]]),
|
|
279
|
-
|
|
282
|
+
globalBypass: false,
|
|
280
283
|
} as ApprovalPolicyProvider);
|
|
281
284
|
|
|
282
285
|
feedAll(sb, [
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-execution args capture for the HITL approval gate (native harness).
|
|
3
|
+
*
|
|
4
|
+
* When the graph pauses for approval — before the tool runs — this captures a
|
|
5
|
+
* sanitized args preview so the gate can show the proposed change. It owns only
|
|
6
|
+
* the native-specific seam: correlating a gated `tool_call_id` to its arguments
|
|
7
|
+
* from graph state (the authoritative single source of truth at the interrupt,
|
|
8
|
+
* since the streamed input cache is empty and the interrupt value carries only a
|
|
9
|
+
* few fields). The approval card renders the proposed write/edit content from
|
|
10
|
+
* these args; there is no separate captured `file_changes` (removed in Phase 5
|
|
11
|
+
* Slice 4 — the args are the single source, and the Cursor deny-gate's
|
|
12
|
+
* exact-apply reads the same args on resume; see execute-cursor/exact-apply.ts).
|
|
13
|
+
*
|
|
14
|
+
* @since First-Class Diff Review (#186), approval-gate phase
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** What the gate capture contributes to a `WAITING_APPROVAL` `ToolCall`. */
|
|
18
|
+
export interface ApprovalCaptureResult {
|
|
19
|
+
/** Sanitized JSON args preview, omitted when there are no args to show. */
|
|
20
|
+
readonly argsPreview?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { sanitizeArgsPreview } from "./status-builder-shared.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Correlate a gated `tool_call_id` to its arguments by scanning graph-state
|
|
27
|
+
* messages for the AI-message tool call that emitted it.
|
|
28
|
+
*
|
|
29
|
+
* Matching keys on the presence of a `tool_calls` array, which is robust to
|
|
30
|
+
* whether the message is a hydrated LangChain class instance or a plain
|
|
31
|
+
* serialized object: a `ToolMessage` carries a singular `tool_call_id`, never a
|
|
32
|
+
* `tool_calls` array, so it cannot be mistaken for the emitting AI message.
|
|
33
|
+
*
|
|
34
|
+
* Returns the args object (possibly empty when the tool call took no arguments),
|
|
35
|
+
* or `undefined` when no message in scope emitted this id — e.g. a tool call
|
|
36
|
+
* whose AI message lives only inside a nested sub-agent state, which this
|
|
37
|
+
* parent-scope scan does not reach. That degrades the gate to an args-less
|
|
38
|
+
* preview for that one interrupt, exactly as today.
|
|
39
|
+
*/
|
|
40
|
+
export function findAiMessageToolCallArgs(
|
|
41
|
+
messages: readonly unknown[],
|
|
42
|
+
toolCallId: string,
|
|
43
|
+
): Record<string, unknown> | undefined {
|
|
44
|
+
for (const msg of messages) {
|
|
45
|
+
if (!msg || typeof msg !== "object") continue;
|
|
46
|
+
const toolCalls = (msg as { tool_calls?: unknown }).tool_calls;
|
|
47
|
+
if (!Array.isArray(toolCalls)) continue;
|
|
48
|
+
|
|
49
|
+
for (const tc of toolCalls) {
|
|
50
|
+
if (!tc || typeof tc !== "object") continue;
|
|
51
|
+
const entry = tc as { id?: unknown; args?: unknown };
|
|
52
|
+
if (entry.id !== toolCallId) continue;
|
|
53
|
+
|
|
54
|
+
return entry.args &&
|
|
55
|
+
typeof entry.args === "object" &&
|
|
56
|
+
!Array.isArray(entry.args)
|
|
57
|
+
? (entry.args as Record<string, unknown>)
|
|
58
|
+
: {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Capture the gate's args preview from a single correlation lookup, keeping the
|
|
66
|
+
* call site thin.
|
|
67
|
+
*
|
|
68
|
+
* Returns an empty result when the tool call cannot be correlated or took no
|
|
69
|
+
* arguments, so the gate falls back to today's behavior for that interrupt.
|
|
70
|
+
*/
|
|
71
|
+
export function captureApprovalArtifacts(opts: {
|
|
72
|
+
readonly toolCallId: string;
|
|
73
|
+
readonly messages: readonly unknown[];
|
|
74
|
+
}): ApprovalCaptureResult {
|
|
75
|
+
const args = findAiMessageToolCallArgs(opts.messages, opts.toolCallId);
|
|
76
|
+
if (!args || Object.keys(args).length === 0) return {};
|
|
77
|
+
|
|
78
|
+
const argsPreview = sanitizeArgsPreview(args) || undefined;
|
|
79
|
+
return { argsPreview };
|
|
80
|
+
}
|
|
@@ -41,7 +41,14 @@ export interface InjectedFile {
|
|
|
41
41
|
export interface InjectAttachmentsOptions {
|
|
42
42
|
readonly backend: WorkspaceBackend;
|
|
43
43
|
readonly attachments: readonly Attachment[];
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Artifact storage for cloud-mode attachment download. `undefined` when the
|
|
46
|
+
* runner could not build a store (proxy misconfig). With no attachments this is
|
|
47
|
+
* never touched (the function early-returns); a cloud-mode attachment that
|
|
48
|
+
* genuinely needs storage surfaces a clear {@link AttachmentInjectionError}
|
|
49
|
+
* rather than dereferencing undefined.
|
|
50
|
+
*/
|
|
51
|
+
readonly storage: ArtifactStorage | undefined;
|
|
45
52
|
readonly isLocalMode: boolean;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -328,7 +335,7 @@ function deriveFilename(storageKey: string): string {
|
|
|
328
335
|
|
|
329
336
|
async function downloadAttachment(
|
|
330
337
|
attachment: Attachment,
|
|
331
|
-
storage: ArtifactStorage,
|
|
338
|
+
storage: ArtifactStorage | undefined,
|
|
332
339
|
isLocalMode: boolean,
|
|
333
340
|
): Promise<Buffer> {
|
|
334
341
|
// Local mode fast path: read directly from filesystem
|
|
@@ -351,16 +358,18 @@ async function downloadAttachment(
|
|
|
351
358
|
);
|
|
352
359
|
}
|
|
353
360
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
// The attachment must come from storage, but the runner could not build one
|
|
362
|
+
// (proxy misconfig). Fail with an actionable message rather than dereferencing
|
|
363
|
+
// undefined — consistent with the degraded posture, never a silent skip.
|
|
364
|
+
if (!storage) {
|
|
365
|
+
throw new AttachmentInjectionError(
|
|
366
|
+
attachment.filename,
|
|
367
|
+
`artifact storage is unavailable, so this attachment (key: ${attachment.storageKey}) cannot be downloaded`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
361
370
|
|
|
362
|
-
|
|
363
|
-
return
|
|
371
|
+
try {
|
|
372
|
+
return await storage.download(attachment.storageKey);
|
|
364
373
|
} catch (err) {
|
|
365
374
|
if (err instanceof AttachmentInjectionError) throw err;
|
|
366
375
|
throw new AttachmentInjectionError(
|