@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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test harness for the generated Cursor preToolUse bash hook.
|
|
3
|
+
*
|
|
4
|
+
* Runs the ACTUAL bash script the runner writes into the workspace, feeding it
|
|
5
|
+
* the REAL hook-input shapes captured from `@cursor/sdk`. It is the out-of-process
|
|
6
|
+
* deny-oracle's only honest test surface: write the runner-owned state file (with
|
|
7
|
+
* any approval grants), invoke the hook, and read back its allow/deny decision
|
|
8
|
+
* and the denial ledger.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from `__tests__/hook-script.test.ts` so it is shared by both that
|
|
11
|
+
* behavior suite and the deny-oracle adapter in the gateway Contract Test Kit
|
|
12
|
+
* (`__test-utils__/gateway-substrate.ts`) — one harness, no drift.
|
|
13
|
+
*
|
|
14
|
+
* Each harness owns a throwaway workspace under the OS temp dir and self-cleans
|
|
15
|
+
* via `onTestFinished`, so it must be called from within a running test.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { onTestFinished } from "vitest";
|
|
19
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
20
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { generateHookScript } from "../hook-script.js";
|
|
25
|
+
import { buildApprovalState, type ApprovalGrant, type McpToolPolicyEntry } from "../approval-state.js";
|
|
26
|
+
import type { MergedToolPolicy, ApprovalCategory } from "../approval-policy.js";
|
|
27
|
+
import { readCasObservations, type CasObservations } from "../cas-observations.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether `bash` is available on this machine. Hook tests must be skipped where
|
|
31
|
+
* it is not — use `const d = hasBash ? describe : describe.skip`.
|
|
32
|
+
*/
|
|
33
|
+
export const hasBash: boolean = (() => {
|
|
34
|
+
try {
|
|
35
|
+
execSync("bash -c 'exit 0'", { stdio: "ignore" });
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
export interface CursorHookHarness {
|
|
43
|
+
/** The throwaway workspace root (git repo when capture mode is on). */
|
|
44
|
+
readonly root: string;
|
|
45
|
+
/** Run the hook against a single hook-input payload and report its decision. */
|
|
46
|
+
decide(input: object): { permission: string; raw: string };
|
|
47
|
+
/**
|
|
48
|
+
* The denial ledger entries the hook has appended this turn. `input` is the
|
|
49
|
+
* base64(JSON(tool_input)) the hook captures (decode + JSON.parse to inspect).
|
|
50
|
+
*/
|
|
51
|
+
ledger(): Array<{ toolName: string; token: string; input?: string }>;
|
|
52
|
+
/** Truncate the denial ledger (a fresh turn). */
|
|
53
|
+
resetLedger(): void;
|
|
54
|
+
/**
|
|
55
|
+
* The cas-observations the hook staged this turn (captured gitignored writes +
|
|
56
|
+
* secret-blocked paths), read back through the real sidecar reader. Empty when
|
|
57
|
+
* captureIgnored is off / nothing was staged.
|
|
58
|
+
*/
|
|
59
|
+
observations(): Promise<CasObservations>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CursorHookHarnessOptions {
|
|
63
|
+
/** Pre-armed spec.auto_approve_all (the whole-run global bypass). */
|
|
64
|
+
autoApproveAll?: boolean;
|
|
65
|
+
/** Built-in categories with a run-lifetime scoped lease. */
|
|
66
|
+
leasedCategories?: ApprovalCategory[];
|
|
67
|
+
grants?: ApprovalGrant[];
|
|
68
|
+
mcpPolicies?: Record<string, McpToolPolicyEntry>;
|
|
69
|
+
/** Omit the state file to exercise the fail-closed (deny) path. */
|
|
70
|
+
noStateFile?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Process the hook treats as "the runner". Defaults to this test process,
|
|
73
|
+
* which is an ancestor of the bash child `execFileSync` spawns — so the scope
|
|
74
|
+
* guard sees the call as the runner's own agent and applies the gate. Pass a
|
|
75
|
+
* non-ancestor PID to exercise the foreign-client path (issue #173).
|
|
76
|
+
*/
|
|
77
|
+
runnerPid?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Capture mode (git workspaces): the hook ALLOWS write/edit/delete to flow,
|
|
80
|
+
* gating only gitignored paths, shell, and MCP. When set, the harness baked
|
|
81
|
+
* workspace root is the throwaway workspace so `git check-ignore` resolves.
|
|
82
|
+
*/
|
|
83
|
+
captureMode?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Initialize the throwaway workspace as a git repo with these gitignore
|
|
86
|
+
* patterns, so capture mode's `git check-ignore` has a real repo to consult.
|
|
87
|
+
*/
|
|
88
|
+
gitignored?: string[];
|
|
89
|
+
/**
|
|
90
|
+
* CAS capture of gitignored writes (deep-agent parity). When true, the hook
|
|
91
|
+
* stages a non-secret gitignored write's before-bytes into the cas-observations
|
|
92
|
+
* sidecar and ALLOWS it, and hard-blocks a secret-like gitignored write —
|
|
93
|
+
* instead of denying every gitignored write. Requires a git repo (set via
|
|
94
|
+
* {@link captureMode} or {@link gitignored}).
|
|
95
|
+
*/
|
|
96
|
+
captureIgnored?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Whether the workspace is a git tree (Slice 2c). Default true. When false the
|
|
99
|
+
* throwaway workspace is NOT git-initialized and the state's `gitWorkspace` flag
|
|
100
|
+
* is false, so the hook CAS-stages EVERY write (not only gitignored ones) and
|
|
101
|
+
* skips the git-tracked flow arm.
|
|
102
|
+
*/
|
|
103
|
+
gitWorkspace?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a throwaway workspace, write the generated hook + (optionally) the
|
|
108
|
+
* approval state file, and return a driver for the real bash hook.
|
|
109
|
+
*/
|
|
110
|
+
export function setupCursorHookHarness(opts: CursorHookHarnessOptions = {}): CursorHookHarness {
|
|
111
|
+
const ws = mkdtempSync(join(tmpdir(), "hook-script-"));
|
|
112
|
+
onTestFinished(() => rmSync(ws, { recursive: true, force: true }));
|
|
113
|
+
|
|
114
|
+
// Capture-mode tests need a real git repo so `git check-ignore` resolves. A
|
|
115
|
+
// non-git test (gitWorkspace:false) deliberately leaves the workspace un-inited
|
|
116
|
+
// so the hook's substrate matches the state's gitWorkspace flag.
|
|
117
|
+
if ((opts.captureMode || opts.gitignored) && opts.gitWorkspace !== false) {
|
|
118
|
+
execSync("git init -q", { cwd: ws });
|
|
119
|
+
if (opts.gitignored && opts.gitignored.length > 0) {
|
|
120
|
+
writeFileSync(join(ws, ".gitignore"), opts.gitignored.join("\n") + "\n", "utf-8");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dir = join(ws, ".cursor", "hooks");
|
|
125
|
+
mkdirSync(dir, { recursive: true });
|
|
126
|
+
const statePath = join(dir, "state.json");
|
|
127
|
+
const ledgerPath = join(dir, "denials.jsonl");
|
|
128
|
+
const pointerPath = join(dir, "active.json");
|
|
129
|
+
const scriptPath = join(dir, "hook.sh");
|
|
130
|
+
// The hook script is STABLE (no baked per-turn paths); it resolves the current
|
|
131
|
+
// turn's state/ledger/runner from the active-turn pointer it re-reads each call.
|
|
132
|
+
// The workspace root is baked so capture mode's gitignore check finds the repo.
|
|
133
|
+
writeFileSync(scriptPath, generateHookScript(pointerPath, ws), "utf-8");
|
|
134
|
+
writeFileSync(
|
|
135
|
+
pointerPath,
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
stateFile: statePath,
|
|
138
|
+
ledgerFile: ledgerPath,
|
|
139
|
+
runnerPid: opts.runnerPid ?? process.pid,
|
|
140
|
+
}),
|
|
141
|
+
"utf-8",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!opts.noStateFile) {
|
|
145
|
+
const policies = new Map<string, MergedToolPolicy>(
|
|
146
|
+
Object.entries(opts.mcpPolicies ?? {}).map(([name, p]) => [
|
|
147
|
+
`srv/${name}`,
|
|
148
|
+
{
|
|
149
|
+
toolName: name,
|
|
150
|
+
mcpServerSlug: "srv",
|
|
151
|
+
requiresApproval: p.requiresApproval,
|
|
152
|
+
approvalMessage: p.message ?? "",
|
|
153
|
+
source: "classifier_default" as const,
|
|
154
|
+
},
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
const state = buildApprovalState(
|
|
158
|
+
policies,
|
|
159
|
+
opts.autoApproveAll ?? false,
|
|
160
|
+
new Set(opts.leasedCategories ?? []),
|
|
161
|
+
opts.grants,
|
|
162
|
+
opts.captureMode ?? false,
|
|
163
|
+
opts.captureIgnored ?? false,
|
|
164
|
+
opts.gitWorkspace ?? true,
|
|
165
|
+
);
|
|
166
|
+
writeFileSync(statePath, JSON.stringify(state), "utf-8");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
root: ws,
|
|
171
|
+
decide(input: object) {
|
|
172
|
+
const raw = execFileSync("bash", [scriptPath], { input: JSON.stringify(input) }).toString();
|
|
173
|
+
const permission = raw.includes('"permission":"deny"')
|
|
174
|
+
? "deny"
|
|
175
|
+
: raw.includes('"permission":"allow"')
|
|
176
|
+
? "allow"
|
|
177
|
+
: "?";
|
|
178
|
+
return { permission, raw };
|
|
179
|
+
},
|
|
180
|
+
ledger() {
|
|
181
|
+
if (!existsSync(ledgerPath)) return [];
|
|
182
|
+
return readFileSync(ledgerPath, "utf-8")
|
|
183
|
+
.split("\n")
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.map((l) => JSON.parse(l));
|
|
186
|
+
},
|
|
187
|
+
resetLedger() {
|
|
188
|
+
writeFileSync(ledgerPath, "", "utf-8");
|
|
189
|
+
},
|
|
190
|
+
// The sidecar lives beside the state/ledger files (its `dir` is the harness's
|
|
191
|
+
// hitlDir analog), exactly as the runner derives it from dirname(STATE_FILE).
|
|
192
|
+
observations() {
|
|
193
|
+
return readCasObservations(dir);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Real preToolUse hook-input shapes (PascalCase name, file_path/command in
|
|
199
|
+
// tool_input). These omit hook_event_name on purpose: a payload with no event
|
|
200
|
+
// must still take the built-in arm (the script only diverts to the MCP arm on an
|
|
201
|
+
// explicit beforeMCPExecution).
|
|
202
|
+
export const hookWrite = (filePath: string, content = "x") => ({ tool_name: "Write", tool_input: { file_path: filePath, content } });
|
|
203
|
+
export const hookEdit = (filePath: string, oldString = "a", newString = "b") => ({ tool_name: "StrReplace", tool_input: { file_path: filePath, old_string: oldString, new_string: newString } });
|
|
204
|
+
export const hookShell = (command: string) => ({ tool_name: "Shell", tool_input: { command, cwd: "/x", timeout: 30000 } });
|
|
205
|
+
export const hookDelete = (filePath: string) => ({ tool_name: "Delete", tool_input: { file_path: filePath } });
|
|
206
|
+
export const hookRead = (filePath: string) => ({ tool_name: "Read", tool_input: { file_path: filePath } });
|
|
207
|
+
|
|
208
|
+
// Real beforeMCPExecution shape (captured live): bare tool_name, tool_input as a
|
|
209
|
+
// JSON STRING, server identity, and the hook_event_name discriminator.
|
|
210
|
+
export const hookMcp = (name: string, input: Record<string, unknown> = {}) => ({
|
|
211
|
+
tool_name: name,
|
|
212
|
+
tool_input: JSON.stringify(input),
|
|
213
|
+
mcp_server_name: "srv",
|
|
214
|
+
command: "npx -y srv mcp",
|
|
215
|
+
hook_event_name: "beforeMCPExecution",
|
|
216
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor deny-oracle adapter for the HITL gateway Contract Test Kit.
|
|
3
|
+
*
|
|
4
|
+
* Drives the REAL out-of-process gateway: the runner writes the approval state
|
|
5
|
+
* file (with any grants), then the generated bash preToolUse hook makes the
|
|
6
|
+
* allow/deny decision for the tool the agent would run inside cursor-agent. There
|
|
7
|
+
* is no in-process execution to observe, so `executed` means "the hook allowed
|
|
8
|
+
* it" and the substrate cannot count executions (`observesExecution: false`).
|
|
9
|
+
*
|
|
10
|
+
* Cross-taxonomy by construction: grants are minted from the STREAM-side identity
|
|
11
|
+
* (`edit`/`shell`/`delete`), while the hook is fed the HOOK-side payload
|
|
12
|
+
* (`Write`/`Shell`/`Delete`). So every approve drive exercises the category
|
|
13
|
+
* collapse that lets a stream-minted grant match a hook-named call. The grant
|
|
14
|
+
* token binds the exact resource, so this substrate enforces lease isolation
|
|
15
|
+
* (`enforcesExactResource: true`) and implements `authorizeAfterGrant`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
setupCursorHookHarness,
|
|
20
|
+
hasBash,
|
|
21
|
+
hookWrite,
|
|
22
|
+
hookShell,
|
|
23
|
+
hookDelete,
|
|
24
|
+
hookRead,
|
|
25
|
+
hookMcp,
|
|
26
|
+
} from "./cursor-hook-harness.js";
|
|
27
|
+
import { toolIdentity, type ApprovalGrant } from "../approval-state.js";
|
|
28
|
+
import { contentDigest } from "../../../shared/file-tools.js";
|
|
29
|
+
import type { ApprovalCategory } from "../approval-policy.js";
|
|
30
|
+
import type {
|
|
31
|
+
GatewayDecision,
|
|
32
|
+
GatewayOutcome,
|
|
33
|
+
GatewaySubstrate,
|
|
34
|
+
ProposedAction,
|
|
35
|
+
} from "../../../__test-utils__/approval-contract/types.js";
|
|
36
|
+
|
|
37
|
+
/** Build the real preToolUse hook-input payload for an abstract action. */
|
|
38
|
+
function hookInputFor(action: ProposedAction): object {
|
|
39
|
+
switch (action.kind) {
|
|
40
|
+
case "write":
|
|
41
|
+
return hookWrite(action.resource, action.content ?? "x");
|
|
42
|
+
case "shell":
|
|
43
|
+
return hookShell(action.resource);
|
|
44
|
+
case "delete":
|
|
45
|
+
return hookDelete(action.resource);
|
|
46
|
+
case "read":
|
|
47
|
+
return hookRead(action.resource);
|
|
48
|
+
case "mcp":
|
|
49
|
+
return hookMcp(action.mcpToolName ?? "mcp_tool");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Stream-side (SDK) tool name per gated category — deliberately the OTHER
|
|
54
|
+
// taxonomy from the hook input, so a grant minted here matches a hook-named call
|
|
55
|
+
// only via the canonical category, not the raw name.
|
|
56
|
+
const STREAM_NAME: Record<"write" | "shell" | "delete", string> = {
|
|
57
|
+
write: "edit",
|
|
58
|
+
shell: "shell",
|
|
59
|
+
delete: "delete",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Mint the approval grant for an action using its stream-side identity. Only the
|
|
64
|
+
* gated built-in categories are ever granted in the contract (read is never
|
|
65
|
+
* gated; the MCP cases under test are auto-approved, needing no grant).
|
|
66
|
+
*/
|
|
67
|
+
function grantFor(action: ProposedAction): ApprovalGrant {
|
|
68
|
+
if (action.kind === "read" || action.kind === "mcp") {
|
|
69
|
+
throw new Error(`grantFor: ${action.kind} actions are not granted in the contract`);
|
|
70
|
+
}
|
|
71
|
+
const streamName = STREAM_NAME[action.kind];
|
|
72
|
+
// Mirror hookInputFor: a write carries whole-file content (so its digest is
|
|
73
|
+
// content-exact and matches the hook's), a delete carries only a path (no
|
|
74
|
+
// content), and a shell carries its command (the salient is already exact).
|
|
75
|
+
const args: Record<string, unknown> =
|
|
76
|
+
action.kind === "shell"
|
|
77
|
+
? { command: action.resource }
|
|
78
|
+
: action.kind === "delete"
|
|
79
|
+
? { path: action.resource }
|
|
80
|
+
: { path: action.resource, content: action.content ?? "x" };
|
|
81
|
+
const identity = toolIdentity(streamName, "", args);
|
|
82
|
+
return {
|
|
83
|
+
toolName: streamName,
|
|
84
|
+
mcpServerSlug: "",
|
|
85
|
+
key: identity.key,
|
|
86
|
+
salient: identity.salient,
|
|
87
|
+
// Content-exact for a write (the sibling-isolation probe), coarse otherwise —
|
|
88
|
+
// matching how the runner grants from the authoritative captured args.
|
|
89
|
+
contentDigest: contentDigest(args),
|
|
90
|
+
sourceToolCallId: `consent-${action.kind}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function decide(grants: ApprovalGrant[], action: ProposedAction): Promise<GatewayOutcome> {
|
|
95
|
+
// MCP cases under test are auto-approved: no policy entry, so the hook allows
|
|
96
|
+
// them. Built-ins are gated by the script's baked-in category set, not state.
|
|
97
|
+
const harness = setupCursorHookHarness({ grants });
|
|
98
|
+
const { permission } = harness.decide(hookInputFor(action));
|
|
99
|
+
const executed = permission === "allow";
|
|
100
|
+
return { executed, gated: !executed };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A gated built-in's contract kind IS its approval category (write/shell/delete),
|
|
104
|
+
// so it doubles as the leasedCategories entry the hook reads.
|
|
105
|
+
function leaseCategoryOf(action: ProposedAction): ApprovalCategory {
|
|
106
|
+
if (action.kind === "read" || action.kind === "mcp") {
|
|
107
|
+
throw new Error(`leaseCategoryOf: ${action.kind} is not a leasable built-in class`);
|
|
108
|
+
}
|
|
109
|
+
return action.kind;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createCursorSubstrate(): GatewaySubstrate {
|
|
113
|
+
return {
|
|
114
|
+
name: "cursor",
|
|
115
|
+
available: hasBash,
|
|
116
|
+
capabilities: {
|
|
117
|
+
observesExecution: false,
|
|
118
|
+
enforcesExactResource: true,
|
|
119
|
+
// The grant binds (category, path, contentDigest), so a DIFFERENT edit to
|
|
120
|
+
// the same file re-gates — the sibling-hole fix.
|
|
121
|
+
enforcesExactContent: true,
|
|
122
|
+
appliesRunLifetimeLease: true,
|
|
123
|
+
// The Cursor hook's deny decision does not carry approval_policy_source;
|
|
124
|
+
// provenance is projected at message-reconstruction time (message-translator)
|
|
125
|
+
// and asserted by the corpus + resolveApprovalProvenance suites instead.
|
|
126
|
+
surfacesGatePolicySource: false,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async authorize(action: ProposedAction, decision: GatewayDecision): Promise<GatewayOutcome> {
|
|
130
|
+
const grants = decision === "approve" ? [grantFor(action)] : [];
|
|
131
|
+
return decide(grants, action);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async authorizeAfterGrant(granted: ProposedAction, probe: ProposedAction): Promise<GatewayOutcome> {
|
|
135
|
+
return decide([grantFor(granted)], probe);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async authorizeUnderClassLease(leased: ProposedAction, probe: ProposedAction): Promise<GatewayOutcome> {
|
|
139
|
+
// A run-lifetime category lease is carried in the state file (no per-call
|
|
140
|
+
// grant), exactly as the runner writes it after an APPROVE_ALL; the hook
|
|
141
|
+
// allows a probe iff its category is leased.
|
|
142
|
+
const harness = setupCursorHookHarness({ leasedCategories: [leaseCategoryOf(leased)] });
|
|
143
|
+
const { permission } = harness.decide(hookInputFor(probe));
|
|
144
|
+
const executed = permission === "allow";
|
|
145
|
+
return { executed, gated: !executed };
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @regression file-hitl-phase0 — pins file-edit HITL fix #6 (see _projects/2026-06/20260630.01.file-change-hitl-redesign/tasks/T01_3_regression-manifest.md)
|
|
3
|
+
*
|
|
2
4
|
* Unit tests for the Cursor-harness HITL approval gate logic.
|
|
3
5
|
*
|
|
4
6
|
* The crux this suite guards: the Cursor preToolUse hook and the SDK event
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { describe, it, expect } from "vitest";
|
|
16
|
-
import { create } from "@bufbuild/protobuf";
|
|
18
|
+
import { create, type MessageInitShape } from "@bufbuild/protobuf";
|
|
17
19
|
import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
18
20
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
19
21
|
import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
@@ -35,7 +37,7 @@ import {
|
|
|
35
37
|
} from "../approval-state.js";
|
|
36
38
|
import { buildReinvocationPrompt } from "../prompt-builder.js";
|
|
37
39
|
|
|
38
|
-
function pending(overrides:
|
|
40
|
+
function pending(overrides: MessageInitShape<typeof PendingApprovalSchema>): PendingApproval {
|
|
39
41
|
return create(PendingApprovalSchema, {
|
|
40
42
|
toolCallId: "call-1",
|
|
41
43
|
toolName: "edit",
|
|
@@ -163,7 +165,17 @@ describe("buildApprovalGrants", () => {
|
|
|
163
165
|
[pending({ toolCallId: "c1", toolName: "edit", argsPreview: JSON.stringify({ path: "/x/gated.txt" }) })],
|
|
164
166
|
new Map([["c1", ApprovalAction.APPROVE]]),
|
|
165
167
|
);
|
|
166
|
-
expect(grants).toEqual([{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt" }]);
|
|
168
|
+
expect(grants).toEqual([{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt", contentDigest: "", sourceToolCallId: "c1" }]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("binds an approved edit's grant to its content digest (sibling-isolation)", () => {
|
|
172
|
+
const digest = "abc123";
|
|
173
|
+
const grants = buildApprovalGrants(
|
|
174
|
+
[pending({ toolCallId: "c1", toolName: "edit", argsPreview: JSON.stringify({ path: "/x/gated.txt" }) })],
|
|
175
|
+
new Map([["c1", ApprovalAction.APPROVE]]),
|
|
176
|
+
new Map([["c1", digest]]),
|
|
177
|
+
);
|
|
178
|
+
expect(grants).toEqual([{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt", contentDigest: digest, sourceToolCallId: "c1" }]);
|
|
167
179
|
});
|
|
168
180
|
|
|
169
181
|
it("creates a name-only grant for an approved MCP tool", () => {
|
|
@@ -171,7 +183,7 @@ describe("buildApprovalGrants", () => {
|
|
|
171
183
|
[pending({ toolCallId: "c1", toolName: "apply_x", mcpServerSlug: "planton", argsPreview: JSON.stringify({ path: "ignored" }) })],
|
|
172
184
|
new Map([["c1", ApprovalAction.APPROVE]]),
|
|
173
185
|
);
|
|
174
|
-
expect(grants).toEqual([{ toolName: "apply_x", mcpServerSlug: "planton", key: "apply_x", salient: "" }]);
|
|
186
|
+
expect(grants).toEqual([{ toolName: "apply_x", mcpServerSlug: "planton", key: "apply_x", salient: "", contentDigest: "", sourceToolCallId: "c1" }]);
|
|
175
187
|
});
|
|
176
188
|
|
|
177
189
|
it("ignores skipped and rejected approvals", () => {
|
|
@@ -191,23 +203,43 @@ describe("buildApprovalGrants", () => {
|
|
|
191
203
|
|
|
192
204
|
describe("buildApprovalState", () => {
|
|
193
205
|
const mcpPolicies = new Map<string, MergedToolPolicy>([
|
|
194
|
-
["planton/apply_x", { toolName: "apply_x", mcpServerSlug: "planton", requiresApproval: true, approvalMessage: "Apply X" }],
|
|
206
|
+
["planton/apply_x", { toolName: "apply_x", mcpServerSlug: "planton", requiresApproval: true, approvalMessage: "Apply X", source: "classifier_default" }],
|
|
195
207
|
]);
|
|
196
208
|
|
|
197
209
|
it("carries MCP policies and exact-resource grant tokens (gated set is baked into the hook, not the state)", () => {
|
|
198
|
-
const grants = [{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt" }];
|
|
199
|
-
const state = buildApprovalState(mcpPolicies, false, grants);
|
|
210
|
+
const grants = [{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt", contentDigest: "", sourceToolCallId: "consent-1" }];
|
|
211
|
+
const state = buildApprovalState(mcpPolicies, false, new Set(), grants);
|
|
200
212
|
|
|
201
213
|
expect(state.autoApproveAll).toBe(false);
|
|
214
|
+
expect(state.leasedCategories).toEqual([]);
|
|
202
215
|
expect(state.mcpToolPolicies.apply_x).toEqual({ requiresApproval: true, message: "Apply X" });
|
|
216
|
+
// No digest -> the primary token degrades to the coarse grant token.
|
|
203
217
|
expect(state.approvedGrantTokens).toEqual([grantToken("write", "/x/gated.txt")]);
|
|
204
218
|
// builtInGatedList is no longer part of the state file (baked into the hook).
|
|
205
|
-
expect((state as Record<string, unknown>).builtInGatedList).toBeUndefined();
|
|
219
|
+
expect((state as unknown as Record<string, unknown>).builtInGatedList).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("emits the CONTENT token when a grant carries a content digest", () => {
|
|
223
|
+
const grants = [{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt", contentDigest: "deadbeef", sourceToolCallId: "consent-1" }];
|
|
224
|
+
const state = buildApprovalState(mcpPolicies, false, new Set(), grants);
|
|
225
|
+
// A content-identified grant authorizes base64(key\nsalient\ndigest), so a
|
|
226
|
+
// DIFFERENT edit to the same path (different digest) does not match.
|
|
227
|
+
expect(state.approvedGrantTokens).toEqual([
|
|
228
|
+
Buffer.from("write\n/x/gated.txt\ndeadbeef", "utf-8").toString("base64"),
|
|
229
|
+
]);
|
|
230
|
+
expect(state.approvedGrantTokens[0]).not.toBe(grantToken("write", "/x/gated.txt"));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("writes leasedCategories for run-lifetime scoped leases", () => {
|
|
234
|
+
const state = buildApprovalState(mcpPolicies, false, new Set(["shell", "write"]));
|
|
235
|
+
expect(state.autoApproveAll).toBe(false);
|
|
236
|
+
expect(state.leasedCategories.sort()).toEqual(["shell", "write"]);
|
|
206
237
|
});
|
|
207
238
|
|
|
208
239
|
it("defaults grants to empty when none provided", () => {
|
|
209
|
-
const state = buildApprovalState(mcpPolicies, true);
|
|
240
|
+
const state = buildApprovalState(mcpPolicies, true, new Set());
|
|
210
241
|
expect(state.autoApproveAll).toBe(true);
|
|
242
|
+
expect(state.leasedCategories).toEqual([]);
|
|
211
243
|
expect(state.approvedGrants).toEqual([]);
|
|
212
244
|
expect(state.approvedGrantTokens).toEqual([]);
|
|
213
245
|
});
|