@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
|
@@ -54,23 +54,45 @@
|
|
|
54
54
|
* recorded here correlates to the streamed tool call, and an approval grant
|
|
55
55
|
* matches the agent's re-attempt on reinvocation.
|
|
56
56
|
*
|
|
57
|
+
* Content-exact identity (the sibling-hole fix): for a file edit the coarse
|
|
58
|
+
* (category, salient) is not enough — approving one edit to a file must not let
|
|
59
|
+
* a DIFFERENT edit to the SAME file ride through. So the hook ALSO computes a
|
|
60
|
+
* CONTENT token `base64(category \n salient \n contentDigest)`, where
|
|
61
|
+
* contentDigest is a sha256 over the edit content (mirror of file-tools.ts
|
|
62
|
+
* contentDigest; see buildContentDigestScript). It allows a built-in when EITHER
|
|
63
|
+
* the content token (a file edit approved with this exact content) OR the coarse
|
|
64
|
+
* token (shell/delete, or a content-less degrade) is granted, and records the
|
|
65
|
+
* content token as the denial identity. The runner grants the content token when
|
|
66
|
+
* it has the approved content (the persisted approval_content_digest), so a
|
|
67
|
+
* sibling edit re-gates; it degrades to the coarse grant only when the content is
|
|
68
|
+
* unrecoverable.
|
|
69
|
+
*
|
|
57
70
|
* Policy evaluation order (first match wins). The model is "gate the dangerous
|
|
58
71
|
* set, allow the rest" — matching the native harness and avoiding denial of
|
|
59
72
|
* auto-approved MCP tools (which are absent from mcpToolPolicies):
|
|
60
73
|
* 0. Scope guard: not the runner's own agent → allow (never touch the ledger)
|
|
61
|
-
* 1. Missing state file → deny (fail-closed); autoApproveAll
|
|
74
|
+
* 1. Missing state file → deny (fail-closed); autoApproveAll (the pre-armed
|
|
75
|
+
* spec.auto_approve_all global bypass) → allow
|
|
62
76
|
* 2. beforeMCPExecution event → MCP tool present in mcpToolPolicies
|
|
63
77
|
* (require-approval):
|
|
64
78
|
* a. name token in approvedGrantTokens → allow (reinvocation grant)
|
|
65
79
|
* b. otherwise → record denial, deny
|
|
66
|
-
* (auto-approved / unlisted MCP tools fall through → allow
|
|
80
|
+
* (auto-approved / unlisted MCP tools fall through → allow; a server-scoped
|
|
81
|
+
* lease drops the server's tools from mcpToolPolicies, so they fall through)
|
|
67
82
|
* 3. preToolUse event → gated built-in (category non-empty):
|
|
68
83
|
* a. identity token in approvedGrantTokens → allow (reinvocation grant)
|
|
69
|
-
* b.
|
|
84
|
+
* b. category in leasedCategories → allow (run-lifetime scoped lease)
|
|
85
|
+
* c. otherwise → record denial, deny
|
|
70
86
|
* (read-only / ungated built-ins fall through → allow)
|
|
71
87
|
*/
|
|
72
88
|
|
|
73
89
|
import { SALIENT_ARG_FIELDS, getBuiltInGatedCategories } from "./approval-policy.js";
|
|
90
|
+
import {
|
|
91
|
+
EDIT_OLD_FIELDS,
|
|
92
|
+
EDIT_NEW_FIELDS,
|
|
93
|
+
WRITE_CONTENT_FIELDS,
|
|
94
|
+
} from "../../shared/file-tools.js";
|
|
95
|
+
import { buildObservationStagingScript, CAS_OBSERVATIONS_DIRNAME } from "./cas-observations.js";
|
|
74
96
|
|
|
75
97
|
// Shown to the model when the gate denies a tool call. It must NOT teach the
|
|
76
98
|
// model to ask for permission in prose or to "stop and wait" — that framing
|
|
@@ -83,9 +105,24 @@ import { SALIENT_ARG_FIELDS, getBuiltInGatedCategories } from "./approval-policy
|
|
|
83
105
|
// quotes, apostrophes, or backslashes.
|
|
84
106
|
const APPROVAL_REQUIRED_AGENT_MESSAGE =
|
|
85
107
|
"This action has been submitted to the user for approval automatically; you " +
|
|
86
|
-
"do not need to ask for permission.
|
|
87
|
-
"
|
|
88
|
-
"
|
|
108
|
+
"do not need to ask for permission. This is the platform approval gate working " +
|
|
109
|
+
"as intended — it is not an error and not a Cursor misconfiguration, so never " +
|
|
110
|
+
"tell the user to change Cursor settings or enable hooks. Do not retry it or " +
|
|
111
|
+
"attempt a workaround for this action. The platform will resume you " +
|
|
112
|
+
"automatically after the user responds — continue with the rest of the task.";
|
|
113
|
+
|
|
114
|
+
// Shown to the model when a secret-like gitignored write is hard-blocked (DD-E /
|
|
115
|
+
// DD-18). Unlike APPROVAL_REQUIRED_AGENT_MESSAGE, this must NOT promise a resume:
|
|
116
|
+
// the write is discarded and never captured for review, so the model must move on
|
|
117
|
+
// rather than wait or retry. Same embedding constraint (single-quoted bash echo
|
|
118
|
+
// of a JSON object): no double quotes, apostrophes, or backslashes.
|
|
119
|
+
const SECRET_BLOCKED_AGENT_MESSAGE =
|
|
120
|
+
"This file was blocked for security because its path matches a secret-like " +
|
|
121
|
+
"pattern Stigmer will not capture for review. Nothing was written. This is the " +
|
|
122
|
+
"platform safety gate working as intended — it is not an error and not a Cursor " +
|
|
123
|
+
"misconfiguration, so never tell the user to change Cursor settings or enable " +
|
|
124
|
+
"hooks. Do not retry this write or attempt a workaround; the write will not be " +
|
|
125
|
+
"applied. Continue with the rest of the task.";
|
|
89
126
|
|
|
90
127
|
/**
|
|
91
128
|
* Build the bash `case` arms that map an incoming hook `tool_name` to its
|
|
@@ -107,20 +144,53 @@ function buildCategoryCaseArms(): string {
|
|
|
107
144
|
return arms.join("\n");
|
|
108
145
|
}
|
|
109
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Build the inline content-digest extractor — a BYTE-IDENTICAL MIRROR of
|
|
149
|
+
* {@link file://../../shared/file-tools.ts} `contentDigest()`.
|
|
150
|
+
*
|
|
151
|
+
* Computes the same `sha256(JSON.stringify(["w", content]))` /
|
|
152
|
+
* `sha256(JSON.stringify(["e", old, new]))` from the parsed tool_input `a`,
|
|
153
|
+
* using the SAME union field lists injected from file-tools.ts (so the
|
|
154
|
+
* file_path/path & content/contents cross-layer name divergence is normalized
|
|
155
|
+
* identically) and the SAME Node binary as the runner — so the hook-side and
|
|
156
|
+
* runner-side digests agree. Any change to the format here or in
|
|
157
|
+
* file-tools.ts MUST be mirrored in the other. Empty (`dig===""`) for a tool
|
|
158
|
+
* with no edit content (shell/delete/read/MCP).
|
|
159
|
+
*
|
|
160
|
+
* Authored as part of a single-quoted bash string, so the JS must not contain
|
|
161
|
+
* single quotes (JSON.stringify emits double quotes).
|
|
162
|
+
*/
|
|
163
|
+
function buildContentDigestScript(): string {
|
|
164
|
+
const wc = JSON.stringify(WRITE_CONTENT_FIELDS);
|
|
165
|
+
const eo = JSON.stringify(EDIT_OLD_FIELDS);
|
|
166
|
+
const en = JSON.stringify(EDIT_NEW_FIELDS);
|
|
167
|
+
return [
|
|
168
|
+
`const pick=(fl)=>{for(const f of fl){const v=a[f];if(typeof v==="string")return v;}return null;};`,
|
|
169
|
+
`const sha=(x)=>require("crypto").createHash("sha256").update(x,"utf8").digest("hex");`,
|
|
170
|
+
`let dig="";`,
|
|
171
|
+
`const _wc=pick(${wc});`,
|
|
172
|
+
`if(_wc!==null){dig=sha(JSON.stringify(["w",_wc]));}`,
|
|
173
|
+
`else{const _o=pick(${eo}),_n=pick(${en});if(_o!==null||_n!==null){dig=sha(JSON.stringify(["e",_o===null?"":_o,_n===null?"":_n]));}}`,
|
|
174
|
+
].join("");
|
|
175
|
+
}
|
|
176
|
+
|
|
110
177
|
/**
|
|
111
178
|
* Build the inline Node.js identity extractor embedded in the hook script.
|
|
112
179
|
*
|
|
113
180
|
* Parses the hook's stdin JSON properly (the bash fallback's grep truncates
|
|
114
|
-
* string values at the first escaped quote) and emits
|
|
115
|
-
* canonical category, identity token, MCP name-token,
|
|
116
|
-
* event discriminator: `preToolUse` for built-ins, `beforeMCPExecution`
|
|
117
|
-
*
|
|
118
|
-
* approval
|
|
181
|
+
* string values at the first escaped quote) and emits SEVEN lines: tool_name,
|
|
182
|
+
* canonical category, coarse identity token, MCP name-token, hook_event_name
|
|
183
|
+
* (the event discriminator: `preToolUse` for built-ins, `beforeMCPExecution`
|
|
184
|
+
* for MCP), base64(JSON(tool_input)) — the authoritative pre-execution args the
|
|
185
|
+
* runner overlays onto the gated tool call for the approval preview — and the
|
|
186
|
+
* CONTENT token (base64(category \n salient \n contentDigest), empty when the
|
|
187
|
+
* tool has no edit content). The token encodings must stay byte-identical to
|
|
188
|
+
* grantToken()/contentToken() in approval-state.ts.
|
|
119
189
|
*
|
|
120
190
|
* Authored as a single-quoted bash string, so the JS must not contain single
|
|
121
|
-
* quotes. The category map
|
|
122
|
-
* approval-policy.ts — the same source the runner
|
|
123
|
-
* never disagree.
|
|
191
|
+
* quotes. The category map, salient field list, and edit/content field lists are
|
|
192
|
+
* baked from approval-policy.ts / file-tools.ts — the same source the runner
|
|
193
|
+
* uses — so the two sides can never disagree.
|
|
124
194
|
*/
|
|
125
195
|
function buildNodeIdentityScript(): string {
|
|
126
196
|
const categoryMap: Record<string, string> = {};
|
|
@@ -133,51 +203,79 @@ function buildNodeIdentityScript(): string {
|
|
|
133
203
|
`const t=JSON.parse(require("fs").readFileSync(0,"utf8"));`,
|
|
134
204
|
`const name=typeof t.tool_name==="string"?t.tool_name:"";`,
|
|
135
205
|
`const cat=(${categories})[name]||"";`,
|
|
136
|
-
|
|
206
|
+
// tool_input is an object for built-ins (preToolUse) but a JSON STRING for
|
|
207
|
+
// MCP tools (beforeMCPExecution). Parse the string form so the captured
|
|
208
|
+
// input is the same object shape on both paths.
|
|
209
|
+
`let a={};`,
|
|
210
|
+
`if(t.tool_input&&typeof t.tool_input==="object"){a=t.tool_input;}`,
|
|
211
|
+
`else if(typeof t.tool_input==="string"){try{const p=JSON.parse(t.tool_input);if(p&&typeof p==="object")a=p;}catch(e){}}`,
|
|
137
212
|
`let s="";`,
|
|
138
213
|
`for(const f of ${fields}){const v=a[f];if(typeof v==="string"&&v){s=v;break;}}`,
|
|
139
214
|
`const b=(x)=>Buffer.from(x,"utf8").toString("base64");`,
|
|
215
|
+
// Content digest of the edit (mirror of file-tools.ts contentDigest); `dig`
|
|
216
|
+
// is "" for a non-edit tool, in which case the content token (line 7) is "".
|
|
217
|
+
buildContentDigestScript(),
|
|
140
218
|
`const ev=typeof t.hook_event_name==="string"?t.hook_event_name:"";`,
|
|
141
|
-
|
|
219
|
+
// Line 6 is base64(JSON(tool_input)): the AUTHORITATIVE pre-execution args
|
|
220
|
+
// the runner overlays onto the gated tool call so the approval card can show
|
|
221
|
+
// the proposed change before the user approves. Base64 keeps the bash side
|
|
222
|
+
// free of quoting/escaping concerns even for large multi-line file content.
|
|
223
|
+
// Line 7 is the CONTENT token (empty when no digest) — the exact-identity
|
|
224
|
+
// grant the runner authorizes for a file edit. Line 8 is base64(salient) —
|
|
225
|
+
// the raw resource value (file path / command) capture mode needs to run
|
|
226
|
+
// `git check-ignore` on a file path; base64 keeps newlines/quotes out of the
|
|
227
|
+
// line-oriented bash parse.
|
|
228
|
+
`process.stdout.write(name+"\\n"+cat+"\\n"+b(cat+"\\n"+s)+"\\n"+b(name+"\\n")+"\\n"+ev+"\\n"+b(JSON.stringify(a))+"\\n"+(dig?b(cat+"\\n"+s+"\\n"+dig):"")+"\\n"+b(s));`,
|
|
142
229
|
].join("");
|
|
143
230
|
}
|
|
144
231
|
|
|
145
232
|
/**
|
|
146
|
-
* Generates the bash hook script content.
|
|
233
|
+
* Generates the STABLE bash hook script content.
|
|
234
|
+
*
|
|
235
|
+
* The script is STABLE across executions in a runner process — its only inputs
|
|
236
|
+
* are the absolute path of the active-turn pointer (and the runner's Node
|
|
237
|
+
* binary), both constant for a given workspace. This is deliberate and
|
|
238
|
+
* load-bearing: the Cursor SDK loads `<workspace>/.cursor/hooks.json` (the hook
|
|
239
|
+
* script PATH) ONCE per runner process and caches it, ignoring later
|
|
240
|
+
* per-execution rewrites. A per-session script with per-session baked paths
|
|
241
|
+
* therefore gets cached at the FIRST execution and reused for every later one,
|
|
242
|
+
* recording their denials into the FIRST session's ledger while each later runner
|
|
243
|
+
* reads its own (empty) ledger and silently completes — the no-approval-button /
|
|
244
|
+
* "execution completed" regression. Keeping the script stable and resolving the
|
|
245
|
+
* CURRENT turn's artifacts from the pointer (which bash re-reads every
|
|
246
|
+
* invocation; see {@link ActiveTurnPointer}/writeActiveTurnPointer) makes a
|
|
247
|
+
* long-lived multi-session runner correct.
|
|
147
248
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* generation time from approval-policy.ts.
|
|
249
|
+
* From the pointer the script reads the current turn's approval-state file (the
|
|
250
|
+
* single source of truth for the dynamic inputs: autoApproveAll, leasedCategories,
|
|
251
|
+
* mcpToolPolicies, approvedGrantTokens), denial ledger, and runner PID. The
|
|
252
|
+
* static policy (which built-ins are gated, their categories, the salient arg
|
|
253
|
+
* fields) is baked at generation time from approval-policy.ts.
|
|
154
254
|
*
|
|
155
255
|
* The identity token encoding (`base64(key \n salient)`) must stay byte-identical
|
|
156
256
|
* to grantToken() in approval-state.ts.
|
|
157
257
|
*
|
|
158
258
|
* Scope guard (the crux of issue #173): the Cursor SDK loads project hooks from
|
|
159
|
-
* `<workspace>/.cursor/hooks.json`,
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* invocation
|
|
259
|
+
* `<workspace>/.cursor/hooks.json`, the SAME per-repo surface every Cursor client
|
|
260
|
+
* reads. When a session runs against the user's real repo, the user's own
|
|
261
|
+
* interactive Cursor IDE would otherwise load and run this hook too — gating the
|
|
262
|
+
* IDE, polluting the denial ledger, and (in multi-root windows) failing closed.
|
|
263
|
+
* We make the gate apply ONLY to the runner's own agent by checking, on every
|
|
264
|
+
* invocation, whether the runner PID (FROM THE POINTER) is an ancestor of the
|
|
265
|
+
* hook process. The SDK runs hooks in-process via child_process, so the runner's
|
|
266
|
+
* own agent (and its delegated sub-agents) spawn the hook as a descendant of the
|
|
267
|
+
* runner; any other Cursor client spawns it under a different process tree. A
|
|
268
|
+
* non-descendant invocation is allowed immediately and never touches the ledger.
|
|
269
|
+
* Combined with pointer teardown, a leftover hooks.json is self-neutralizing:
|
|
270
|
+
* once the turn ends (pointer removed) or the runner exits (PID dead), no
|
|
271
|
+
* invocation gates, so the gate is inert.
|
|
172
272
|
*/
|
|
173
|
-
export function generateHookScript(
|
|
174
|
-
stateFilePath: string,
|
|
175
|
-
ledgerFilePath: string,
|
|
176
|
-
runnerPid: number,
|
|
177
|
-
): string {
|
|
273
|
+
export function generateHookScript(activePointerPath: string, workspaceRoot = ""): string {
|
|
178
274
|
const salientFields = SALIENT_ARG_FIELDS.join(" ");
|
|
179
275
|
const categoryCaseArms = buildCategoryCaseArms();
|
|
180
276
|
const nodeIdentityScript = buildNodeIdentityScript();
|
|
277
|
+
const observationStagingScript = buildObservationStagingScript();
|
|
278
|
+
const nodeBin = process.execPath;
|
|
181
279
|
return `#!/bin/bash
|
|
182
280
|
# Stigmer HITL approval hook for Cursor (preToolUse + beforeMCPExecution).
|
|
183
281
|
# Generated by cursor-runner — do not edit manually.
|
|
@@ -193,9 +291,47 @@ set -euo pipefail
|
|
|
193
291
|
|
|
194
292
|
INPUT=$(cat)
|
|
195
293
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
294
|
+
NODE_BIN="${nodeBin}"
|
|
295
|
+
ACTIVE_FILE="${activePointerPath}"
|
|
296
|
+
# Baked workspace root for capture-mode's gitignore check (empty in unit tests
|
|
297
|
+
# that don't exercise capture mode; the check then falls back to the path's dir).
|
|
298
|
+
GIT_ROOT="${workspaceRoot}"
|
|
299
|
+
|
|
300
|
+
# --- Resolve the CURRENT turn from the runner-written pointer ----------------
|
|
301
|
+
# The Cursor SDK caches .cursor/hooks.json (the hook script PATH) for the runner
|
|
302
|
+
# process, so THIS script is stable across executions and the per-turn pointer
|
|
303
|
+
# (active.json) — which bash re-reads on every invocation — is the only thing
|
|
304
|
+
# that changes. It names the CURRENT turn's approval-state, denial ledger, and
|
|
305
|
+
# runner PID. This indirection is what makes a long-lived runner correct: a
|
|
306
|
+
# per-session script baked with per-session paths would be cached at the FIRST
|
|
307
|
+
# execution and reused for every later one, recording their denials to the FIRST
|
|
308
|
+
# session's ledger while each later runner reads its own empty ledger and
|
|
309
|
+
# silently completes (the no-approval-button / "completed" regression).
|
|
310
|
+
#
|
|
311
|
+
# A missing/garbled pointer means no active Stigmer turn (between turns, after
|
|
312
|
+
# teardown, or a dead runner) -> allow (inert), matching the
|
|
313
|
+
# leftover-hooks.json-is-inert invariant (issue #173).
|
|
314
|
+
if [ ! -f "$ACTIVE_FILE" ]; then
|
|
315
|
+
echo '{"permission":"allow"}'
|
|
316
|
+
exit 0
|
|
317
|
+
fi
|
|
318
|
+
PTR=$(ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e 'const p=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write((p.stateFile||"")+"\\n"+(p.ledgerFile||"")+"\\n"+((p.runnerPid==null)?"":String(p.runnerPid)))' "$ACTIVE_FILE" 2>/dev/null || true)
|
|
319
|
+
if [ -n "$PTR" ]; then
|
|
320
|
+
STATE_FILE=$(printf '%s\\n' "$PTR" | sed -n 1p)
|
|
321
|
+
LEDGER_FILE=$(printf '%s\\n' "$PTR" | sed -n 2p)
|
|
322
|
+
RUNNER_PID=$(printf '%s\\n' "$PTR" | sed -n 3p)
|
|
323
|
+
else
|
|
324
|
+
# Node unavailable: the pointer holds plain ~/.stigmer paths and an integer
|
|
325
|
+
# (no JSON-escaped quotes), so grep/cut is reliable here.
|
|
326
|
+
STATE_FILE=$(grep -o '"stateFile":"[^"]*"' "$ACTIVE_FILE" | head -1 | cut -d'"' -f4 || true)
|
|
327
|
+
LEDGER_FILE=$(grep -o '"ledgerFile":"[^"]*"' "$ACTIVE_FILE" | head -1 | cut -d'"' -f4 || true)
|
|
328
|
+
RUNNER_PID=$(grep -o '"runnerPid":[0-9]*' "$ACTIVE_FILE" | head -1 | cut -d: -f2 || true)
|
|
329
|
+
fi
|
|
330
|
+
if [ -z "$RUNNER_PID" ]; then
|
|
331
|
+
# Pointer unreadable -> no scope owner to gate for; stay inert.
|
|
332
|
+
echo '{"permission":"allow"}'
|
|
333
|
+
exit 0
|
|
334
|
+
fi
|
|
199
335
|
|
|
200
336
|
# --- Scope guard: gate ONLY the runner's own agent (issue #173) -------------
|
|
201
337
|
# The Cursor SDK runs hooks in-process, so the runner's own agent invocations
|
|
@@ -230,14 +366,31 @@ if ! __stigmer_is_own_agent; then
|
|
|
230
366
|
exit 0
|
|
231
367
|
fi
|
|
232
368
|
|
|
369
|
+
# --- Capture-mode helper: is a path gitignored? -----------------------------
|
|
370
|
+
# In capture mode the runner snapshots the working tree with git and reconciles
|
|
371
|
+
# it to the user's per-file decisions on resume, but a gitignored path (e.g.
|
|
372
|
+
# .env, build output) is invisible to that snapshot — so it can be neither
|
|
373
|
+
# captured for review nor reverted on reject. Such writes/deletes therefore stay
|
|
374
|
+
# on the deny-gate. Returns 0 (true) when the path is ignored. A non-git context
|
|
375
|
+
# or a missing path returns non-zero (treated as not-ignored -> allow).
|
|
376
|
+
__stigmer_is_gitignored() {
|
|
377
|
+
_p="$1"
|
|
378
|
+
[ -z "$_p" ] && return 1
|
|
379
|
+
if [ -n "$GIT_ROOT" ]; then
|
|
380
|
+
git -C "$GIT_ROOT" check-ignore -q -- "$_p" 2>/dev/null
|
|
381
|
+
else
|
|
382
|
+
git -C "$(dirname "$_p")" check-ignore -q -- "$_p" 2>/dev/null
|
|
383
|
+
fi
|
|
384
|
+
}
|
|
385
|
+
|
|
233
386
|
# --- Canonical identity: tool_name / category / identity token / MCP token ---
|
|
234
387
|
# Computed by the same Node.js binary that runs the cursor-runner (absolute path
|
|
235
388
|
# baked at generation time) so JSON string values — file paths and especially
|
|
236
389
|
# shell commands containing quotes, newlines, or unicode escapes — decode to the
|
|
237
390
|
# exact bytes the runner sees in the stream event. ELECTRON_RUN_AS_NODE makes
|
|
238
391
|
# the invocation safe when the runner is embedded in an Electron app (where
|
|
239
|
-
# process.execPath is the Electron binary).
|
|
240
|
-
|
|
392
|
+
# process.execPath is the Electron binary). NODE_BIN is defined once near the top
|
|
393
|
+
# (it also parses the active-turn pointer).
|
|
241
394
|
IDENTITY=$(printf '%s' "$INPUT" | ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e '${nodeIdentityScript}' 2>/dev/null || true)
|
|
242
395
|
if [ -n "$IDENTITY" ]; then
|
|
243
396
|
TOOL_NAME=$(printf '%s\\n' "$IDENTITY" | sed -n 1p)
|
|
@@ -245,6 +398,16 @@ if [ -n "$IDENTITY" ]; then
|
|
|
245
398
|
TOKEN=$(printf '%s\\n' "$IDENTITY" | sed -n 3p)
|
|
246
399
|
MCP_TOKEN=$(printf '%s\\n' "$IDENTITY" | sed -n 4p)
|
|
247
400
|
HOOK_EVENT=$(printf '%s\\n' "$IDENTITY" | sed -n 5p)
|
|
401
|
+
# base64(JSON(tool_input)) — the authoritative args the runner overlays onto
|
|
402
|
+
# the gated tool call. A single unwrapped base64 line (Node does not wrap), so
|
|
403
|
+
# sed reads it whole even for large file content.
|
|
404
|
+
INPUT_B64=$(printf '%s\\n' "$IDENTITY" | sed -n 6p)
|
|
405
|
+
# Content token (base64 of category\\nsalient\\ndigest), empty for a non-edit
|
|
406
|
+
# tool. The exact-identity grant the runner authorizes for a file edit.
|
|
407
|
+
CONTENT_TOKEN=$(printf '%s\\n' "$IDENTITY" | sed -n 7p)
|
|
408
|
+
# Raw salient (base64) — the file path / command. Capture mode decodes it to
|
|
409
|
+
# run git check-ignore on a file path.
|
|
410
|
+
SALIENT=$(printf '%s\\n' "$IDENTITY" | sed -n 8p | base64 -d 2>/dev/null || true)
|
|
248
411
|
else
|
|
249
412
|
# Fallback when the Node binary cannot run: grep/cut extraction. Best-effort
|
|
250
413
|
# only — '"field":"[^"]*"' truncates at the first JSON-escaped quote, so the
|
|
@@ -266,6 +429,11 @@ ${categoryCaseArms}
|
|
|
266
429
|
esac
|
|
267
430
|
TOKEN=$(printf '%s\\n%s' "$CATEGORY" "$SALIENT" | base64 | tr -d '\\n')
|
|
268
431
|
MCP_TOKEN=$(printf '%s\\n' "$TOOL_NAME" | base64 | tr -d '\\n')
|
|
432
|
+
# The grep fallback cannot reliably capture full multi-line tool_input, so the
|
|
433
|
+
# gated call degrades to today's stream-recovered args (no authoritative input)
|
|
434
|
+
# and cannot compute a content digest — the coarse token is the only identity.
|
|
435
|
+
INPUT_B64=""
|
|
436
|
+
CONTENT_TOKEN=""
|
|
269
437
|
fi
|
|
270
438
|
|
|
271
439
|
# --- Failsafe: missing state file → deny (fail-closed) ---
|
|
@@ -276,6 +444,66 @@ fi
|
|
|
276
444
|
|
|
277
445
|
STATE=$(cat "$STATE_FILE")
|
|
278
446
|
|
|
447
|
+
# Capture mode (git workspaces): file mutations flow during the turn and are
|
|
448
|
+
# captured/gated per-file by the runner at the turn boundary (see
|
|
449
|
+
# shared/filereview/git-substrate.ts). Read once; consulted only in the
|
|
450
|
+
# gated-built-in arm below.
|
|
451
|
+
CAPTURE_MODE=false
|
|
452
|
+
if echo "$STATE" | grep -q '"captureMode":true'; then
|
|
453
|
+
CAPTURE_MODE=true
|
|
454
|
+
fi
|
|
455
|
+
# CAS capture of gitignored writes (the deep-agent parity switch). Set only when
|
|
456
|
+
# capture mode is on AND an artifact storage is configured (to persist blobs). It
|
|
457
|
+
# governs whether a non-secret gitignored write is staged+flowed for review vs.
|
|
458
|
+
# kept on the deny-gate. Read here; consulted only in the gitignored-capture arm
|
|
459
|
+
# below, which runs BEFORE auto-approve-all (capture is a turn property).
|
|
460
|
+
CAPTURE_IGNORED=false
|
|
461
|
+
if echo "$STATE" | grep -q '"captureIgnored":true'; then
|
|
462
|
+
CAPTURE_IGNORED=true
|
|
463
|
+
fi
|
|
464
|
+
# gitWorkspace selects the capture substrate (Slice 2c). Default true when the
|
|
465
|
+
# key is absent (older state files). When false the workspace is NOT a git tree:
|
|
466
|
+
# there is no git snapshot, so EVERY file write is CAS-staged below (not only
|
|
467
|
+
# gitignored ones), the git-tracked flow arm is skipped, and a delete stays gated
|
|
468
|
+
# (no CAS delete-capture path, parity with the deep-agent).
|
|
469
|
+
GIT_WORKSPACE=true
|
|
470
|
+
if echo "$STATE" | grep -q '"gitWorkspace":false'; then
|
|
471
|
+
GIT_WORKSPACE=false
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# --- Capture mode: observe CAS-owned writes for review ----------------------
|
|
475
|
+
# Runs BEFORE the auto-approve-all shortcut and the grant/lease checks because
|
|
476
|
+
# capture is a property of the TURN, not authorization: a non-secret CAS-owned
|
|
477
|
+
# write must be staged and reviewed even under the global bypass, and a
|
|
478
|
+
# secret-like one must be hard-blocked in every mode. WHICH writes are CAS-owned
|
|
479
|
+
# depends on the substrate: in a git tree it is only the GITIGNORED writes (git
|
|
480
|
+
# captures the tracked ones); in a NON-GIT workspace it is EVERY write (there is
|
|
481
|
+
# no git snapshot). Only a built-in write/edit (category "write") takes this arm;
|
|
482
|
+
# deletes and shell/MCP stay on the deny-gate below (parity with the deep-agent
|
|
483
|
+
# approval gate). The staging runs on the runner's own Node binary (the
|
|
484
|
+
# disk-backed mirror of CasCaptureFilesystemBackend.recordBefore): the salient
|
|
485
|
+
# path rides stdin (no argv escaping), the workspace root and the per-turn
|
|
486
|
+
# cas-observations dir ride argv. "captured" -> allow (apply-then-review);
|
|
487
|
+
# "secret" -> hard-block; "error"/Node-unavailable -> fail closed (deny, since a
|
|
488
|
+
# write we cannot capture cannot be reviewed).
|
|
489
|
+
if [ "$CAPTURE_IGNORED" = "true" ] && [ "$CATEGORY" = "write" ] && [ -n "$SALIENT" ] && { [ "$GIT_WORKSPACE" = "false" ] || __stigmer_is_gitignored "$SALIENT"; }; then
|
|
490
|
+
OBS_DIR="$(dirname "$STATE_FILE")/${CAS_OBSERVATIONS_DIRNAME}"
|
|
491
|
+
OBS_RESULT=$(printf '%s' "$SALIENT" | ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e '${observationStagingScript}' "$GIT_ROOT" "$OBS_DIR" 2>/dev/null || echo error)
|
|
492
|
+
if [ "$OBS_RESULT" = "captured" ]; then
|
|
493
|
+
echo '{"permission":"allow"}'
|
|
494
|
+
exit 0
|
|
495
|
+
elif [ "$OBS_RESULT" = "secret" ]; then
|
|
496
|
+
echo '{"permission":"deny","agent_message":"${SECRET_BLOCKED_AGENT_MESSAGE}","user_message":"Blocked for security: this file matches a secret-like path and was not written or captured for review."}'
|
|
497
|
+
exit 0
|
|
498
|
+
else
|
|
499
|
+
# Node unavailable or a staging error: fail closed. A gitignored write we
|
|
500
|
+
# cannot stage cannot be captured for review, so keep gating it (today's
|
|
501
|
+
# behavior) rather than letting unreviewable bytes flow.
|
|
502
|
+
echo '{"permission":"deny","agent_message":"${APPROVAL_REQUIRED_AGENT_MESSAGE}","user_message":"Tool requires approval: '"$TOOL_NAME"'"}'
|
|
503
|
+
exit 0
|
|
504
|
+
fi
|
|
505
|
+
fi
|
|
506
|
+
|
|
279
507
|
# --- 1. Auto-approve all ---
|
|
280
508
|
if echo "$STATE" | grep -q '"autoApproveAll":true'; then
|
|
281
509
|
echo '{"permission":"allow"}'
|
|
@@ -284,9 +512,13 @@ fi
|
|
|
284
512
|
|
|
285
513
|
# Append a denial record to the ledger. Best-effort: a ledger write failure must
|
|
286
514
|
# never abort the decision (the deny still goes out on stdout). toolName is raw
|
|
287
|
-
# for human-readable debugging; token drives correlation in the runner
|
|
515
|
+
# for human-readable debugging; token drives correlation in the runner; input is
|
|
516
|
+
# base64(JSON(tool_input)) — the authoritative pre-execution args the runner
|
|
517
|
+
# overlays for the approval preview (empty on the grep fallback path). Written
|
|
518
|
+
# with printf (a builtin, so no ARG_MAX limit) because the input can be a large
|
|
519
|
+
# multi-MB file body.
|
|
288
520
|
record_denial() {
|
|
289
|
-
|
|
521
|
+
printf '{"toolName":"%s","token":"%s","input":"%s"}\\n' "$TOOL_NAME" "$1" "$INPUT_B64" >> "$LEDGER_FILE" 2>/dev/null || true
|
|
290
522
|
}
|
|
291
523
|
|
|
292
524
|
# --- 2. MCP tools (beforeMCPExecution event) ---
|
|
@@ -321,12 +553,46 @@ fi
|
|
|
321
553
|
|
|
322
554
|
# --- 3. Gated built-in tools (preToolUse event, category non-empty) ---
|
|
323
555
|
if [ -n "$CATEGORY" ]; then
|
|
324
|
-
#
|
|
325
|
-
|
|
556
|
+
# Capture mode (GIT tree only): a git-tracked file mutation (write/edit/delete)
|
|
557
|
+
# flows freely — the runner captures the whole change set with git at the turn
|
|
558
|
+
# boundary and gates it per-file for review. A gitignored path is invisible to
|
|
559
|
+
# that git snapshot: a non-secret gitignored WRITE was already handled above
|
|
560
|
+
# (staged + allowed, or hard-blocked) when captureIgnored is on; here it only
|
|
561
|
+
# reaches the deny-gate when captureIgnored is off (no artifact storage) or it is
|
|
562
|
+
# a gitignored DELETE (no CAS capture path, parity with deep-agent). shell
|
|
563
|
+
# (category "shell") never takes this branch and stays gated as always. In a
|
|
564
|
+
# NON-GIT workspace this arm is skipped entirely (there is no git diff): writes
|
|
565
|
+
# were CAS-staged above and a delete falls through to the deny-gate.
|
|
566
|
+
if [ "$CAPTURE_MODE" = "true" ] && [ "$GIT_WORKSPACE" = "true" ] && { [ "$CATEGORY" = "write" ] || [ "$CATEGORY" = "delete" ]; }; then
|
|
567
|
+
if ! __stigmer_is_gitignored "$SALIENT"; then
|
|
568
|
+
echo '{"permission":"allow"}'
|
|
569
|
+
exit 0
|
|
570
|
+
fi
|
|
571
|
+
fi
|
|
572
|
+
# Reinvocation grant: allow when the CONTENT-exact token (a file edit approved
|
|
573
|
+
# earlier with this exact content) OR the COARSE token (a shell/delete, or the
|
|
574
|
+
# content-less degrade) is in approvedGrantTokens. A sibling edit to the same
|
|
575
|
+
# file has a different content token and no coarse grant, so it re-gates.
|
|
576
|
+
if { [ -n "$CONTENT_TOKEN" ] && echo "$STATE" | grep -qF "\\"$CONTENT_TOKEN\\""; } || echo "$STATE" | grep -qF "\\"$TOKEN\\""; then
|
|
326
577
|
echo '{"permission":"allow"}'
|
|
327
578
|
exit 0
|
|
328
579
|
fi
|
|
329
|
-
|
|
580
|
+
# Run-lifetime category lease: the user chose "approve all <category>" earlier
|
|
581
|
+
# in this run (the scoped successor to autoApproveAll), so every built-in of
|
|
582
|
+
# this category is allowed for the rest of the run. Matched within the extracted
|
|
583
|
+
# leasedCategories array so a category word elsewhere in the state can't grant.
|
|
584
|
+
LEASED_CATEGORIES=$(echo "$STATE" | grep -o '"leasedCategories":\\[[^]]*\\]' | head -1 || true)
|
|
585
|
+
if [ -n "$LEASED_CATEGORIES" ] && echo "$LEASED_CATEGORIES" | grep -q "\\"$CATEGORY\\""; then
|
|
586
|
+
echo '{"permission":"allow"}'
|
|
587
|
+
exit 0
|
|
588
|
+
fi
|
|
589
|
+
# Record the PRIMARY token (content-exact when available, else coarse) so the
|
|
590
|
+
# runner's denial correlation keys on the SAME identity it grants on approval.
|
|
591
|
+
if [ -n "$CONTENT_TOKEN" ]; then
|
|
592
|
+
record_denial "$CONTENT_TOKEN"
|
|
593
|
+
else
|
|
594
|
+
record_denial "$TOKEN"
|
|
595
|
+
fi
|
|
330
596
|
echo '{"permission":"deny","agent_message":"${APPROVAL_REQUIRED_AGENT_MESSAGE}","user_message":"Tool requires approval: '"$TOOL_NAME"'"}'
|
|
331
597
|
exit 0
|
|
332
598
|
fi
|