@stigmer/runner 3.0.9-dev.20260615153829 → 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 +16 -3
- package/dist/activities/execute-deep-agent/hitl.js +11 -3
- 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 +393 -33
- 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 +17 -5
- package/dist/activities/execute-deep-agent/status-builder.js +44 -3
- 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 +32 -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 +284 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
- 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 +91 -10
- 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 +23 -7
- package/src/activities/execute-deep-agent/index.ts +463 -40
- 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 +63 -8
- 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 +36 -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,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test: resuming a Cursor execution after an approval must emit an
|
|
3
|
+
* APPEND-ONLY transcript that the backend's append-only-at-identity guard
|
|
4
|
+
* accepts.
|
|
5
|
+
*
|
|
6
|
+
* This is the precise, deterministic reproduction of the production stall traced
|
|
7
|
+
* to `aex_01kvz3pw20j6t0hw80wpevnztb` (open-computer-use "send a Slack self-DM"):
|
|
8
|
+
*
|
|
9
|
+
* 1. The agent gated two MCP tools (list_apps, get_app_state); the user
|
|
10
|
+
* approved BOTH. The persisted run-1 transcript holds them as
|
|
11
|
+
* WAITING_APPROVAL / approval_action=APPROVE.
|
|
12
|
+
* 2. On resume the Cursor SDK re-issues those approved tools with BRAND-NEW
|
|
13
|
+
* call ids and the agent advances to a third tool (click) which gets gated.
|
|
14
|
+
* 3. Before the fix, the resume rebuilt the transcript from empty, so the new
|
|
15
|
+
* status carried only the fresh ids and DROPPED the two committed ids. The
|
|
16
|
+
* backend guard (nonTerminalTranscriptRegression / transcriptRegressionReason)
|
|
17
|
+
* rejects "would drop a previously-committed tool call", so the new `click`
|
|
18
|
+
* pending was never persisted, pending_approvals stayed 0, and the workflow
|
|
19
|
+
* watchdog failed the run ("approval propagation is broken").
|
|
20
|
+
*
|
|
21
|
+
* The fix (seed the transcript on resume + reconcile re-runs by canonical
|
|
22
|
+
* identity in MessageAccumulator) makes the resume status a strict superset:
|
|
23
|
+
* the committed ids survive, the re-runs reconcile in place, and the genuinely
|
|
24
|
+
* new gated tool is appended. The guard then accepts it.
|
|
25
|
+
*
|
|
26
|
+
* The test asserts both directions against a local mirror of the Go guard
|
|
27
|
+
* (nonTerminalTranscriptRegression): a from-empty rebuild is REJECTED (the bug),
|
|
28
|
+
* a seeded rebuild is ACCEPTED (the fix).
|
|
29
|
+
*
|
|
30
|
+
* This exercises the MessageAccumulator + denial-reconciliation directly — the
|
|
31
|
+
* unit that owns the resume emitter — rather than the full Temporal activity,
|
|
32
|
+
* keeping the reproduction hermetic and pinned to the exact code under change.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { describe, it, expect } from "vitest";
|
|
36
|
+
import { create, clone } from "@bufbuild/protobuf";
|
|
37
|
+
import {
|
|
38
|
+
AgentMessageSchema,
|
|
39
|
+
ToolCallSchema,
|
|
40
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
41
|
+
import type {
|
|
42
|
+
AgentMessage,
|
|
43
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
44
|
+
import {
|
|
45
|
+
ApprovalAction,
|
|
46
|
+
MessageType,
|
|
47
|
+
ToolCallStatus,
|
|
48
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
49
|
+
import type { SDKMessage } from "@cursor/sdk";
|
|
50
|
+
import {
|
|
51
|
+
MessageAccumulator,
|
|
52
|
+
reconcileDeniedToolCalls,
|
|
53
|
+
clearProvisionalPostDenialNarration,
|
|
54
|
+
buildToolCallProto,
|
|
55
|
+
} from "../message-translator.js";
|
|
56
|
+
import {
|
|
57
|
+
grantToken,
|
|
58
|
+
buildApprovalGrants,
|
|
59
|
+
buildApprovalState,
|
|
60
|
+
reconstructAdjudicatedApprovals,
|
|
61
|
+
} from "../approval-state.js";
|
|
62
|
+
import type { DeniedLedgerEntry } from "../approval-state.js";
|
|
63
|
+
import {
|
|
64
|
+
setupCursorHookHarness,
|
|
65
|
+
hasBash,
|
|
66
|
+
hookWrite,
|
|
67
|
+
hookShell,
|
|
68
|
+
hookMcp,
|
|
69
|
+
} from "../__test-utils__/cursor-hook-harness.js";
|
|
70
|
+
import {
|
|
71
|
+
SubAgentExecutionSchema,
|
|
72
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
73
|
+
import { SubAgentStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
74
|
+
|
|
75
|
+
const MCP_SLUG = "open-computer-use";
|
|
76
|
+
|
|
77
|
+
// The two approved tools' ORIGINAL committed ids (as they appear in the
|
|
78
|
+
// persisted run-1 transcript). The resume re-issues them with the *_RESUME ids.
|
|
79
|
+
const LIST_APPS_ID = "tool_de9b3cd7";
|
|
80
|
+
const GET_STATE_ID = "tool_1c47c984";
|
|
81
|
+
|
|
82
|
+
const RUN1_AI_TEXT = "I'll inspect the open apps first.";
|
|
83
|
+
const RUN2_AI_TEXT = "Now I'll click the compose button.";
|
|
84
|
+
const RUN2_TRAILING_NARRATION = "Done — the message was sent.";
|
|
85
|
+
|
|
86
|
+
/** A Cursor MCP tool_call event (name="mcp", details nested in args). */
|
|
87
|
+
function mcpToolCallEvent(
|
|
88
|
+
callId: string,
|
|
89
|
+
toolName: string,
|
|
90
|
+
status: "running" | "completed" | "error",
|
|
91
|
+
runId: string,
|
|
92
|
+
opts?: { result?: unknown; innerArgs?: Record<string, unknown> },
|
|
93
|
+
): Extract<SDKMessage, { type: "tool_call" }> {
|
|
94
|
+
return {
|
|
95
|
+
type: "tool_call",
|
|
96
|
+
agent_id: "agent-1",
|
|
97
|
+
run_id: runId,
|
|
98
|
+
call_id: callId,
|
|
99
|
+
name: "mcp",
|
|
100
|
+
status,
|
|
101
|
+
result: opts?.result,
|
|
102
|
+
args: {
|
|
103
|
+
providerIdentifier: MCP_SLUG,
|
|
104
|
+
toolName,
|
|
105
|
+
args: opts?.innerArgs ?? {},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assistantEvent(
|
|
111
|
+
runId: string,
|
|
112
|
+
text: string,
|
|
113
|
+
): Extract<SDKMessage, { type: "assistant" }> {
|
|
114
|
+
return {
|
|
115
|
+
type: "assistant",
|
|
116
|
+
agent_id: "agent-1",
|
|
117
|
+
run_id: runId,
|
|
118
|
+
message: { role: "assistant" as const, content: [{ type: "text" as const, text }] },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The persisted run-1 transcript: an AI message bearing the two MCP tools the
|
|
124
|
+
* user already approved (status WAITING_APPROVAL, approval_action APPROVE — the
|
|
125
|
+
* exact shape the backend keeps for a decided-but-not-yet-resumed tool call).
|
|
126
|
+
*/
|
|
127
|
+
function persistedRunOneMessages(): AgentMessage[] {
|
|
128
|
+
return [
|
|
129
|
+
create(AgentMessageSchema, {
|
|
130
|
+
type: MessageType.MESSAGE_AI,
|
|
131
|
+
content: RUN1_AI_TEXT,
|
|
132
|
+
timestamp: "2026-06-24T00:00:01.000Z",
|
|
133
|
+
toolCalls: [
|
|
134
|
+
create(ToolCallSchema, {
|
|
135
|
+
id: LIST_APPS_ID,
|
|
136
|
+
name: "list_apps",
|
|
137
|
+
mcpServerSlug: MCP_SLUG,
|
|
138
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
139
|
+
requiresApproval: true,
|
|
140
|
+
approvalAction: ApprovalAction.APPROVE,
|
|
141
|
+
}),
|
|
142
|
+
create(ToolCallSchema, {
|
|
143
|
+
id: GET_STATE_ID,
|
|
144
|
+
name: "get_app_state",
|
|
145
|
+
mcpServerSlug: MCP_SLUG,
|
|
146
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
147
|
+
requiresApproval: true,
|
|
148
|
+
approvalAction: ApprovalAction.APPROVE,
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
}),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The events the SDK re-emits on resume: the two approved tools re-run with
|
|
157
|
+
* FRESH ids and complete, the agent narrates and calls a new tool (click) which
|
|
158
|
+
* is gated (emitted running, then cut off by the first-denial stop), followed by
|
|
159
|
+
* trailing provisional narration.
|
|
160
|
+
*/
|
|
161
|
+
function resumeEvents(): SDKMessage[] {
|
|
162
|
+
return [
|
|
163
|
+
assistantEvent("r2", RUN2_AI_TEXT),
|
|
164
|
+
mcpToolCallEvent(`${LIST_APPS_ID}_RESUME`, "list_apps", "running", "r2"),
|
|
165
|
+
mcpToolCallEvent(`${LIST_APPS_ID}_RESUME`, "list_apps", "completed", "r2", {
|
|
166
|
+
result: "Slack, Mail, Safari",
|
|
167
|
+
}),
|
|
168
|
+
mcpToolCallEvent(`${GET_STATE_ID}_RESUME`, "get_app_state", "running", "r2"),
|
|
169
|
+
mcpToolCallEvent(`${GET_STATE_ID}_RESUME`, "get_app_state", "completed", "r2", {
|
|
170
|
+
result: "Slack focused",
|
|
171
|
+
}),
|
|
172
|
+
// The genuinely-new gated tool: emitted running, never completes (the hook
|
|
173
|
+
// denies it and the turn is cut off at the first denial).
|
|
174
|
+
mcpToolCallEvent("tool_click_RESUME", "click", "running", "r2"),
|
|
175
|
+
// Trailing provisional narration in a NEW turn (separate run id) — the kind
|
|
176
|
+
// clearProvisionalPostDenialNarration must blank for the CURRENT turn only.
|
|
177
|
+
assistantEvent("r3", RUN2_TRAILING_NARRATION),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The denial ledger after resume holds only the freshly-gated `click`. */
|
|
182
|
+
function clickDenialLedger(): DeniedLedgerEntry[] {
|
|
183
|
+
return [{ toolName: "click", token: grantToken("click", "") }];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Local mirror of the backend's nonTerminalTranscriptRegression (Go) /
|
|
188
|
+
* transcriptRegressionReason (Java): for a non-terminal execution, an incoming
|
|
189
|
+
* transcript must neither shrink nor drop a previously-committed tool-call id.
|
|
190
|
+
* Returns the rejection reason, or undefined if the update would be accepted.
|
|
191
|
+
*/
|
|
192
|
+
function guardRejectionReason(
|
|
193
|
+
existing: AgentMessage[],
|
|
194
|
+
incoming: AgentMessage[],
|
|
195
|
+
): string | undefined {
|
|
196
|
+
if (incoming.length < existing.length) return "would shrink the message transcript";
|
|
197
|
+
const incomingIds = new Set<string>();
|
|
198
|
+
for (const m of incoming) for (const tc of m.toolCalls) if (tc.id) incomingIds.add(tc.id);
|
|
199
|
+
for (const m of existing) {
|
|
200
|
+
for (const tc of m.toolCalls) {
|
|
201
|
+
if (tc.id && !incomingIds.has(tc.id)) return "would drop a previously-committed tool call";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function allToolCalls(messages: AgentMessage[]) {
|
|
208
|
+
return messages.flatMap((m) => m.toolCalls);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
describe("Cursor HITL resume — append-only transcript", () => {
|
|
212
|
+
it("BUG (no seeding): a from-empty rebuild drops committed ids and the guard rejects it", () => {
|
|
213
|
+
const committed = persistedRunOneMessages();
|
|
214
|
+
|
|
215
|
+
// Pre-fix behavior: status.messages starts empty on resume, so the
|
|
216
|
+
// accumulator can never reconcile onto the committed calls.
|
|
217
|
+
const fromEmpty: AgentMessage[] = [];
|
|
218
|
+
const acc = new MessageAccumulator(fromEmpty, { seededSubAgents: [] });
|
|
219
|
+
for (const event of resumeEvents()) acc.processEvent(event);
|
|
220
|
+
acc.finalize();
|
|
221
|
+
|
|
222
|
+
// The committed ids are absent — exactly what trips the backend guard.
|
|
223
|
+
const ids = new Set(allToolCalls(fromEmpty).map((tc) => tc.id));
|
|
224
|
+
expect(ids.has(LIST_APPS_ID)).toBe(false);
|
|
225
|
+
expect(ids.has(GET_STATE_ID)).toBe(false);
|
|
226
|
+
|
|
227
|
+
expect(guardRejectionReason(committed, fromEmpty)).toBe(
|
|
228
|
+
"would drop a previously-committed tool call",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("FIX (seeding + identity reconciliation): the resume is a strict superset the guard accepts", async () => {
|
|
233
|
+
const committed = persistedRunOneMessages();
|
|
234
|
+
|
|
235
|
+
// Post-fix behavior: index.ts seeds status.messages from the persisted
|
|
236
|
+
// execution (cloned) before constructing the accumulator.
|
|
237
|
+
const seeded = committed.map((m) => clone(AgentMessageSchema, m));
|
|
238
|
+
const acc = new MessageAccumulator(seeded, { seededSubAgents: [] });
|
|
239
|
+
for (const event of resumeEvents()) acc.processEvent(event);
|
|
240
|
+
acc.finalize();
|
|
241
|
+
|
|
242
|
+
// Post-stream denial overlay (the activity's Phase 12): only `click` is in
|
|
243
|
+
// the ledger, so only it flips to WAITING_APPROVAL.
|
|
244
|
+
const denied = await reconcileDeniedToolCalls(seeded, clickDenialLedger());
|
|
245
|
+
const redacted = clearProvisionalPostDenialNarration(seeded, denied);
|
|
246
|
+
|
|
247
|
+
const tools = allToolCalls(seeded);
|
|
248
|
+
|
|
249
|
+
// 1. The committed ids are preserved (reconciled in place, not re-keyed).
|
|
250
|
+
const byId = new Map(tools.map((tc) => [tc.id, tc]));
|
|
251
|
+
expect(byId.has(LIST_APPS_ID)).toBe(true);
|
|
252
|
+
expect(byId.has(GET_STATE_ID)).toBe(true);
|
|
253
|
+
|
|
254
|
+
// 2. No duplicate rows: the fresh *_RESUME ids never produced new tool calls.
|
|
255
|
+
expect(tools.filter((tc) => tc.name === "list_apps")).toHaveLength(1);
|
|
256
|
+
expect(tools.filter((tc) => tc.name === "get_app_state")).toHaveLength(1);
|
|
257
|
+
expect(tools.some((tc) => tc.id === `${LIST_APPS_ID}_RESUME`)).toBe(false);
|
|
258
|
+
expect(tools.some((tc) => tc.id === `${GET_STATE_ID}_RESUME`)).toBe(false);
|
|
259
|
+
|
|
260
|
+
// 3. The approved re-runs reconciled onto the seeded calls and completed.
|
|
261
|
+
expect(byId.get(LIST_APPS_ID)!.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
262
|
+
expect(byId.get(LIST_APPS_ID)!.result).toContain("Slack");
|
|
263
|
+
expect(byId.get(GET_STATE_ID)!.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
264
|
+
|
|
265
|
+
// 4. The genuinely-new gated tool is appended and surfaced as pending.
|
|
266
|
+
const click = tools.find((tc) => tc.name === "click");
|
|
267
|
+
expect(click).toBeDefined();
|
|
268
|
+
expect(click!.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
269
|
+
|
|
270
|
+
// 5. Provisional post-denial narration is redacted for the CURRENT turn only:
|
|
271
|
+
// the trailing "Done" message is blanked, while the seeded run-1 text and
|
|
272
|
+
// the gated turn's own narration are preserved.
|
|
273
|
+
expect(redacted).toHaveLength(1);
|
|
274
|
+
const allText = seeded.map((m) => m.content).join("\n");
|
|
275
|
+
expect(allText).toContain(RUN1_AI_TEXT);
|
|
276
|
+
expect(allText).toContain(RUN2_AI_TEXT);
|
|
277
|
+
expect(allText).not.toContain(RUN2_TRAILING_NARRATION);
|
|
278
|
+
|
|
279
|
+
// 6. The decisive invariant: the seeded resume passes the backend guard.
|
|
280
|
+
expect(guardRejectionReason(committed, seeded)).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("FIX: a sub-agent's gated tool also survives resume (seeded sub-agent rows are retained)", () => {
|
|
284
|
+
// Sub-agent parity: seedCursorTranscriptFromExecution hands seeded sub-agent
|
|
285
|
+
// executions to the accumulator so they are not dropped on the rebuilt
|
|
286
|
+
// status (the accumulator owns status.subAgentExecutions).
|
|
287
|
+
const seededSub = create(SubAgentExecutionSchema, {
|
|
288
|
+
id: "sub_1",
|
|
289
|
+
name: "researcher",
|
|
290
|
+
status: SubAgentStatus.SUB_AGENT_IN_PROGRESS,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const acc = new MessageAccumulator([], { seededSubAgents: [seededSub] });
|
|
294
|
+
acc.finalize();
|
|
295
|
+
|
|
296
|
+
expect(acc.subAgentExecutions.some((s) => s.id === "sub_1")).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("FIX: the approved tools are ALLOWED by the hook on resume — not re-denied (the loop's source)", () => {
|
|
300
|
+
// The transcript-superset fix keeps the run off the watchdog, but the OTHER
|
|
301
|
+
// half of "no loop" is that the approved tools' grants actually let the hook
|
|
302
|
+
// ALLOW the re-issued calls. Drive the real bash hook with the grants the
|
|
303
|
+
// resume mints from the persisted run-1 decisions and prove the two approved
|
|
304
|
+
// MCP tools pass while the genuinely-new `click` is still gated.
|
|
305
|
+
if (!hasBash) return; // hook tests require bash; mirrored skip of the d() guard.
|
|
306
|
+
|
|
307
|
+
const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(persistedRunOneMessages());
|
|
308
|
+
const grants = buildApprovalGrants(pendingApprovals, decisions);
|
|
309
|
+
const state = buildApprovalState(new Map(), false, new Set(), grants);
|
|
310
|
+
|
|
311
|
+
const harness = setupCursorHookHarness({
|
|
312
|
+
grants: state.approvedGrants,
|
|
313
|
+
mcpPolicies: {
|
|
314
|
+
list_apps: { requiresApproval: true },
|
|
315
|
+
get_app_state: { requiresApproval: true },
|
|
316
|
+
click: { requiresApproval: true },
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Both approved tools are allowed on the resumed turn (fresh ids, same name).
|
|
321
|
+
expect(harness.decide(hookMcp("list_apps")).permission).toBe("allow");
|
|
322
|
+
expect(harness.decide(hookMcp("get_app_state")).permission).toBe("allow");
|
|
323
|
+
// The brand-new gated tool is NOT covered by any grant -> still denied.
|
|
324
|
+
expect(harness.decide(hookMcp("click")).permission).toBe("deny");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The exact "two co-pending approvals -> resume -> both allowed -> COMPLETE"
|
|
330
|
+
* scenario, on the built-in edit+shell path (the file-diff case from the bug
|
|
331
|
+
* report). This is the success terminus the production loop never reached: when
|
|
332
|
+
* every gated tool was approved and the agent has nothing new to gate, the resume
|
|
333
|
+
* must re-run both approved tools, complete, and leave ZERO tools waiting — a
|
|
334
|
+
* legitimate zero-pending because the run is TERMINAL, not because the guard
|
|
335
|
+
* dropped a pending (the watchdog's failure mode). Asserting "no WAITING_APPROVAL
|
|
336
|
+
* survives a clean completion" is what distinguishes the two zero-pending states.
|
|
337
|
+
*/
|
|
338
|
+
describe("Cursor HITL resume — two approvals then clean completion (no loop)", () => {
|
|
339
|
+
const NOTES_PATH = "/work/notes.md";
|
|
340
|
+
const WRITE_ID = "tool_write_notes";
|
|
341
|
+
const SHELL_CMD = "cat >> notes.md << 'EOF'\nappended\nEOF";
|
|
342
|
+
const SHELL_ID = "tool_append_notes";
|
|
343
|
+
|
|
344
|
+
/** Persisted run-1: an edit and a shell, both gated, both APPROVED. */
|
|
345
|
+
function committedBuiltInApprovals(): AgentMessage[] {
|
|
346
|
+
const edit = create(ToolCallSchema, {
|
|
347
|
+
id: WRITE_ID,
|
|
348
|
+
name: "edit",
|
|
349
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
350
|
+
requiresApproval: true,
|
|
351
|
+
approvalAction: ApprovalAction.APPROVE,
|
|
352
|
+
argsPreview: JSON.stringify({ path: NOTES_PATH, content: "# Notes" }),
|
|
353
|
+
});
|
|
354
|
+
const shell = create(ToolCallSchema, {
|
|
355
|
+
id: SHELL_ID,
|
|
356
|
+
name: "shell",
|
|
357
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
358
|
+
requiresApproval: true,
|
|
359
|
+
approvalAction: ApprovalAction.APPROVE,
|
|
360
|
+
argsPreview: JSON.stringify({ command: SHELL_CMD }),
|
|
361
|
+
});
|
|
362
|
+
return [
|
|
363
|
+
create(AgentMessageSchema, {
|
|
364
|
+
type: MessageType.MESSAGE_AI,
|
|
365
|
+
content: "I'll create and append to the notes file.",
|
|
366
|
+
timestamp: "2026-06-24T00:00:01.000Z",
|
|
367
|
+
toolCalls: [edit, shell],
|
|
368
|
+
}),
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Resume re-issues both approved tools (fresh ids) and they COMPLETE; no new gate. */
|
|
373
|
+
function builtInResumeEvents(): SDKMessage[] {
|
|
374
|
+
const editArgs = { path: NOTES_PATH, content: "# Notes" };
|
|
375
|
+
const shellArgs = { command: SHELL_CMD };
|
|
376
|
+
return [
|
|
377
|
+
assistantEvent("r2", "Applying the approved file changes."),
|
|
378
|
+
{
|
|
379
|
+
type: "tool_call", agent_id: "agent-1", run_id: "r2",
|
|
380
|
+
call_id: `${WRITE_ID}_RESUME`, name: "edit", status: "running", args: editArgs,
|
|
381
|
+
} as SDKMessage,
|
|
382
|
+
{
|
|
383
|
+
type: "tool_call", agent_id: "agent-1", run_id: "r2",
|
|
384
|
+
call_id: `${WRITE_ID}_RESUME`, name: "edit", status: "completed",
|
|
385
|
+
result: "wrote notes.md", args: editArgs,
|
|
386
|
+
} as SDKMessage,
|
|
387
|
+
{
|
|
388
|
+
type: "tool_call", agent_id: "agent-1", run_id: "r2",
|
|
389
|
+
call_id: `${SHELL_ID}_RESUME`, name: "shell", status: "running", args: shellArgs,
|
|
390
|
+
} as SDKMessage,
|
|
391
|
+
{
|
|
392
|
+
type: "tool_call", agent_id: "agent-1", run_id: "r2",
|
|
393
|
+
call_id: `${SHELL_ID}_RESUME`, name: "shell", status: "completed",
|
|
394
|
+
result: "appended", args: shellArgs,
|
|
395
|
+
} as SDKMessage,
|
|
396
|
+
assistantEvent("r2", "Done — the notes file is ready."),
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
it("re-runs both approved tools in place and completes with ZERO tools waiting", async () => {
|
|
401
|
+
const committed = committedBuiltInApprovals();
|
|
402
|
+
const seeded = committed.map((m) => clone(AgentMessageSchema, m));
|
|
403
|
+
|
|
404
|
+
const acc = new MessageAccumulator(seeded, { seededSubAgents: [] });
|
|
405
|
+
for (const event of builtInResumeEvents()) acc.processEvent(event);
|
|
406
|
+
acc.finalize();
|
|
407
|
+
|
|
408
|
+
// The hook allowed both (no denials this turn), so the overlay adds nothing.
|
|
409
|
+
const denied = await reconcileDeniedToolCalls(seeded, []);
|
|
410
|
+
expect(denied).toHaveLength(0);
|
|
411
|
+
|
|
412
|
+
const tools = allToolCalls(seeded);
|
|
413
|
+
const byId = new Map(tools.map((tc) => [tc.id, tc]));
|
|
414
|
+
|
|
415
|
+
// In-place reconcile: the committed ids survive and carry the completion;
|
|
416
|
+
// the fresh *_RESUME ids never spawned duplicate rows.
|
|
417
|
+
expect(tools.filter((tc) => tc.name === "edit")).toHaveLength(1);
|
|
418
|
+
expect(tools.filter((tc) => tc.name === "shell")).toHaveLength(1);
|
|
419
|
+
expect(byId.get(WRITE_ID)!.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
420
|
+
expect(byId.get(SHELL_ID)!.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
421
|
+
|
|
422
|
+
// The decisive no-loop property: nothing is left WAITING_APPROVAL, so the
|
|
423
|
+
// run can reach a terminal status. Zero pending here is CORRECT (the run is
|
|
424
|
+
// done), unlike the watchdog's zero-pending-while-WAITING failure mode.
|
|
425
|
+
expect(tools.some((tc) => tc.status === ToolCallStatus.TOOL_CALL_WAITING_APPROVAL)).toBe(false);
|
|
426
|
+
|
|
427
|
+
// And the transcript is still a strict superset the backend guard accepts.
|
|
428
|
+
expect(guardRejectionReason(committed, seeded)).toBeUndefined();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("the built-in grants actually let the hook allow both re-issues (full closure)", () => {
|
|
432
|
+
if (!hasBash) return;
|
|
433
|
+
|
|
434
|
+
const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(committedBuiltInApprovals());
|
|
435
|
+
const grants = buildApprovalGrants(pendingApprovals, decisions);
|
|
436
|
+
const state = buildApprovalState(new Map(), false, new Set(), grants);
|
|
437
|
+
|
|
438
|
+
const harness = setupCursorHookHarness({ grants: state.approvedGrants });
|
|
439
|
+
// Exactly the two approved resources are allowed on resume...
|
|
440
|
+
expect(harness.decide(hookWrite(NOTES_PATH)).permission).toBe("allow");
|
|
441
|
+
expect(harness.decide(hookShell(SHELL_CMD)).permission).toBe("allow");
|
|
442
|
+
// ...and an unrelated write/shell of the same category is still gated.
|
|
443
|
+
expect(harness.decide(hookWrite("/work/secret.env")).permission).toBe("deny");
|
|
444
|
+
expect(harness.decide(hookShell("rm -rf /")).permission).toBe("deny");
|
|
445
|
+
});
|
|
446
|
+
});
|