@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
- package/dist/activities/execute-deep-agent/hitl.js +5 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +370 -56
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
- package/dist/activities/execute-deep-agent/status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +15 -1
- package/src/activities/execute-deep-agent/index.ts +434 -64
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +19 -7
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed snapshot/restore substrate for the apply-then-review HITL
|
|
3
|
+
* subsystem — the sibling of {@link ./git-substrate.js} for paths git cannot
|
|
4
|
+
* snapshot (`.gitignored` and non-git files). It closes the "gap" the git
|
|
5
|
+
* substrate documents at its top: shell/MCP and gitignored writes are not
|
|
6
|
+
* reversible by git, so today they stay on the deny-gate; this module makes
|
|
7
|
+
* gitignored / non-git file edits capturable, reviewable, and reversible.
|
|
8
|
+
*
|
|
9
|
+
* THE MODEL (mirrors the git substrate, different storage domain)
|
|
10
|
+
* --------------------------------------------------------------
|
|
11
|
+
* The git substrate snapshots the whole working tree at the turn boundary and
|
|
12
|
+
* diffs two pinned tree objects. CAS cannot do that: git does not see ignored
|
|
13
|
+
* paths, and a plain directory has no history to recover a pre-edit byte from.
|
|
14
|
+
* So the CAS substrate is fed the exact before/after bytes of each touched path
|
|
15
|
+
* (captured by the harness at mutation time — the deep-agent's
|
|
16
|
+
* CasCaptureFilesystemBackend, or the Cursor hook staging the file before it
|
|
17
|
+
* allows the write) and:
|
|
18
|
+
*
|
|
19
|
+
* 1. Stores each distinct before/after body as a CONTENT-ADDRESSED blob under
|
|
20
|
+
* `artifacts/{executionId}/filereview/cas/blobs/{sha256}` (deduped by hash,
|
|
21
|
+
* idempotent), reusing the existing {@link ArtifactStorage} — no new store.
|
|
22
|
+
* 2. Writes a per-change-set MANIFEST (path -> before/after blob refs) under
|
|
23
|
+
* `artifacts/{executionId}/filereview/cas/{changeSetId}.manifest.json`. The
|
|
24
|
+
* manifest is the durable, authoritative source for the resume-time
|
|
25
|
+
* reconcile — the CAS analogue of the git substrate's pinned refs.
|
|
26
|
+
* 3. On resume, reconciles the workspace to the user's per-file decisions
|
|
27
|
+
* sourced ENTIRELY from the manifest: approved files are written to their
|
|
28
|
+
* "after" bytes; rejected/undecided files are snapped back to their "before"
|
|
29
|
+
* bytes (or removed, for a rejected create). Symmetric and idempotent.
|
|
30
|
+
*
|
|
31
|
+
* DURABILITY DOMAIN (design doc 11 D3)
|
|
32
|
+
* -----------------------------------
|
|
33
|
+
* Unlike git refs, which live inside the repo, CAS blobs + manifests live in
|
|
34
|
+
* artifact storage (a fixed host path in OSS, R2 via the proxy in Cloud) — a
|
|
35
|
+
* DIFFERENT durability domain that must outlive the multi-day approval wait and
|
|
36
|
+
* a sandbox recycle. Because reconcile reads only from the manifest + blobs
|
|
37
|
+
* (never the live tree), it converges regardless of the tree's current state,
|
|
38
|
+
* so a Temporal retry or a recycled workspace is harmless.
|
|
39
|
+
*
|
|
40
|
+
* WHAT THIS MODULE IS NOT
|
|
41
|
+
* -----------------------
|
|
42
|
+
* It is harness-agnostic and wiring-agnostic: it takes explicit before/after
|
|
43
|
+
* bytes, never reads gitignore, never talks to a hook. WHICH paths flow here (vs
|
|
44
|
+
* stay on the deny-gate or block as DIFF_UNREVIEWABLE), and the secret-safety
|
|
45
|
+
* gate (design doc 12), are the harness adapter's concern, applied BEFORE bytes
|
|
46
|
+
* reach this module. This module never persists a path it is not given.
|
|
47
|
+
*
|
|
48
|
+
* @since File-Change HITL Redesign (Phase 3 — CAS)
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
52
|
+
import { dirname, join } from "node:path";
|
|
53
|
+
import {
|
|
54
|
+
FileCaptureClass,
|
|
55
|
+
FileChangeKind,
|
|
56
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
57
|
+
import type { ArtifactStorage } from "../artifact-storage.js";
|
|
58
|
+
import { bytesLookBinary } from "../file-change.js";
|
|
59
|
+
import { sha256Bytes, sha256Hex } from "./digest.js";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reads an artifact's raw bytes back by storage key. Injected so this module
|
|
63
|
+
* stays free of HTTP/transport concerns and is trivially testable; production
|
|
64
|
+
* callers build one with {@link casBlobReader}.
|
|
65
|
+
*/
|
|
66
|
+
export type BlobReader = (storageKey: string) => Promise<Buffer>;
|
|
67
|
+
|
|
68
|
+
/** A stored, content-addressed blob and where to find it. */
|
|
69
|
+
export interface CasBlobRef {
|
|
70
|
+
/** Lowercase-hex SHA-256 of the bytes (the content address + enforcement digest). */
|
|
71
|
+
readonly sha256: string;
|
|
72
|
+
/** Artifact-storage key the bytes live under. */
|
|
73
|
+
readonly storageKey: string;
|
|
74
|
+
readonly sizeBytes: number;
|
|
75
|
+
/** A NUL byte was present — the diff cannot render as text (blocks COMPLETE). */
|
|
76
|
+
readonly isBinary: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One captured file within a CAS change set (before/after blob refs). */
|
|
80
|
+
export interface CasCapturedFile {
|
|
81
|
+
/** Path before the change (workspace-relative). Empty for ADD. */
|
|
82
|
+
readonly pathBefore: string;
|
|
83
|
+
/** Path after the change. Empty for DELETE. */
|
|
84
|
+
readonly pathAfter: string;
|
|
85
|
+
readonly kind: FileChangeKind;
|
|
86
|
+
/** Which substrate class captured it (GIT_IGNORED_CAPTURED | NON_GIT_CAS). */
|
|
87
|
+
readonly captureClass: FileCaptureClass;
|
|
88
|
+
/** Pre-edit blob; absent for ADD. */
|
|
89
|
+
readonly before?: CasBlobRef;
|
|
90
|
+
/** Post-edit blob; absent for DELETE. */
|
|
91
|
+
readonly after?: CasBlobRef;
|
|
92
|
+
/** False when a side is binary — the change set cannot be approved as complete. */
|
|
93
|
+
readonly diffComplete: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The durable manifest for one change set — the reconcile's source of truth. */
|
|
97
|
+
export interface CasManifest {
|
|
98
|
+
readonly changeSetId: string;
|
|
99
|
+
readonly files: readonly CasCapturedFile[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A pointer to a persisted {@link CasManifest} (mirrors the proto CasManifestRef). */
|
|
103
|
+
export interface CasSnapshotRef {
|
|
104
|
+
/** SHA-256 over the canonical manifest JSON (tamper/integrity check). */
|
|
105
|
+
readonly manifestDigest: string;
|
|
106
|
+
/** Artifact-storage key the manifest JSON lives under. */
|
|
107
|
+
readonly artifactUri: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The before/after bytes of one path the harness captured this turn. */
|
|
111
|
+
export interface CasPathCapture {
|
|
112
|
+
/** Workspace-root-relative path. */
|
|
113
|
+
readonly path: string;
|
|
114
|
+
/** Pre-edit bytes; `null` when the file did not exist before (an ADD). */
|
|
115
|
+
readonly before: Uint8Array | null;
|
|
116
|
+
/** Post-edit bytes; `null` when the file was removed (a DELETE). */
|
|
117
|
+
readonly after: Uint8Array | null;
|
|
118
|
+
/** Which substrate class this path belongs to (for the ledger). */
|
|
119
|
+
readonly captureClass: FileCaptureClass;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Key namespace (design doc 08: execution-scoped, reuse existing authz)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/** The CAS blob key for a content hash, under the execution's artifact prefix. */
|
|
127
|
+
export function casBlobKey(executionId: string, sha256: string): string {
|
|
128
|
+
return `artifacts/${executionId}/filereview/cas/blobs/${sha256}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The manifest key for a change set. The change-set id (`{exec}:{turnSeq}`)
|
|
133
|
+
* carries a colon; it is sanitized to a filesystem/object-store-safe segment so
|
|
134
|
+
* the key is portable across the local FS and R2 backends.
|
|
135
|
+
*/
|
|
136
|
+
export function casManifestKey(executionId: string, changeSetId: string): string {
|
|
137
|
+
const safe = changeSetId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
138
|
+
return `artifacts/${executionId}/filereview/cas/${safe}.manifest.json`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Capture
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Capture a CAS change set: store every distinct before/after body as a
|
|
147
|
+
* content-addressed blob (deduped, idempotent), assemble the per-file manifest,
|
|
148
|
+
* persist it, and return the manifest plus its ref for the ledger's SnapshotRef.
|
|
149
|
+
*
|
|
150
|
+
* Captures whose before and after bytes are byte-identical (a path touched but
|
|
151
|
+
* left unchanged) are dropped — they are not a reviewable change, exactly as the
|
|
152
|
+
* git substrate's diff omits unchanged files.
|
|
153
|
+
*/
|
|
154
|
+
export async function snapshotCasChangeSet(opts: {
|
|
155
|
+
readonly storage: ArtifactStorage;
|
|
156
|
+
readonly executionId: string;
|
|
157
|
+
readonly changeSetId: string;
|
|
158
|
+
readonly captures: readonly CasPathCapture[];
|
|
159
|
+
}): Promise<{ manifest: CasManifest; ref: CasSnapshotRef }> {
|
|
160
|
+
const { storage, executionId, changeSetId, captures } = opts;
|
|
161
|
+
|
|
162
|
+
const files: CasCapturedFile[] = [];
|
|
163
|
+
for (const capture of captures) {
|
|
164
|
+
const file = await buildCasCapturedFile(storage, executionId, capture);
|
|
165
|
+
if (file) files.push(file);
|
|
166
|
+
}
|
|
167
|
+
// Deterministic order (by after path, then before path) so the manifest digest
|
|
168
|
+
// is stable and, later, composes with the git manifest into one aggregate
|
|
169
|
+
// digest that is byte-identical across editions (design doc 08 D3).
|
|
170
|
+
files.sort(compareByPath);
|
|
171
|
+
|
|
172
|
+
const manifest: CasManifest = { changeSetId, files };
|
|
173
|
+
const canonical = canonicalManifestJson(manifest);
|
|
174
|
+
const manifestKey = casManifestKey(executionId, changeSetId);
|
|
175
|
+
await storage.upload(manifestKey, Buffer.from(canonical, "utf8"), "application/json");
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
manifest,
|
|
179
|
+
ref: { manifestDigest: sha256Hex(canonical), artifactUri: manifestKey },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Store one path's before/after blobs and classify the change. Returns
|
|
185
|
+
* `undefined` for a no-op (both sides absent, or an unchanged touch).
|
|
186
|
+
*/
|
|
187
|
+
async function buildCasCapturedFile(
|
|
188
|
+
storage: ArtifactStorage,
|
|
189
|
+
executionId: string,
|
|
190
|
+
capture: CasPathCapture,
|
|
191
|
+
): Promise<CasCapturedFile | undefined> {
|
|
192
|
+
const { path, before, after, captureClass } = capture;
|
|
193
|
+
if (before === null && after === null) return undefined;
|
|
194
|
+
|
|
195
|
+
const beforeBuf = before === null ? null : Buffer.from(before);
|
|
196
|
+
const afterBuf = after === null ? null : Buffer.from(after);
|
|
197
|
+
|
|
198
|
+
// A touch that did not change the bytes is not a reviewable change.
|
|
199
|
+
if (beforeBuf && afterBuf && beforeBuf.equals(afterBuf)) return undefined;
|
|
200
|
+
|
|
201
|
+
const beforeRef = beforeBuf ? await storeBlob(storage, executionId, beforeBuf) : undefined;
|
|
202
|
+
const afterRef = afterBuf ? await storeBlob(storage, executionId, afterBuf) : undefined;
|
|
203
|
+
|
|
204
|
+
const kind =
|
|
205
|
+
beforeRef === undefined
|
|
206
|
+
? FileChangeKind.ADD
|
|
207
|
+
: afterRef === undefined
|
|
208
|
+
? FileChangeKind.DELETE
|
|
209
|
+
: FileChangeKind.MODIFY;
|
|
210
|
+
|
|
211
|
+
const isCreate = kind === FileChangeKind.ADD;
|
|
212
|
+
const isDelete = kind === FileChangeKind.DELETE;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
pathBefore: isCreate ? "" : path,
|
|
216
|
+
pathAfter: isDelete ? "" : path,
|
|
217
|
+
kind,
|
|
218
|
+
captureClass,
|
|
219
|
+
before: beforeRef,
|
|
220
|
+
after: afterRef,
|
|
221
|
+
// Binary on either side means the diff cannot render as text; the change set
|
|
222
|
+
// then cannot be approved as complete (parity with the git substrate).
|
|
223
|
+
diffComplete: !(beforeRef?.isBinary || afterRef?.isBinary),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Upload bytes under their content-address, skipping the write when present. */
|
|
228
|
+
async function storeBlob(
|
|
229
|
+
storage: ArtifactStorage,
|
|
230
|
+
executionId: string,
|
|
231
|
+
bytes: Buffer,
|
|
232
|
+
): Promise<CasBlobRef> {
|
|
233
|
+
const sha256 = sha256Bytes(bytes);
|
|
234
|
+
const storageKey = casBlobKey(executionId, sha256);
|
|
235
|
+
// Content-addressed: identical bytes yield the same key, so an existing blob
|
|
236
|
+
// is never re-uploaded (idempotent under Temporal retries + intra-turn dedup).
|
|
237
|
+
if (!(await storage.exists(storageKey))) {
|
|
238
|
+
await storage.upload(storageKey, bytes, "application/octet-stream");
|
|
239
|
+
}
|
|
240
|
+
return { sha256, storageKey, sizeBytes: bytes.length, isBinary: bytesLookBinary(bytes) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Resume: load + reconcile
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Load a change set's CAS manifest for the resume-time reconcile, digest-verified
|
|
249
|
+
* against the ledger's {@link CasSnapshotRef}.
|
|
250
|
+
*
|
|
251
|
+
* Called ONLY when the caller has already determined this turn captured CAS files
|
|
252
|
+
* — from the change set's CANDIDATE snapshot (the ledger's record of the
|
|
253
|
+
* substrate), never by probing storage. The ref is the whole contract: the
|
|
254
|
+
* manifest is fetched from the exact `artifactUri` the CANDIDATE event recorded
|
|
255
|
+
* (not a re-derived key, so a pending change set survives any future key-scheme
|
|
256
|
+
* change), and the downloaded bytes must hash to `manifestDigest` before they are
|
|
257
|
+
* trusted. Per-blob content addresses protect blob BODIES, but only this check
|
|
258
|
+
* protects the path→blob mapping itself — a corrupted or swapped manifest could
|
|
259
|
+
* otherwise direct a valid blob to the wrong path.
|
|
260
|
+
*
|
|
261
|
+
* Fail-closed: a missing manifest (surfaced by the reader) or a digest mismatch
|
|
262
|
+
* is a genuine integrity error, never a "this was a git-only turn" signal.
|
|
263
|
+
* (Deciding the substrate from storage existence was unsafe: a presigned/proxy
|
|
264
|
+
* backend cannot answer "does this key exist?" reliably, and a git-only turn —
|
|
265
|
+
* which writes no manifest — would then attempt a doomed download.)
|
|
266
|
+
*/
|
|
267
|
+
export async function loadCasManifest(opts: {
|
|
268
|
+
readonly readBlob: BlobReader;
|
|
269
|
+
readonly ref: CasSnapshotRef;
|
|
270
|
+
}): Promise<CasManifest> {
|
|
271
|
+
const { readBlob, ref } = opts;
|
|
272
|
+
const bytes = await readBlob(ref.artifactUri);
|
|
273
|
+
const actual = sha256Bytes(bytes);
|
|
274
|
+
if (actual !== ref.manifestDigest) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`CAS manifest integrity check failed for '${ref.artifactUri}': ` +
|
|
277
|
+
`expected ${ref.manifestDigest}, got ${actual}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return JSON.parse(bytes.toString("utf8")) as CasManifest;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Ensure the APPROVED files hold their captured "after" bytes on disk. An
|
|
285
|
+
* approved CREATE/MODIFY writes the after blob (integrity-checked against its
|
|
286
|
+
* content address); an approved DELETE removes the file. Idempotent — re-running
|
|
287
|
+
* converges (the after blob is immutable), so a Temporal retry is safe.
|
|
288
|
+
*
|
|
289
|
+
* Enforcement ("what you approve is what applies"): the downloaded bytes must
|
|
290
|
+
* still hash to the blob's content address; a mismatch (storage corruption)
|
|
291
|
+
* throws rather than writing wrong bytes.
|
|
292
|
+
*/
|
|
293
|
+
export async function applyCasApproved(opts: {
|
|
294
|
+
readonly readBlob: BlobReader;
|
|
295
|
+
readonly workspaceRoot: string;
|
|
296
|
+
readonly files: readonly CasCapturedFile[];
|
|
297
|
+
}): Promise<void> {
|
|
298
|
+
const { readBlob, workspaceRoot, files } = opts;
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
if (file.kind === FileChangeKind.DELETE) {
|
|
301
|
+
await rm(join(workspaceRoot, file.pathBefore), { force: true });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// CREATE or MODIFY: write the approved after bytes.
|
|
305
|
+
if (!file.after) continue;
|
|
306
|
+
await writeVerifiedBlob(readBlob, join(workspaceRoot, file.pathAfter), file.after);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Snap the REJECTED/undecided files back to their captured "before" bytes: a
|
|
312
|
+
* rejected CREATE is removed; a rejected MODIFY/DELETE is rewritten with its
|
|
313
|
+
* before blob. Idempotent, and the byte-exact inverse of {@link applyCasApproved}.
|
|
314
|
+
*/
|
|
315
|
+
export async function restoreCasToBaseline(opts: {
|
|
316
|
+
readonly readBlob: BlobReader;
|
|
317
|
+
readonly workspaceRoot: string;
|
|
318
|
+
readonly files: readonly CasCapturedFile[];
|
|
319
|
+
}): Promise<void> {
|
|
320
|
+
const { readBlob, workspaceRoot, files } = opts;
|
|
321
|
+
for (const file of files) {
|
|
322
|
+
if (file.kind === FileChangeKind.ADD) {
|
|
323
|
+
// The file did not exist before this turn — remove the agent's creation.
|
|
324
|
+
await rm(join(workspaceRoot, file.pathAfter), { force: true });
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// MODIFY or DELETE: the baseline holds the file — restore its exact bytes.
|
|
328
|
+
if (!file.before) continue;
|
|
329
|
+
await writeVerifiedBlob(readBlob, join(workspaceRoot, file.pathBefore), file.before);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Adapt the artifact storage port's {@link ArtifactStorage.download} into a
|
|
335
|
+
* {@link BlobReader}. The seam is retained so the reconcile functions
|
|
336
|
+
* ({@link loadCasManifest}, {@link applyCasApproved}, {@link restoreCasToBaseline})
|
|
337
|
+
* depend only on "read one blob by key", not on the whole storage interface.
|
|
338
|
+
* `download` already throws a descriptive, key-scoped error on a missing blob or
|
|
339
|
+
* transport failure, so no extra wrapping is needed here.
|
|
340
|
+
*/
|
|
341
|
+
export function casBlobReader(storage: ArtifactStorage): BlobReader {
|
|
342
|
+
return (storageKey: string): Promise<Buffer> => storage.download(storageKey);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Internals
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/** Download a blob, verify its content address, and write it to `abs`. */
|
|
350
|
+
async function writeVerifiedBlob(
|
|
351
|
+
readBlob: BlobReader,
|
|
352
|
+
abs: string,
|
|
353
|
+
ref: CasBlobRef,
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
const bytes = await readBlob(ref.storageKey);
|
|
356
|
+
const actual = sha256Bytes(bytes);
|
|
357
|
+
if (actual !== ref.sha256) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`CAS blob integrity check failed for '${ref.storageKey}': ` +
|
|
360
|
+
`expected ${ref.sha256}, got ${actual}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
364
|
+
await writeFile(abs, bytes);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Stable order: by after path, then before path (substrate-agnostic, doc 08 D3). */
|
|
368
|
+
function compareByPath(a: CasCapturedFile, b: CasCapturedFile): number {
|
|
369
|
+
const ka = a.pathAfter || a.pathBefore;
|
|
370
|
+
const kb = b.pathAfter || b.pathBefore;
|
|
371
|
+
if (ka < kb) return -1;
|
|
372
|
+
if (ka > kb) return 1;
|
|
373
|
+
return a.pathBefore < b.pathBefore ? -1 : a.pathBefore > b.pathBefore ? 1 : 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Serialize a blob ref fully so the persisted manifest round-trips faithfully. */
|
|
377
|
+
function serializeBlob(b: CasBlobRef | undefined): object | null {
|
|
378
|
+
return b
|
|
379
|
+
? { sha256: b.sha256, storageKey: b.storageKey, sizeBytes: b.sizeBytes, isBinary: b.isBinary }
|
|
380
|
+
: null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The canonical JSON the manifest is BOTH persisted as and digested over: files
|
|
385
|
+
* in sorted order with a fixed key order, blob refs serialized in full (incl.
|
|
386
|
+
* `storageKey`) so a reloaded manifest is a faithful {@link CasManifest} the
|
|
387
|
+
* resume reconcile can read directly. Every field is deterministic, so the digest
|
|
388
|
+
* is a stable integrity check over the exact stored bytes.
|
|
389
|
+
*/
|
|
390
|
+
function canonicalManifestJson(manifest: CasManifest): string {
|
|
391
|
+
const files = manifest.files.map((f) => ({
|
|
392
|
+
pathBefore: f.pathBefore,
|
|
393
|
+
pathAfter: f.pathAfter,
|
|
394
|
+
kind: f.kind,
|
|
395
|
+
captureClass: f.captureClass,
|
|
396
|
+
before: serializeBlob(f.before),
|
|
397
|
+
after: serializeBlob(f.after),
|
|
398
|
+
diffComplete: f.diffComplete,
|
|
399
|
+
}));
|
|
400
|
+
return JSON.stringify({ changeSetId: manifest.changeSetId, files });
|
|
401
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical file-review digests — the runner-side mirror of the Go
|
|
3
|
+
* {@code filereview/digest.go} and the Java {@code FileDigest}, locked
|
|
4
|
+
* byte-for-byte by `apis/testdata/hitl/file-digest/vectors.json`.
|
|
5
|
+
*
|
|
6
|
+
* These are ENFORCEMENT digests, never correlation keys: the reconcile refuses
|
|
7
|
+
* to apply content whose digest differs from what the user approved. The runner
|
|
8
|
+
* is the sole producer of `before_sha256`/`after_sha256` (over the exact captured
|
|
9
|
+
* bytes) and of the derived `file_digest`/`aggregate_digest`; the backends only
|
|
10
|
+
* carry them. Keeping the formula identical across all three editions is what
|
|
11
|
+
* lets the cross-edition corpus prove parity.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The proto enum value NAME for a {@link FileChangeKind}, e.g.
|
|
19
|
+
* `FILE_CHANGE_KIND_MODIFY`. The digest is computed over this name (matching Go's
|
|
20
|
+
* `kind.String()` and Java's `kind.name()`), NOT the TS member name, so all three
|
|
21
|
+
* editions hash an identical string. The cross-edition digest corpus validates
|
|
22
|
+
* this map; a missing or wrong entry fails the parity test.
|
|
23
|
+
*/
|
|
24
|
+
const FILE_CHANGE_KIND_NAME: Readonly<Record<FileChangeKind, string>> = {
|
|
25
|
+
[FileChangeKind.UNSPECIFIED]: "FILE_CHANGE_KIND_UNSPECIFIED",
|
|
26
|
+
[FileChangeKind.ADD]: "FILE_CHANGE_KIND_ADD",
|
|
27
|
+
[FileChangeKind.MODIFY]: "FILE_CHANGE_KIND_MODIFY",
|
|
28
|
+
[FileChangeKind.DELETE]: "FILE_CHANGE_KIND_DELETE",
|
|
29
|
+
[FileChangeKind.RENAME]: "FILE_CHANGE_KIND_RENAME",
|
|
30
|
+
[FileChangeKind.BINARY_CHANGE]: "FILE_CHANGE_KIND_BINARY_CHANGE",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The proto enum value name for a FileChangeKind (for the digest canonical form). */
|
|
34
|
+
export function fileChangeKindName(kind: FileChangeKind): string {
|
|
35
|
+
return FILE_CHANGE_KIND_NAME[kind] ?? "FILE_CHANGE_KIND_UNSPECIFIED";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Lowercase-hex SHA-256 of a UTF-8 string. */
|
|
39
|
+
export function sha256Hex(s: string): string {
|
|
40
|
+
return createHash("sha256").update(s, "utf8").digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Lowercase-hex SHA-256 of raw bytes — used for `before_sha256`/`after_sha256`. */
|
|
44
|
+
export function sha256Bytes(bytes: Uint8Array): string {
|
|
45
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The identity+content fields a {@link fileDigest} is computed over. */
|
|
49
|
+
export interface FileDigestInput {
|
|
50
|
+
readonly pathBefore: string;
|
|
51
|
+
readonly pathAfter: string;
|
|
52
|
+
readonly kind: FileChangeKind;
|
|
53
|
+
readonly beforeSha256: string;
|
|
54
|
+
readonly afterSha256: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The canonical per-file digest:
|
|
59
|
+
* `sha256(join("\n", [path_before, path_after, kind_name, before_sha256, after_sha256]))`.
|
|
60
|
+
* Digesting the content HASHES (not raw bytes) keeps the string pure ASCII, free
|
|
61
|
+
* of any line-ending or encoding ambiguity across editions.
|
|
62
|
+
*/
|
|
63
|
+
export function fileDigest(input: FileDigestInput): string {
|
|
64
|
+
return sha256Hex(
|
|
65
|
+
[
|
|
66
|
+
input.pathBefore,
|
|
67
|
+
input.pathAfter,
|
|
68
|
+
fileChangeKindName(input.kind),
|
|
69
|
+
input.beforeSha256,
|
|
70
|
+
input.afterSha256,
|
|
71
|
+
].join("\n"),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The canonical change-set digest: `sha256(join("\n", sort_ascending(file_digests)))`.
|
|
77
|
+
* Sorting makes it independent of file order (a change set is an unordered set of
|
|
78
|
+
* files for identity). The empty set hashes the empty string.
|
|
79
|
+
*/
|
|
80
|
+
export function aggregateDigest(inputs: readonly FileDigestInput[]): string {
|
|
81
|
+
const digests = inputs.map(fileDigest).sort();
|
|
82
|
+
return sha256Hex(digests.join("\n"));
|
|
83
|
+
}
|