@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
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
12
|
import { describe, it, expect } from "vitest";
|
|
13
13
|
import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
14
|
-
import { classifyTool } from "../tool-kind.js";
|
|
14
|
+
import { classifyTool, toolApprovalCategory } from "../tool-kind.js";
|
|
15
15
|
|
|
16
16
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
// runner/src/shared/__tests__ -> repo root is five levels up.
|
|
@@ -56,3 +56,26 @@ describe("classifyTool", () => {
|
|
|
56
56
|
expect(classifyTool("read", "planton")).toBe(ToolKind.FILE_READ);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe("toolApprovalCategory", () => {
|
|
61
|
+
it("collapses write AND edit (both taxonomies) onto 'write'", () => {
|
|
62
|
+
for (const name of ["write", "write_file", "Write", "edit", "edit_file", "StrReplace", "EditNotebook"]) {
|
|
63
|
+
expect(toolApprovalCategory(name)).toBe("write");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("maps the delete and shell aliases to their category", () => {
|
|
68
|
+
for (const name of ["delete", "delete_file", "remove_file", "Delete"]) {
|
|
69
|
+
expect(toolApprovalCategory(name)).toBe("delete");
|
|
70
|
+
}
|
|
71
|
+
for (const name of ["shell", "bash", "execute", "execute_command", "run_command", "terminal", "Shell"]) {
|
|
72
|
+
expect(toolApprovalCategory(name)).toBe("shell");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns undefined for read-only and unknown built-ins (not gated by category)", () => {
|
|
77
|
+
for (const name of ["read", "ls", "glob", "grep", "search", "think", "totally_unknown"]) {
|
|
78
|
+
expect(toolApprovalCategory(name)).toBeUndefined();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the shared tool-row file-review presentation helpers:
|
|
3
|
+
* `stampFileEditRow` (the observational-row stamp both harnesses apply to
|
|
4
|
+
* flowed file edits) and `hideToolCallRow`/`isToolCallRowHidden` (the collapse
|
|
5
|
+
* shape kept for denial twins and legacy pre-stamping sessions).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { create } from "@bufbuild/protobuf";
|
|
10
|
+
import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
11
|
+
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
12
|
+
import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
13
|
+
import { collectSubAgentToolCallIds, hideToolCallRow, isToolCallRowHidden, stampFileEditRow } from "../tool-row.js";
|
|
14
|
+
|
|
15
|
+
describe("stampFileEditRow", () => {
|
|
16
|
+
it("stamps additively: content, status, and identity all survive", () => {
|
|
17
|
+
const tc = create(ToolCallSchema, {
|
|
18
|
+
id: "tc-1",
|
|
19
|
+
name: "edit",
|
|
20
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
21
|
+
result: '{"status":"completed","value":{"diffString":"+hi","linesAdded":1}}',
|
|
22
|
+
argsPreview: '{"path":"src/app.ts"}',
|
|
23
|
+
args: { path: "src/app.ts", old_string: "a", new_string: "b" },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
stampFileEditRow(tc, "exec-1:0");
|
|
27
|
+
|
|
28
|
+
expect(tc.fileChangeSetId).toBe("exec-1:0");
|
|
29
|
+
// The row is an observational record: everything the streamed card rendered
|
|
30
|
+
// from stays in place (the diff renders from result/args, never argsPreview).
|
|
31
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
32
|
+
expect(tc.args).toEqual({ path: "src/app.ts", old_string: "a", new_string: "b" });
|
|
33
|
+
expect(tc.result).toContain("diffString");
|
|
34
|
+
expect(tc.argsPreview).toBe('{"path":"src/app.ts"}');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("never overwrites an existing stamp (the cross-turn mis-attribution guard)", () => {
|
|
38
|
+
// A resume seeds prior turns' rows into the transcript; the turn-boundary
|
|
39
|
+
// pass re-walks all of them. The stamp must be first-writer-wins or turn N
|
|
40
|
+
// would claim turn N-1's rows.
|
|
41
|
+
const tc = create(ToolCallSchema, {
|
|
42
|
+
id: "tc-old",
|
|
43
|
+
name: "write",
|
|
44
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
45
|
+
args: { path: "notes.md", content: "x" },
|
|
46
|
+
fileChangeSetId: "exec-1:0",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
stampFileEditRow(tc, "exec-1:1");
|
|
50
|
+
|
|
51
|
+
expect(tc.fileChangeSetId).toBe("exec-1:0");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("withholds content for a secret-like TRACKED path but keeps the path visible (DD-12 D4)", () => {
|
|
55
|
+
// The hook denies secret-like gitignored writes before they flow, but a
|
|
56
|
+
// committed credentials file is outside its scope — the stamp is the last
|
|
57
|
+
// line of defense against persisting its bytes in the transcript.
|
|
58
|
+
const tc = create(ToolCallSchema, {
|
|
59
|
+
id: "tc-secret",
|
|
60
|
+
name: "write",
|
|
61
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
62
|
+
result: "wrote credentials.json",
|
|
63
|
+
argsPreview: '{"path":"config/credentials.json"}',
|
|
64
|
+
args: { path: "config/credentials.json", content: "TOKEN=super-secret" },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
stampFileEditRow(tc, "exec-1:0");
|
|
68
|
+
|
|
69
|
+
expect(tc.fileChangeSetId).toBe("exec-1:0");
|
|
70
|
+
expect(tc.args).toEqual({ path: "config/credentials.json" }); // path only, no body
|
|
71
|
+
expect(tc.result).toBe("");
|
|
72
|
+
expect(tc.argsPreview).toBe("");
|
|
73
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED); // still visible
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("fail-closes when the path cannot be determined: content withheld", () => {
|
|
77
|
+
const tc = create(ToolCallSchema, {
|
|
78
|
+
id: "tc-weird",
|
|
79
|
+
name: "write",
|
|
80
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
81
|
+
result: "ok",
|
|
82
|
+
args: { unrecognized_shape: "payload" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
stampFileEditRow(tc, "exec-1:0");
|
|
86
|
+
|
|
87
|
+
expect(tc.fileChangeSetId).toBe("exec-1:0");
|
|
88
|
+
expect(tc.args).toBeUndefined();
|
|
89
|
+
expect(tc.result).toBe("");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("hideToolCallRow", () => {
|
|
94
|
+
it("collapses a completed file-edit row to the hidden shape", () => {
|
|
95
|
+
const tc = create(ToolCallSchema, {
|
|
96
|
+
id: "tc-1",
|
|
97
|
+
name: "write",
|
|
98
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
99
|
+
result: "wrote file",
|
|
100
|
+
argsPreview: "path=src/app.ts",
|
|
101
|
+
args: { file_path: "src/app.ts", content: "console.log('hi')" },
|
|
102
|
+
requiresApproval: true,
|
|
103
|
+
approvalMessage: "Write file",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
hideToolCallRow(tc);
|
|
107
|
+
|
|
108
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
|
|
109
|
+
expect(tc.requiresApproval).toBe(false);
|
|
110
|
+
expect(tc.result).toBe("");
|
|
111
|
+
expect(tc.error).toBe("");
|
|
112
|
+
expect(tc.argsPreview).toBe("");
|
|
113
|
+
expect(tc.approvalMessage).toBe("");
|
|
114
|
+
expect(tc.completedAt).not.toBe("");
|
|
115
|
+
// Identity is preserved (append-only): id + name survive the collapse.
|
|
116
|
+
expect(tc.id).toBe("tc-1");
|
|
117
|
+
expect(tc.name).toBe("write");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("scrubs args so a hidden row carries no content (design doc 12, D4)", () => {
|
|
121
|
+
// A file-mutating tool's args hold the full write body. For a secret-like
|
|
122
|
+
// path this content would otherwise persist into the transcript / Temporal
|
|
123
|
+
// history, defeating the never-persist-secret-contents contract. The hidden
|
|
124
|
+
// row is redundant with the file_change_set, so args must be dropped.
|
|
125
|
+
const tc = create(ToolCallSchema, {
|
|
126
|
+
id: "tc-secret",
|
|
127
|
+
name: "write",
|
|
128
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
129
|
+
args: { file_path: ".env", content: "API_KEY=super-secret-value" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
hideToolCallRow(tc);
|
|
133
|
+
|
|
134
|
+
expect(tc.args).toBeUndefined();
|
|
135
|
+
expect(isToolCallRowHidden(tc)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("is idempotent", () => {
|
|
139
|
+
const tc = create(ToolCallSchema, {
|
|
140
|
+
id: "tc-1",
|
|
141
|
+
name: "edit",
|
|
142
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
143
|
+
result: "ok",
|
|
144
|
+
});
|
|
145
|
+
hideToolCallRow(tc);
|
|
146
|
+
const firstCompletedAt = tc.completedAt;
|
|
147
|
+
hideToolCallRow(tc);
|
|
148
|
+
expect(isToolCallRowHidden(tc)).toBe(true);
|
|
149
|
+
expect(tc.completedAt).toBe(firstCompletedAt); // not overwritten
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("isToolCallRowHidden", () => {
|
|
154
|
+
it("recognizes a hidden row", () => {
|
|
155
|
+
const tc = create(ToolCallSchema, { id: "x", name: "write", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "y" });
|
|
156
|
+
expect(isToolCallRowHidden(tc)).toBe(false);
|
|
157
|
+
hideToolCallRow(tc);
|
|
158
|
+
expect(isToolCallRowHidden(tc)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("does not treat a live WAITING_APPROVAL row as hidden", () => {
|
|
162
|
+
const tc = create(ToolCallSchema, {
|
|
163
|
+
id: "x",
|
|
164
|
+
name: "shell",
|
|
165
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
166
|
+
requiresApproval: true,
|
|
167
|
+
});
|
|
168
|
+
expect(isToolCallRowHidden(tc)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("does not treat a SKIPPED row with lingering args as hidden", () => {
|
|
172
|
+
// A row collapsed by an older code path may still carry args; the predicate
|
|
173
|
+
// must report it as not-yet-hidden so it gets re-scrubbed rather than skipped.
|
|
174
|
+
const tc = create(ToolCallSchema, {
|
|
175
|
+
id: "x",
|
|
176
|
+
name: "write",
|
|
177
|
+
status: ToolCallStatus.TOOL_CALL_SKIPPED,
|
|
178
|
+
args: { file_path: ".env", content: "leftover" },
|
|
179
|
+
});
|
|
180
|
+
expect(isToolCallRowHidden(tc)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("collectSubAgentToolCallIds", () => {
|
|
185
|
+
function subAgent(id: string, ...toolCallIds: string[]) {
|
|
186
|
+
return create(SubAgentExecutionSchema, {
|
|
187
|
+
id,
|
|
188
|
+
messages: [
|
|
189
|
+
create(AgentMessageSchema, {
|
|
190
|
+
type: 1, // MESSAGE_AI
|
|
191
|
+
toolCalls: toolCallIds.map((tcId) =>
|
|
192
|
+
create(ToolCallSchema, { id: tcId, name: "edit" }),
|
|
193
|
+
),
|
|
194
|
+
}),
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
it("returns an empty set for no sub-agents", () => {
|
|
200
|
+
expect(collectSubAgentToolCallIds([]).size).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("flattens tool-call ids across sub-agents and messages", () => {
|
|
204
|
+
const ids = collectSubAgentToolCallIds([
|
|
205
|
+
subAgent("sa-1", "tc-a", "tc-b"),
|
|
206
|
+
subAgent("sa-2", "tc-c"),
|
|
207
|
+
]);
|
|
208
|
+
expect([...ids].sort()).toEqual(["tc-a", "tc-b", "tc-c"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("collects ids across multiple messages of one sub-agent", () => {
|
|
212
|
+
const sa = create(SubAgentExecutionSchema, {
|
|
213
|
+
id: "sa-1",
|
|
214
|
+
messages: [
|
|
215
|
+
create(AgentMessageSchema, { type: 1, toolCalls: [create(ToolCallSchema, { id: "tc-a", name: "edit" })] }),
|
|
216
|
+
create(AgentMessageSchema, { type: 1, toolCalls: [create(ToolCallSchema, { id: "tc-b", name: "write" })] }),
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
expect([...collectSubAgentToolCallIds([sa]).values()].sort()).toEqual(["tc-a", "tc-b"]);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed Temporal activity input shared by ExecuteCursor and ExecuteDeepAgent.
|
|
3
|
+
*
|
|
4
|
+
* The Go and Java control planes now invoke these activities with a single
|
|
5
|
+
* object carrying snake_case keys (mirroring the polyglot workflow-input
|
|
6
|
+
* convention) instead of positional string args. This ends the prior drift
|
|
7
|
+
* where Go sent (executionId, threadId) and Java sent
|
|
8
|
+
* (executionId, threadId, invokerIdentityAccountId) while this single runner
|
|
9
|
+
* read only the first two positionally.
|
|
10
|
+
*
|
|
11
|
+
* TRANSITIONAL DUAL-SHAPE: the activity is MaximumAttempts=1 (a failed activity
|
|
12
|
+
* fails the execution with no retry), and the control plane and this runner
|
|
13
|
+
* deploy separately. To make the rollout order-independent, the runner accepts
|
|
14
|
+
* BOTH the new object and the legacy positional args. Deploy the runner first,
|
|
15
|
+
* then the control planes; once both are everywhere, drop the positional branch
|
|
16
|
+
* (and the `string` arms below).
|
|
17
|
+
*/
|
|
18
|
+
export interface ExecuteActivityInput {
|
|
19
|
+
readonly execution_id: string;
|
|
20
|
+
readonly thread_id: string;
|
|
21
|
+
/** Carried for cross-edition parity; the runner hydrates the invoker from the DB. */
|
|
22
|
+
readonly invoker_identity_account_id?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Monotonic HITL-cycle index within this execution: 0 on the first invocation,
|
|
25
|
+
* then the workflow's approvalCycle on each reinvocation. The Cursor producer
|
|
26
|
+
* mints its deterministic file-review change-set id ({@code executionId:turnSeq})
|
|
27
|
+
* from it. Absent on the legacy positional wire shape (defaults to 0).
|
|
28
|
+
*/
|
|
29
|
+
readonly turn_seq?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NormalizedActivityInput {
|
|
33
|
+
readonly executionId: string;
|
|
34
|
+
readonly threadId: string;
|
|
35
|
+
/** HITL-cycle index for this invocation; 0 on the first turn / legacy wire shape. */
|
|
36
|
+
readonly turnSeq: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes either the new typed object or the legacy positional args into the
|
|
41
|
+
* `{ executionId, threadId, turnSeq }` the inner activity path consumes.
|
|
42
|
+
* `thread_id` is empty on a first invocation (new harness state) and `turn_seq`
|
|
43
|
+
* defaults to 0 (first turn, or the legacy positional shape that never carried it).
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeActivityInput(
|
|
46
|
+
arg0: ExecuteActivityInput | string | undefined,
|
|
47
|
+
arg1?: string,
|
|
48
|
+
): NormalizedActivityInput {
|
|
49
|
+
if (arg0 !== null && typeof arg0 === "object") {
|
|
50
|
+
return {
|
|
51
|
+
executionId: arg0.execution_id ?? "",
|
|
52
|
+
threadId: arg0.thread_id ?? "",
|
|
53
|
+
turnSeq: arg0.turn_seq ?? 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { executionId: arg0 ?? "", threadId: arg1 ?? "", turnSeq: 0 };
|
|
57
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Deterministic canonicalization of a tool action into a byte-stable form.
|
|
2
|
+
// Domain: HITL approval. This is the cross-language contract the approval
|
|
3
|
+
// fingerprint (Phase 2) will hash; Phase 1 *defines and computes* it without
|
|
4
|
+
// hashing, so Go/Java/TS can be pinned to byte-identical output via the shared
|
|
5
|
+
// vector corpus (apis/testdata/hitl/canonicalization/).
|
|
6
|
+
//
|
|
7
|
+
// Why this exists: the March 2026 dedup bug was caused by hashing divergent
|
|
8
|
+
// representations of the same action (display args in one place, raw args in
|
|
9
|
+
// another). The fix is a single, explicit normalization pass — applied at ONE
|
|
10
|
+
// layer — that erases incidental differences (path separators, workspace
|
|
11
|
+
// location, shell whitespace, MCP slug case, secret values) before any hash.
|
|
12
|
+
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { posix } from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface ToolActionInput {
|
|
17
|
+
// Engine tool name (Cursor PascalCase or native snake_case). Identity is kept
|
|
18
|
+
// as-given (only trimmed) because ToolKind already normalizes cross-harness
|
|
19
|
+
// naming elsewhere; canonicalization must not silently merge distinct tools.
|
|
20
|
+
toolName: string;
|
|
21
|
+
// MCP server slug, or empty/undefined for built-in tools.
|
|
22
|
+
mcpServerSlug?: string;
|
|
23
|
+
// File paths the action targets (absolute or workspace-relative).
|
|
24
|
+
paths?: string[];
|
|
25
|
+
// Shell command line, when this is a shell action.
|
|
26
|
+
shellCommand?: string;
|
|
27
|
+
// Remaining structured arguments.
|
|
28
|
+
args?: Record<string, unknown>;
|
|
29
|
+
// Top-level keys within `args` whose values are secrets: replaced by a stable
|
|
30
|
+
// digest so the canonical form never carries cleartext.
|
|
31
|
+
secretKeys?: string[];
|
|
32
|
+
// Workspace root used to rewrite absolute paths to workspace-relative ones, so
|
|
33
|
+
// the same edit canonicalizes identically regardless of checkout location.
|
|
34
|
+
workspaceRoot?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The normalized action. Field order here is irrelevant — canonicalJson sorts
|
|
38
|
+
// keys — but the shape is the contract Go/Java must reproduce.
|
|
39
|
+
export interface CanonicalToolAction {
|
|
40
|
+
toolName: string;
|
|
41
|
+
mcpServerSlug: string;
|
|
42
|
+
paths: string[];
|
|
43
|
+
shellCommand: string;
|
|
44
|
+
args: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function canonicalizeToolAction(input: ToolActionInput): CanonicalToolAction {
|
|
48
|
+
return {
|
|
49
|
+
toolName: input.toolName.trim(),
|
|
50
|
+
// Slugs are case-insensitive identity; lowercase so "GitHub" == "github".
|
|
51
|
+
mcpServerSlug: (input.mcpServerSlug ?? "").trim().toLowerCase(),
|
|
52
|
+
paths: normalizePaths(input.paths ?? [], input.workspaceRoot),
|
|
53
|
+
shellCommand: normalizeShellCommand(input.shellCommand ?? ""),
|
|
54
|
+
args: redactSecrets(input.args ?? {}, new Set(input.secretKeys ?? [])),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// canonicalToolActionJson is the single entry point: normalize, then serialize
|
|
59
|
+
// to canonical JSON. The returned string is the byte-stable contract value.
|
|
60
|
+
export function canonicalToolActionJson(input: ToolActionInput): string {
|
|
61
|
+
return canonicalJson(canonicalizeToolAction(input));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// canonicalJson serializes a value to RFC 8785-style canonical JSON: object keys
|
|
65
|
+
// sorted by UTF-16 code unit, no insignificant whitespace, UTF-8 output.
|
|
66
|
+
//
|
|
67
|
+
// The input domain is deliberately constrained to strings, booleans, null,
|
|
68
|
+
// integers, arrays, and plain objects — the shape of a normalized tool action.
|
|
69
|
+
// Non-integer numbers are rejected rather than implementing RFC 8785's full
|
|
70
|
+
// number-formatting algorithm, keeping this hand-rolled serializer small and
|
|
71
|
+
// provably cross-language without a third-party dependency. Revisit if the
|
|
72
|
+
// canonical domain ever needs floats.
|
|
73
|
+
export function canonicalJson(value: unknown): string {
|
|
74
|
+
return serialize(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serialize(v: unknown): string {
|
|
78
|
+
if (v === null) return "null";
|
|
79
|
+
switch (typeof v) {
|
|
80
|
+
case "boolean":
|
|
81
|
+
return v ? "true" : "false";
|
|
82
|
+
case "number":
|
|
83
|
+
if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
|
|
84
|
+
if (!Number.isInteger(v)) {
|
|
85
|
+
throw new Error("canonicalJson: non-integer numbers are not part of the canonical contract");
|
|
86
|
+
}
|
|
87
|
+
return String(v);
|
|
88
|
+
case "string":
|
|
89
|
+
// JSON.stringify produces the same minimal escaping and raw UTF-8 for
|
|
90
|
+
// non-ASCII that RFC 8785 mandates for these inputs.
|
|
91
|
+
return JSON.stringify(v);
|
|
92
|
+
case "object":
|
|
93
|
+
if (Array.isArray(v)) return "[" + v.map(serialize).join(",") + "]";
|
|
94
|
+
return serializeObject(v as Record<string, unknown>);
|
|
95
|
+
default:
|
|
96
|
+
throw new Error(`canonicalJson: unsupported type ${typeof v}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function serializeObject(obj: Record<string, unknown>): string {
|
|
101
|
+
// `undefined`-valued keys are dropped (they have no JSON representation);
|
|
102
|
+
// remaining keys are sorted by UTF-16 code unit, which is JS's default string
|
|
103
|
+
// ordering and matches RFC 8785.
|
|
104
|
+
const keys = Object.keys(obj)
|
|
105
|
+
.filter((k) => obj[k] !== undefined)
|
|
106
|
+
.sort(compareUtf16);
|
|
107
|
+
const parts = keys.map((k) => JSON.stringify(k) + ":" + serialize(obj[k]));
|
|
108
|
+
return "{" + parts.join(",") + "}";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function compareUtf16(a: string, b: string): number {
|
|
112
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizePaths(paths: string[], workspaceRoot?: string): string[] {
|
|
116
|
+
return paths.map((p) => normalizeOnePath(p, workspaceRoot)).sort(compareUtf16);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeOnePath(p: string, workspaceRoot?: string): string {
|
|
120
|
+
// Separator-normalize to forward slashes first so Windows and POSIX agree.
|
|
121
|
+
let s = p.replace(/\\/g, "/");
|
|
122
|
+
if (workspaceRoot) {
|
|
123
|
+
const root = workspaceRoot.replace(/\\/g, "/");
|
|
124
|
+
if (s === root) {
|
|
125
|
+
s = "";
|
|
126
|
+
} else if (s.startsWith(root + "/")) {
|
|
127
|
+
s = s.slice(root.length + 1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Collapse `.`/`..` segments deterministically. posix.normalize("") -> ".".
|
|
131
|
+
return s === "" ? "" : posix.normalize(s);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeShellCommand(cmd: string): string {
|
|
135
|
+
// Collapse runs of whitespace to a single space and trim. The argument VALUE
|
|
136
|
+
// (the command) is what matters; incidental spacing is not identity.
|
|
137
|
+
return cmd.replace(/\s+/g, " ").trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function redactSecrets(
|
|
141
|
+
args: Record<string, unknown>,
|
|
142
|
+
secretKeys: Set<string>,
|
|
143
|
+
): Record<string, unknown> {
|
|
144
|
+
if (secretKeys.size === 0) return args;
|
|
145
|
+
const out: Record<string, unknown> = {};
|
|
146
|
+
for (const [k, v] of Object.entries(args)) {
|
|
147
|
+
out[k] = secretKeys.has(k) ? redactValue(v) : v;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function redactValue(v: unknown): string {
|
|
153
|
+
// Redact-but-stable: a SHA-256 digest keeps the canonical form stable across
|
|
154
|
+
// runs without ever placing the secret in cleartext. This is NOT the approval
|
|
155
|
+
// fingerprint — that is an HMAC keyed on a Stigmer secret, introduced in
|
|
156
|
+
// Phase 2. This digest is unkeyed redaction only.
|
|
157
|
+
const material = typeof v === "string" ? v : canonicalJson(v);
|
|
158
|
+
return "sha256:" + createHash("sha256").update(material, "utf8").digest("hex");
|
|
159
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval fingerprint — the exact-match enforcement identity for the HITL Tool
|
|
3
|
+
* Execution Gateway (Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* A fingerprint is `HMAC-SHA256(key, canonicalForm)` over a canonicalized tool
|
|
6
|
+
* action (see {@link file://./approval-canonicalize.ts}). It answers exactly one
|
|
7
|
+
* question at the moment of side effect: "is *this* action the one the user
|
|
8
|
+
* approved?" An approval is bound to the canonical identity of the action, not to
|
|
9
|
+
* the transient id of the proposing tool call, so a re-driven action that differs
|
|
10
|
+
* from what was approved is re-asked — never silently executed.
|
|
11
|
+
*
|
|
12
|
+
* Why HMAC and not a bare SHA-256 (binding canonical decision,
|
|
13
|
+
* design-decisions/approval-fingerprint-vs-march-rollback.md, Rule 4): the
|
|
14
|
+
* fingerprint is an authorization token, not a correlation key. Keying it under a
|
|
15
|
+
* Stigmer-held secret means a model (or a compromised workspace) cannot forge a
|
|
16
|
+
* value that the gateway will accept. In Phase 2 the fingerprint is
|
|
17
|
+
* recompute-and-compare at one trusted layer (no bearer token crosses a wire), so
|
|
18
|
+
* the anti-forgery property is forward-looking — it earns its keep when a lease
|
|
19
|
+
* later becomes a server-issued bearer token (Phase 7). We pay the small cost now
|
|
20
|
+
* to avoid a migration that re-keys every persisted approval later.
|
|
21
|
+
*
|
|
22
|
+
* Two fidelities share this one canonicalization core, because the two
|
|
23
|
+
* enforcement substrates differ (see the Phase-2 plan, "two enforcement
|
|
24
|
+
* substrates, not one"):
|
|
25
|
+
*
|
|
26
|
+
* - FULL ({@link computeApprovalFingerprint}) — HMAC over the full canonical
|
|
27
|
+
* action (tool + paths + shellCommand + args). Used by the in-process
|
|
28
|
+
* deep-agent gateway, where approve-time and execute-time observe the same
|
|
29
|
+
* arg shape (LangGraph checkpoint replay), so the action can be matched at
|
|
30
|
+
* full fidelity.
|
|
31
|
+
* - COARSE ({@link computeCoarseApprovalFingerprint}) — HMAC over
|
|
32
|
+
* (category, salient) only. Used by the out-of-process Cursor deny-oracle
|
|
33
|
+
* hook, whose stdin payload names the same action with a different taxonomy
|
|
34
|
+
* (`Write` vs `edit`, `file_path` vs `path`) and cannot reproduce the full
|
|
35
|
+
* args. The coarse projection is the documented, substrate-forced coarsening
|
|
36
|
+
* that lets the hook-side and stream-side fingerprints agree; it is the
|
|
37
|
+
* successor identity for execute-cursor's grant token.
|
|
38
|
+
*
|
|
39
|
+
* The fingerprint is enforcement-only and never a correlation key — correlation
|
|
40
|
+
* stays `approval_request_id` + `tool_call_id`.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { createHmac, type BinaryLike } from "node:crypto";
|
|
44
|
+
import {
|
|
45
|
+
canonicalJson,
|
|
46
|
+
canonicalToolActionJson,
|
|
47
|
+
canonicalizeToolAction,
|
|
48
|
+
type ToolActionInput,
|
|
49
|
+
} from "./approval-canonicalize.js";
|
|
50
|
+
import { toolApprovalCategory } from "./tool-kind.js";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Version tag prefixed to every fingerprint. Bumping it is the migration lever
|
|
54
|
+
* if the canonical form or the MAC primitive ever changes: an old lease and a
|
|
55
|
+
* new computation will not compare equal, so a version skew re-asks (safe) rather
|
|
56
|
+
* than silently mismatching. Keep in lockstep with the Go/Java editions.
|
|
57
|
+
*/
|
|
58
|
+
export const APPROVAL_FINGERPRINT_VERSION = "v1";
|
|
59
|
+
|
|
60
|
+
/** The HMAC key. Raw bytes (Buffer) in production; a UTF-8 string in tests. */
|
|
61
|
+
export type FingerprintKey = BinaryLike;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Full-fidelity fingerprint for the in-process deep-agent gateway. Distinct
|
|
65
|
+
* actions (different tool, paths, command, or args) yield distinct fingerprints;
|
|
66
|
+
* the same action is byte-stable across re-invocations.
|
|
67
|
+
*/
|
|
68
|
+
export function computeApprovalFingerprint(key: FingerprintKey, input: ToolActionInput): string {
|
|
69
|
+
return tagged(hmacHex(key, canonicalToolActionJson(input)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The substrate-coarsened identity of a tool action — the only fidelity the
|
|
74
|
+
* Cursor deny-oracle hook can reproduce from its stdin payload.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors execute-cursor's `toolIdentity` so the two converge on one definition:
|
|
77
|
+
* - `tool` is the cross-taxonomy {@link toolApprovalCategory} (`write`/`delete`/
|
|
78
|
+
* `shell`) for gated built-ins, the tool name for MCP tools (whose name is
|
|
79
|
+
* stable across layers), or the trimmed tool name for non-gated built-ins
|
|
80
|
+
* (never lease-matched, so the fallback is harmless).
|
|
81
|
+
* - `salient` is the single resource the action targets (a normalized path or the
|
|
82
|
+
* shell command), reusing the full form's normalization so one rule governs
|
|
83
|
+
* both fidelities. Empty for MCP tools, which are matched by `tool` alone.
|
|
84
|
+
*/
|
|
85
|
+
export interface CoarseToolIdentity {
|
|
86
|
+
tool: string;
|
|
87
|
+
mcpServerSlug: string;
|
|
88
|
+
salient: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function coarseToolIdentity(input: ToolActionInput): CoarseToolIdentity {
|
|
92
|
+
const canonical = canonicalizeToolAction(input);
|
|
93
|
+
if (canonical.mcpServerSlug) {
|
|
94
|
+
return { tool: input.toolName.trim(), mcpServerSlug: canonical.mcpServerSlug, salient: "" };
|
|
95
|
+
}
|
|
96
|
+
const category = toolApprovalCategory(input.toolName);
|
|
97
|
+
return {
|
|
98
|
+
tool: category ?? input.toolName.trim(),
|
|
99
|
+
mcpServerSlug: "",
|
|
100
|
+
salient: canonical.paths[0] || canonical.shellCommand || "",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fingerprint an already-reduced {@link CoarseToolIdentity}. Split out from
|
|
106
|
+
* {@link computeCoarseApprovalFingerprint} so a substrate that has *already*
|
|
107
|
+
* reduced a tool call to its (tool, mcpServerSlug, salient) identity — the Cursor
|
|
108
|
+
* harness, whose hook and stream both key on a raw, un-normalized salient that
|
|
109
|
+
* the bash hook can reproduce without a workspace root — can fingerprint that
|
|
110
|
+
* exact identity through the one shared HMAC+canonical-JSON path, instead of
|
|
111
|
+
* re-deriving the salient via {@link canonicalizeToolAction} (which normalizes
|
|
112
|
+
* paths and would diverge from the hook's raw value).
|
|
113
|
+
*/
|
|
114
|
+
export function fingerprintCoarseIdentity(key: FingerprintKey, identity: CoarseToolIdentity): string {
|
|
115
|
+
return tagged(hmacHex(key, canonicalJson(identity)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Coarse fingerprint for the out-of-process Cursor hook. By construction, two
|
|
120
|
+
* actions that name the same operation in different taxonomies (`Write` vs
|
|
121
|
+
* `edit`) over the same resource collapse to one fingerprint — this is what makes
|
|
122
|
+
* the hook-side (deny/grant) and stream-side (reconciliation) values agree.
|
|
123
|
+
*/
|
|
124
|
+
export function computeCoarseApprovalFingerprint(key: FingerprintKey, input: ToolActionInput): string {
|
|
125
|
+
return fingerprintCoarseIdentity(key, coarseToolIdentity(input));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Derive the per-execution fingerprint key from a runner-held master secret.
|
|
130
|
+
*
|
|
131
|
+
* The key is scoped to one `execution_id`: stable across Temporal re-invocations
|
|
132
|
+
* of the same execution (the gateway approves on one invocation and enforces on
|
|
133
|
+
* the next), and isolated between executions so a fingerprint approved for one
|
|
134
|
+
* cannot be replayed against another. The master-secret source is wired when the
|
|
135
|
+
* gateway first consumes the fingerprint (Slices C/D); until then this is
|
|
136
|
+
* exercised only by tests against a fixed master secret.
|
|
137
|
+
*/
|
|
138
|
+
export function deriveExecutionFingerprintKey(masterSecret: FingerprintKey, executionId: string): Buffer {
|
|
139
|
+
return createHmac("sha256", masterSecret).update(executionId, "utf8").digest();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hmacHex(key: FingerprintKey, canonical: string): string {
|
|
143
|
+
return createHmac("sha256", key).update(canonical, "utf8").digest("hex");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function tagged(mac: string): string {
|
|
147
|
+
return `${APPROVAL_FINGERPRINT_VERSION}:${mac}`;
|
|
148
|
+
}
|