@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
|
@@ -9,21 +9,36 @@
|
|
|
9
9
|
* State file format (JSON):
|
|
10
10
|
* {
|
|
11
11
|
* "autoApproveAll": false,
|
|
12
|
+
* "leasedCategories": ["shell"],
|
|
12
13
|
* "mcpToolPolicies": {
|
|
13
14
|
* "apply_cloud_resource": { "requiresApproval": true, "message": "..." }
|
|
14
15
|
* },
|
|
15
|
-
* "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt" }],
|
|
16
|
-
* "approvedGrantTokens": ["
|
|
16
|
+
* "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt", "contentDigest": "<sha256>" }],
|
|
17
|
+
* "approvedGrantTokens": ["<base64(key\nsalient[\ncontentDigest])>"]
|
|
17
18
|
* }
|
|
18
19
|
*
|
|
20
|
+
* A grant token is the action's PRIMARY token: base64(key \n salient \n digest)
|
|
21
|
+
* for a content-identified file edit (so a DIFFERENT edit to the same file does
|
|
22
|
+
* not match), or base64(key \n salient) for shell/delete/MCP and the rare
|
|
23
|
+
* content-less fallback. See {@link primaryToken}.
|
|
24
|
+
*
|
|
19
25
|
* The hook gates the dangerous built-in set and the MCP tools that require
|
|
20
26
|
* approval (mcpToolPolicies, which by construction holds only require-approval
|
|
21
27
|
* entries); every other tool is allowed. The gated built-in set and its
|
|
22
28
|
* name->category mapping are baked into the generated hook script (from
|
|
23
29
|
* approval-policy.ts), not carried in the state file — only the dynamic inputs
|
|
24
|
-
* (autoApproveAll, mcpToolPolicies, approvedGrantTokens) live
|
|
25
|
-
* the native harness and avoids denying auto-approved MCP
|
|
26
|
-
* absent from the policy map and indistinguishable from unknown
|
|
30
|
+
* (autoApproveAll, leasedCategories, mcpToolPolicies, approvedGrantTokens) live
|
|
31
|
+
* here. This mirrors the native harness and avoids denying auto-approved MCP
|
|
32
|
+
* tools, which are absent from the policy map and indistinguishable from unknown
|
|
33
|
+
* tools by name.
|
|
34
|
+
*
|
|
35
|
+
* Approval leases (the scoped successor to autoApproveAll): `autoApproveAll` is
|
|
36
|
+
* now ONLY the pre-armed spec.auto_approve_all global bypass. An interactive
|
|
37
|
+
* "approve all" of a given class becomes a run-lifetime lease: a built-in
|
|
38
|
+
* category lease is listed in `leasedCategories` (the hook allows any built-in of
|
|
39
|
+
* that category), and an MCP-server lease is applied upstream by dropping the
|
|
40
|
+
* server's tools from mcpToolPolicies (so the hook allows them as auto-approved)
|
|
41
|
+
* — the hook is not server-aware, so omission is the lever there.
|
|
27
42
|
*
|
|
28
43
|
* Why grants instead of tool-call ids: a resumed Cursor agent re-issues the
|
|
29
44
|
* approved tool with a BRAND NEW call id, so matching on the original call id
|
|
@@ -51,15 +66,21 @@
|
|
|
51
66
|
* token next turn.
|
|
52
67
|
*/
|
|
53
68
|
|
|
54
|
-
import {
|
|
69
|
+
import { watch } from "node:fs";
|
|
70
|
+
import { writeFile, readFile, mkdir, rename, rm } from "node:fs/promises";
|
|
55
71
|
import { join } from "node:path";
|
|
56
72
|
import { create } from "@bufbuild/protobuf";
|
|
57
73
|
import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
58
74
|
import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
59
75
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
60
76
|
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
61
|
-
import type { MergedToolPolicy } from "./approval-policy.js";
|
|
62
|
-
import { extractArgKey, approvalCategory } from "./approval-policy.js";
|
|
77
|
+
import type { MergedToolPolicy, ApprovalCategory } from "./approval-policy.js";
|
|
78
|
+
import { extractArgKey, approvalCategory, POLICY_ENGINE_VERSION } from "./approval-policy.js";
|
|
79
|
+
import { contentDigest } from "../../shared/file-tools.js";
|
|
80
|
+
import {
|
|
81
|
+
fingerprintCoarseIdentity,
|
|
82
|
+
type FingerprintKey,
|
|
83
|
+
} from "../../shared/approval-fingerprint.js";
|
|
63
84
|
|
|
64
85
|
export interface McpToolPolicyEntry {
|
|
65
86
|
requiresApproval: boolean;
|
|
@@ -115,13 +136,78 @@ export interface ApprovalGrant {
|
|
|
115
136
|
mcpServerSlug: string;
|
|
116
137
|
key: string;
|
|
117
138
|
salient: string;
|
|
139
|
+
/**
|
|
140
|
+
* Content digest of the approved edit (see {@link contentDigest}), or "" when
|
|
141
|
+
* the action is not content-identified (shell/delete/MCP, or a content-less
|
|
142
|
+
* fallback). When present, the grant authorizes the {@link contentToken} so a
|
|
143
|
+
* DIFFERENT edit to the same path does NOT match; when "", it authorizes the
|
|
144
|
+
* coarse {@link grantToken} (the documented degrade).
|
|
145
|
+
*/
|
|
146
|
+
contentDigest: string;
|
|
147
|
+
/**
|
|
148
|
+
* Id of the adjudicated tool call this grant was minted from — the transcript
|
|
149
|
+
* row carrying the SERVER-authored approval_action. The approved-command
|
|
150
|
+
* auto-keep provenance (DD-28) cites this row as the consent the backend can
|
|
151
|
+
* verify; the hook itself never reads it.
|
|
152
|
+
*/
|
|
153
|
+
sourceToolCallId: string;
|
|
118
154
|
}
|
|
119
155
|
|
|
120
156
|
export interface ApprovalStateFile {
|
|
157
|
+
/** Pre-armed spec.auto_approve_all: the whole-run global bypass. */
|
|
121
158
|
autoApproveAll: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Built-in approval categories with a run-lifetime lease (the scoped successor
|
|
161
|
+
* to a global "approve all"). The hook allows any built-in whose category is
|
|
162
|
+
* listed. MCP-server leases are NOT listed here — they are applied by dropping
|
|
163
|
+
* the server's tools from mcpToolPolicies, since the hook is not server-aware.
|
|
164
|
+
*/
|
|
165
|
+
leasedCategories: string[];
|
|
122
166
|
mcpToolPolicies: Record<string, McpToolPolicyEntry>;
|
|
123
167
|
approvedGrants: ApprovalGrant[];
|
|
124
168
|
approvedGrantTokens: string[];
|
|
169
|
+
/**
|
|
170
|
+
* Capture mode (git workspaces): when true, the hook ALLOWS file mutations
|
|
171
|
+
* (write/edit/delete) to flow during the turn, because the runner captures the
|
|
172
|
+
* whole change set with git at the turn boundary and gates it per-file for
|
|
173
|
+
* review (see shared/filereview/git-substrate.ts). The ONLY exception is a
|
|
174
|
+
* write/delete whose
|
|
175
|
+
* path is gitignored — the git snapshot cannot capture or revert it, so the
|
|
176
|
+
* hook keeps gating those (it runs `git check-ignore`). shell and MCP stay
|
|
177
|
+
* gated as always. False (the default) keeps the classic deny-gate behavior
|
|
178
|
+
* for every file mutation (non-git workspaces / the fallback path).
|
|
179
|
+
*/
|
|
180
|
+
captureMode: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* CAS capture for gitignored writes (the deep-agent parity switch). When true,
|
|
183
|
+
* a non-secret gitignored write/edit no longer stays on the deny-gate: the hook
|
|
184
|
+
* stages its pre-write bytes into the runner-owned cas-observations sidecar and
|
|
185
|
+
* ALLOWS it to flow, and the turn boundary captures it into content-addressable
|
|
186
|
+
* storage as a `GIT_IGNORED_CAPTURED` change for per-file review (mirroring the
|
|
187
|
+
* deep-agent `CasCaptureFilesystemBackend` observer). A secret-like gitignored
|
|
188
|
+
* path is instead hard-blocked (denied, nothing written) and recorded as an
|
|
189
|
+
* unreviewable observation, so its bytes never reach durable storage.
|
|
190
|
+
*
|
|
191
|
+
* Set only when `captureMode` AND an artifact storage is configured — CAS blob
|
|
192
|
+
* persistence requires it. When false, gitignored writes keep the classic
|
|
193
|
+
* deny-gate behavior (gitignored deletes and shell/MCP always do). Independent
|
|
194
|
+
* of `autoApproveAll`: capture is a property of the turn, not authorization, so
|
|
195
|
+
* the hook honors this switch even under the global bypass.
|
|
196
|
+
*/
|
|
197
|
+
captureIgnored: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Whether the primary workspace is a git work tree (Slice 2c). Selects the
|
|
200
|
+
* hook's capture substrate:
|
|
201
|
+
* - `true` (default): git tree — git-tracked write/edit/delete flow freely
|
|
202
|
+
* (the runner captures them from the git diff at the turn boundary); only a
|
|
203
|
+
* GITIGNORED write is CAS-staged (when `captureIgnored`), a gitignored delete
|
|
204
|
+
* stays gated.
|
|
205
|
+
* - `false`: non-git workspace — there is no git snapshot, so EVERY file write
|
|
206
|
+
* is CAS-staged and flowed for review (`captureIgnored` is on in this mode),
|
|
207
|
+
* a delete stays gated (no CAS delete-capture path, parity with the
|
|
208
|
+
* deep-agent), and shell/MCP gate as always.
|
|
209
|
+
*/
|
|
210
|
+
gitWorkspace: boolean;
|
|
125
211
|
}
|
|
126
212
|
|
|
127
213
|
/**
|
|
@@ -134,6 +220,87 @@ export function grantToken(key: string, salient: string): string {
|
|
|
134
220
|
return Buffer.from(`${key}\n${salient}`, "utf-8").toString("base64");
|
|
135
221
|
}
|
|
136
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The content-exact wire token: `base64(key \n salient \n contentDigest)`. This
|
|
225
|
+
* is the exact-identity grant the hook matches for a file-mutating tool, so an
|
|
226
|
+
* approval of one edit does NOT authorize a DIFFERENT edit to the same file (a
|
|
227
|
+
* different digest yields a different token). The hook recomputes the identical
|
|
228
|
+
* token from `tool_input` (it appends the same `\n<digest>` only when the digest
|
|
229
|
+
* is non-empty — see hook-script.ts), so this encoding must stay byte-identical
|
|
230
|
+
* to the hook's, exactly as {@link grantToken} already is.
|
|
231
|
+
*/
|
|
232
|
+
export function contentToken(key: string, salient: string, contentDigest: string): string {
|
|
233
|
+
return Buffer.from(`${key}\n${salient}\n${contentDigest}`, "utf-8").toString("base64");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The single token a tool call authorizes (as a grant) and is recorded under (as
|
|
238
|
+
* a denial): the {@link contentToken} when a content digest is present (file
|
|
239
|
+
* edits/writes), else the coarse {@link grantToken} (shell, delete, MCP, or a
|
|
240
|
+
* content-less grep-fallback). One definition, used by the runner for both grant
|
|
241
|
+
* building and denial correlation and mirrored by the hook, so the deny-time and
|
|
242
|
+
* reinvoke-time identities can never drift.
|
|
243
|
+
*/
|
|
244
|
+
export function primaryToken(key: string, salient: string, contentDigest: string): string {
|
|
245
|
+
return contentDigest ? contentToken(key, salient, contentDigest) : grantToken(key, salient);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The shared HMAC coarse fingerprint of an approval grant.
|
|
250
|
+
*
|
|
251
|
+
* This is the SAME canonical coarse identity the wire token (grantToken) encodes
|
|
252
|
+
* — `(key, salient)` plus the MCP slug — run through the one shared HMAC+canonical
|
|
253
|
+
* path ({@link fingerprintCoarseIdentity}). It is NOT the hook's wire-match value:
|
|
254
|
+
* the hook matches on the mechanically-reproducible base64 token (a bash script
|
|
255
|
+
* can recompute base64 but not a keyed HMAC over a workspace-root-normalized
|
|
256
|
+
* salient). This fingerprint is the cross-substrate, anti-forgery identity used
|
|
257
|
+
* for the runner-side shadow receipt today and as the successor wire token once a
|
|
258
|
+
* lease becomes a server-issued bearer token (Phase 7). Because it shares the
|
|
259
|
+
* exact category + salient the token uses, the hook-side and stream-side
|
|
260
|
+
* fingerprints of one action are equal by construction (see the parity tests).
|
|
261
|
+
*/
|
|
262
|
+
export function grantFingerprint(key: FingerprintKey, grant: ApprovalGrant): string {
|
|
263
|
+
return fingerprintCoarseIdentity(key, {
|
|
264
|
+
tool: grant.key,
|
|
265
|
+
mcpServerSlug: grant.mcpServerSlug,
|
|
266
|
+
salient: grant.salient,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Emit a best-effort shadow ExecutionReceipt for each grant the runner issues
|
|
272
|
+
* this turn (Phase 2, mirror of the deep-agent gateway's receipt).
|
|
273
|
+
*
|
|
274
|
+
* Cursor executes tools out-of-process, so unlike the in-process deep-agent
|
|
275
|
+
* gateway the runner cannot observe the actual side effect — the receipt is
|
|
276
|
+
* issued when the authorization GRANT is written (best-effort, `verified:false`),
|
|
277
|
+
* not at execution. Structured log only: never persisted, no proto. Shares the
|
|
278
|
+
* one HMAC+canonical fingerprint path so the value matches the deep-agent and
|
|
279
|
+
* cross-language corpus definitions.
|
|
280
|
+
*/
|
|
281
|
+
export function emitCursorGrantReceipts(
|
|
282
|
+
grants: readonly ApprovalGrant[],
|
|
283
|
+
fingerprintKey: FingerprintKey,
|
|
284
|
+
executionId: string,
|
|
285
|
+
): void {
|
|
286
|
+
for (const g of grants) {
|
|
287
|
+
console.log(
|
|
288
|
+
"[hitl-gateway] receipt " +
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
executionId,
|
|
291
|
+
toolName: g.toolName,
|
|
292
|
+
mcpServerSlug: g.mcpServerSlug,
|
|
293
|
+
category: g.mcpServerSlug ? "" : g.key,
|
|
294
|
+
authorization: "approval",
|
|
295
|
+
policyEngineVersion: POLICY_ENGINE_VERSION,
|
|
296
|
+
fingerprint: grantFingerprint(fingerprintKey, g),
|
|
297
|
+
substrate: "cursor",
|
|
298
|
+
verified: false,
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
137
304
|
/**
|
|
138
305
|
* Build approval grants from the pending approvals the user adjudicated and
|
|
139
306
|
* their decisions. Only APPROVE / APPROVE_ALL decisions produce grants. Each
|
|
@@ -143,14 +310,15 @@ export function grantToken(key: string, salient: string): string {
|
|
|
143
310
|
export function buildApprovalGrants(
|
|
144
311
|
pendingApprovals: PendingApproval[],
|
|
145
312
|
decisions: Map<string, ApprovalAction>,
|
|
313
|
+
contentDigests?: Map<string, string>,
|
|
146
314
|
): ApprovalGrant[] {
|
|
147
315
|
const grants: ApprovalGrant[] = [];
|
|
148
316
|
for (const pa of pendingApprovals) {
|
|
149
317
|
// Both APPROVE and APPROVE_ALL allow the adjudicated tool through on the
|
|
150
|
-
// resumed turn. APPROVE_ALL additionally
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// the state
|
|
318
|
+
// resumed turn. APPROVE_ALL additionally grants a run-lifetime lease for the
|
|
319
|
+
// clicked action's class (handled by the caller via deriveActiveLeases ->
|
|
320
|
+
// leasedCategories / dropped MCP server), but we still emit a grant here so
|
|
321
|
+
// the clicked tool itself is allowed regardless of how the hook reads state.
|
|
154
322
|
const decision = decisions.get(pa.toolCallId);
|
|
155
323
|
if (decision !== ApprovalAction.APPROVE && decision !== ApprovalAction.APPROVE_ALL) continue;
|
|
156
324
|
|
|
@@ -160,6 +328,14 @@ export function buildApprovalGrants(
|
|
|
160
328
|
mcpServerSlug: pa.mcpServerSlug,
|
|
161
329
|
key: id.key,
|
|
162
330
|
salient: id.salient,
|
|
331
|
+
// The content digest comes from the gate's authoritative captured input
|
|
332
|
+
// (carried on the tool call, see reconstructAdjudicatedApprovals) — NOT
|
|
333
|
+
// from argsPreview, which elides heavy edit content. Empty when the gate
|
|
334
|
+
// had no content (shell/delete/MCP) or it was unrecoverable, in which case
|
|
335
|
+
// the grant degrades to the coarse token (a same-file sibling can ride it,
|
|
336
|
+
// the documented bounded residual).
|
|
337
|
+
contentDigest: contentDigests?.get(pa.toolCallId) ?? "",
|
|
338
|
+
sourceToolCallId: pa.toolCallId,
|
|
163
339
|
});
|
|
164
340
|
}
|
|
165
341
|
return grants;
|
|
@@ -180,7 +356,10 @@ function parseArgs(argsPreview: string): Record<string, unknown> | undefined {
|
|
|
180
356
|
* grants from a previous HITL cycle.
|
|
181
357
|
*
|
|
182
358
|
* The state file carries the hook script's DYNAMIC inputs:
|
|
183
|
-
* -
|
|
359
|
+
* - globalBypass: the pre-armed spec.auto_approve_all (written as autoApproveAll)
|
|
360
|
+
* - leasedCategories: built-in categories with a run-lifetime lease
|
|
361
|
+
* - mcpToolPolicies: per-tool policy for MCP tools requiring approval (leased
|
|
362
|
+
* servers are already absent — dropped upstream by mergeApprovalPolicies)
|
|
184
363
|
* - approvedGrants / approvedGrantTokens: tools approved in the current HITL
|
|
185
364
|
* cycle, allowed through on reinvocation
|
|
186
365
|
*
|
|
@@ -189,8 +368,12 @@ function parseArgs(argsPreview: string): Record<string, unknown> | undefined {
|
|
|
189
368
|
*/
|
|
190
369
|
export function buildApprovalState(
|
|
191
370
|
mergedPolicies: Map<string, MergedToolPolicy>,
|
|
192
|
-
|
|
371
|
+
globalBypass: boolean,
|
|
372
|
+
leasedCategories: ReadonlySet<ApprovalCategory>,
|
|
193
373
|
grants?: ApprovalGrant[],
|
|
374
|
+
captureMode = false,
|
|
375
|
+
captureIgnored = false,
|
|
376
|
+
gitWorkspace = true,
|
|
194
377
|
): ApprovalStateFile {
|
|
195
378
|
const approvedGrants = grants ?? [];
|
|
196
379
|
|
|
@@ -203,10 +386,17 @@ export function buildApprovalState(
|
|
|
203
386
|
}
|
|
204
387
|
|
|
205
388
|
return {
|
|
206
|
-
autoApproveAll,
|
|
389
|
+
autoApproveAll: globalBypass,
|
|
390
|
+
leasedCategories: [...leasedCategories],
|
|
207
391
|
mcpToolPolicies,
|
|
208
392
|
approvedGrants,
|
|
209
|
-
|
|
393
|
+
// The hook matches a tool call's PRIMARY token (content when it can compute a
|
|
394
|
+
// digest from tool_input, else coarse). A content-identified grant authorizes
|
|
395
|
+
// only its exact content; a content-less grant authorizes the coarse token.
|
|
396
|
+
approvedGrantTokens: approvedGrants.map((g) => primaryToken(g.key, g.salient, g.contentDigest)),
|
|
397
|
+
captureMode,
|
|
398
|
+
captureIgnored,
|
|
399
|
+
gitWorkspace,
|
|
210
400
|
};
|
|
211
401
|
}
|
|
212
402
|
|
|
@@ -248,10 +438,21 @@ const DENIAL_LEDGER_FILE = "denials.jsonl";
|
|
|
248
438
|
* the same space as grantToken() (base64 of `toolName \n salientArg`), used to
|
|
249
439
|
* correlate the denial back to the streamed tool call. `toolName` is carried raw
|
|
250
440
|
* for human-readable debugging of the ledger file.
|
|
441
|
+
*
|
|
442
|
+
* `input` is the authoritative pre-execution tool arguments the hook captured
|
|
443
|
+
* from the Cursor `tool_input` payload (decoded from the ledger's base64 form).
|
|
444
|
+
* It is the cursor analog of the native harness reading the AI-message tool-call
|
|
445
|
+
* args out of graph state at the LangGraph interrupt — the one place the COMPLETE
|
|
446
|
+
* proposed change is in hand before the tool runs. The runner overlays it onto
|
|
447
|
+
* the gated tool call so the approval card can show the proposed content/diff
|
|
448
|
+
* before the user approves. Absent when the hook ran its grep fallback (the Node
|
|
449
|
+
* binary was unavailable), in which case the gate degrades to stream-recovered
|
|
450
|
+
* args exactly as before.
|
|
251
451
|
*/
|
|
252
452
|
export interface DeniedLedgerEntry {
|
|
253
453
|
toolName: string;
|
|
254
454
|
token: string;
|
|
455
|
+
input?: Record<string, unknown>;
|
|
255
456
|
}
|
|
256
457
|
|
|
257
458
|
/**
|
|
@@ -278,6 +479,46 @@ export async function resetDenialLedger(hitlDir: string): Promise<string> {
|
|
|
278
479
|
return filePath;
|
|
279
480
|
}
|
|
280
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Watch the denial ledger so the stream loop learns about a hook denial the
|
|
484
|
+
* moment it is written — not on the next tool_call event.
|
|
485
|
+
*
|
|
486
|
+
* The hook appends to denials.jsonl BEFORE Cursor surfaces the failure to the
|
|
487
|
+
* model, so a filesystem notification is the earliest possible signal that a
|
|
488
|
+
* turn must pause. The watcher deliberately does NOT read the file itself: it
|
|
489
|
+
* only flips the caller's dirty flag, and the stream loop confirms with a
|
|
490
|
+
* readDenialLedger() — a notification can fire for the per-turn reset
|
|
491
|
+
* truncation too, and only a read distinguishes "reset to empty" from "denial
|
|
492
|
+
* appended". Watching the DIRECTORY (not the file) survives the file being
|
|
493
|
+
* replaced/truncated between turns.
|
|
494
|
+
*
|
|
495
|
+
* fs.watch is best-effort by platform contract, so this is an accelerator,
|
|
496
|
+
* never the only trigger: the stream loop keeps its tool_call-event read as
|
|
497
|
+
* the backstop. On any watcher error we fall back to that backstop silently.
|
|
498
|
+
*
|
|
499
|
+
* Returns a close function, idempotent and safe to call on every exit path.
|
|
500
|
+
*/
|
|
501
|
+
export function watchDenialLedger(hitlDir: string, onDirty: () => void): () => void {
|
|
502
|
+
try {
|
|
503
|
+
const watcher = watch(hitlDir, (_eventType, filename) => {
|
|
504
|
+
if (!filename || filename === DENIAL_LEDGER_FILE) onDirty();
|
|
505
|
+
});
|
|
506
|
+
// Without an error listener a watcher error crashes the process; with one,
|
|
507
|
+
// the watcher just goes quiet and the tool_call backstop takes over.
|
|
508
|
+
watcher.on("error", () => {});
|
|
509
|
+
return () => {
|
|
510
|
+
try {
|
|
511
|
+
watcher.close();
|
|
512
|
+
} catch {
|
|
513
|
+
// Already closed — nothing to release.
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
} catch {
|
|
517
|
+
// Watch unsupported on this platform/filesystem — backstop only.
|
|
518
|
+
return () => {};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
281
522
|
/**
|
|
282
523
|
* Read the denial ledger written by the hook during the turn. Missing file →
|
|
283
524
|
* no denials. Blank or partially-written lines are tolerated (the hook appends
|
|
@@ -299,11 +540,12 @@ export async function readDenialLedger(
|
|
|
299
540
|
const trimmed = line.trim();
|
|
300
541
|
if (!trimmed) continue;
|
|
301
542
|
try {
|
|
302
|
-
const obj = JSON.parse(trimmed) as
|
|
543
|
+
const obj = JSON.parse(trimmed) as { toolName?: unknown; token?: unknown; input?: unknown };
|
|
303
544
|
if (typeof obj.token === "string" && obj.token) {
|
|
304
545
|
entries.push({
|
|
305
546
|
toolName: typeof obj.toolName === "string" ? obj.toolName : "",
|
|
306
547
|
token: obj.token,
|
|
548
|
+
input: decodeLedgerInput(obj.input),
|
|
307
549
|
});
|
|
308
550
|
}
|
|
309
551
|
} catch {
|
|
@@ -313,6 +555,94 @@ export async function readDenialLedger(
|
|
|
313
555
|
return entries;
|
|
314
556
|
}
|
|
315
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Decode the hook's base64(JSON(tool_input)) into the authoritative args object,
|
|
560
|
+
* or undefined when absent/garbage. Tolerant by construction: a bad capture must
|
|
561
|
+
* never drop the denial it rides on — the gate still surfaces, just without the
|
|
562
|
+
* richer preview.
|
|
563
|
+
*/
|
|
564
|
+
function decodeLedgerInput(raw: unknown): Record<string, unknown> | undefined {
|
|
565
|
+
if (typeof raw !== "string" || raw.length === 0) return undefined;
|
|
566
|
+
try {
|
|
567
|
+
const json = Buffer.from(raw, "base64").toString("utf-8");
|
|
568
|
+
const parsed = JSON.parse(json);
|
|
569
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
570
|
+
? (parsed as Record<string, unknown>)
|
|
571
|
+
: undefined;
|
|
572
|
+
} catch {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
578
|
+
// Active-turn pointer (runner → stable hook): the per-turn indirection that makes
|
|
579
|
+
// a single, process-cached hook script resolve the CURRENT execution's artifacts
|
|
580
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
const ACTIVE_POINTER_FILE = "active.json";
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The current turn's artifact paths, written by the runner into the WORKSPACE's
|
|
586
|
+
* gate directory and read by the stable hook script on every invocation.
|
|
587
|
+
*
|
|
588
|
+
* Why this exists: the Cursor SDK caches `.cursor/hooks.json` (the hook script
|
|
589
|
+
* PATH) for the runner process, so the script must be STABLE across executions
|
|
590
|
+
* (a per-session script gets cached at the first execution and reused for all
|
|
591
|
+
* later ones — recording their denials to the FIRST session's ledger, leaving the
|
|
592
|
+
* current runner's ledger empty so it completes instead of pausing). The stable
|
|
593
|
+
* script bakes in NO per-session paths; it reads this pointer instead, which the
|
|
594
|
+
* runner repoints every turn to the current execution's state file, denial
|
|
595
|
+
* ledger, and runner PID.
|
|
596
|
+
*/
|
|
597
|
+
export interface ActiveTurnPointer {
|
|
598
|
+
/** Absolute path of THIS turn's approval-state file (hook input). */
|
|
599
|
+
stateFile: string;
|
|
600
|
+
/** Absolute path of THIS turn's denial ledger (hook output the runner reads). */
|
|
601
|
+
ledgerFile: string;
|
|
602
|
+
/** PID of the runner that owns THIS turn (the hook's scope-guard anchor). */
|
|
603
|
+
runnerPid: number;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Absolute path of the active-turn pointer inside a workspace's gate directory. */
|
|
607
|
+
export function activePointerPath(gateDir: string): string {
|
|
608
|
+
return join(gateDir, ACTIVE_POINTER_FILE);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Atomically point the workspace's stable hook at the current turn's artifacts.
|
|
613
|
+
*
|
|
614
|
+
* Written compactly (the hook's grep fallback parses `"key":"value"` with no
|
|
615
|
+
* spaces) and atomically (write a temp sibling, then rename) so a hook firing
|
|
616
|
+
* concurrently with the write never reads a half-written pointer. Returns the
|
|
617
|
+
* pointer path.
|
|
618
|
+
*/
|
|
619
|
+
export async function writeActiveTurnPointer(
|
|
620
|
+
gateDir: string,
|
|
621
|
+
pointer: ActiveTurnPointer,
|
|
622
|
+
): Promise<string> {
|
|
623
|
+
await mkdir(gateDir, { recursive: true });
|
|
624
|
+
const filePath = activePointerPath(gateDir);
|
|
625
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
626
|
+
await writeFile(tmpPath, JSON.stringify(pointer), "utf-8");
|
|
627
|
+
await rename(tmpPath, filePath);
|
|
628
|
+
return filePath;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Remove the active-turn pointer on teardown so the gate is INERT between turns:
|
|
633
|
+
* a hook that fires when no turn is active (a leftover cached hooks.json, the
|
|
634
|
+
* user's own IDE) reads no pointer and allows immediately. Best-effort — a
|
|
635
|
+
* teardown failure must never fail the execution, and a stale pointer is itself
|
|
636
|
+
* inert once its runnerPid is gone (the scope guard fails closed to allow).
|
|
637
|
+
*/
|
|
638
|
+
export async function removeActiveTurnPointer(gateDir: string): Promise<void> {
|
|
639
|
+
try {
|
|
640
|
+
await rm(activePointerPath(gateDir), { force: true });
|
|
641
|
+
} catch {
|
|
642
|
+
// Already gone or unwritable — nothing to clean.
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
316
646
|
/**
|
|
317
647
|
* Reconstruct the adjudicated approvals for a HITL reinvocation directly from
|
|
318
648
|
* the tool calls in messages.
|
|
@@ -331,6 +661,15 @@ export async function readDenialLedger(
|
|
|
331
661
|
export interface AdjudicatedApprovals {
|
|
332
662
|
pendingApprovals: PendingApproval[];
|
|
333
663
|
decisions: Map<string, ApprovalAction>;
|
|
664
|
+
/**
|
|
665
|
+
* tool-call id -> content digest of the approved edit, for the content-exact
|
|
666
|
+
* grant. Sourced from the persisted `approval_content_digest` field (stable,
|
|
667
|
+
* immune to the size-limit elision that can drop `args`), falling back to a
|
|
668
|
+
* recompute from `args` only when the field is absent (an execution that
|
|
669
|
+
* predates the field). Empty for a non-content tool — the grant then degrades
|
|
670
|
+
* to the coarse token.
|
|
671
|
+
*/
|
|
672
|
+
contentDigests: Map<string, string>;
|
|
334
673
|
}
|
|
335
674
|
|
|
336
675
|
export function reconstructAdjudicatedApprovals(
|
|
@@ -338,6 +677,7 @@ export function reconstructAdjudicatedApprovals(
|
|
|
338
677
|
): AdjudicatedApprovals {
|
|
339
678
|
const pendingApprovals: PendingApproval[] = [];
|
|
340
679
|
const decisions = new Map<string, ApprovalAction>();
|
|
680
|
+
const contentDigests = new Map<string, string>();
|
|
341
681
|
|
|
342
682
|
for (const msg of messages) {
|
|
343
683
|
for (const tc of msg.toolCalls) {
|
|
@@ -345,6 +685,14 @@ export function reconstructAdjudicatedApprovals(
|
|
|
345
685
|
if (tc.approvalAction === ApprovalAction.UNSPECIFIED) continue;
|
|
346
686
|
|
|
347
687
|
decisions.set(tc.id, tc.approvalAction);
|
|
688
|
+
// Prefer the persisted digest (set at the gate from the authoritative
|
|
689
|
+
// captured input, and never elided); recompute from args only for an
|
|
690
|
+
// execution that predates the field.
|
|
691
|
+
contentDigests.set(
|
|
692
|
+
tc.id,
|
|
693
|
+
tc.approvalContentDigest ||
|
|
694
|
+
(tc.args ? contentDigest(tc.args as Record<string, unknown>) : ""),
|
|
695
|
+
);
|
|
348
696
|
pendingApprovals.push(
|
|
349
697
|
create(PendingApprovalSchema, {
|
|
350
698
|
toolCallId: tc.id,
|
|
@@ -358,5 +706,5 @@ export function reconstructAdjudicatedApprovals(
|
|
|
358
706
|
}
|
|
359
707
|
}
|
|
360
708
|
|
|
361
|
-
return { pendingApprovals, decisions };
|
|
709
|
+
return { pendingApprovals, decisions, contentDigests };
|
|
362
710
|
}
|