@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
- package/dist/activities/execute-deep-agent/hitl.js +11 -3
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +393 -33
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
- package/dist/activities/execute-deep-agent/status-builder.js +44 -3
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +23 -7
- package/src/activities/execute-deep-agent/index.ts +463 -40
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +63 -8
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
APPROVAL_FINGERPRINT_VERSION,
|
|
6
|
+
coarseToolIdentity,
|
|
7
|
+
computeApprovalFingerprint,
|
|
8
|
+
computeCoarseApprovalFingerprint,
|
|
9
|
+
deriveExecutionFingerprintKey,
|
|
10
|
+
} from "../approval-fingerprint.js";
|
|
11
|
+
import type { ToolActionInput } from "../approval-canonicalize.js";
|
|
12
|
+
|
|
13
|
+
interface FingerprintVector {
|
|
14
|
+
name: string;
|
|
15
|
+
input: ToolActionInput;
|
|
16
|
+
expected: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Single source of truth, shared byte-for-byte with the future Go/Java editions.
|
|
20
|
+
const vectorsPath = fileURLToPath(
|
|
21
|
+
new URL("../../../../../../apis/testdata/hitl/fingerprint/vectors.json", import.meta.url),
|
|
22
|
+
);
|
|
23
|
+
const corpus = JSON.parse(readFileSync(vectorsPath, "utf-8")) as {
|
|
24
|
+
key: string;
|
|
25
|
+
full: FingerprintVector[];
|
|
26
|
+
coarse: FingerprintVector[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("approval fingerprint vector corpus", () => {
|
|
30
|
+
it("loads a non-trivial corpus", () => {
|
|
31
|
+
expect(corpus.full.length).toBeGreaterThanOrEqual(4);
|
|
32
|
+
expect(corpus.coarse.length).toBeGreaterThanOrEqual(4);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for (const v of corpus.full) {
|
|
36
|
+
it(`full vector: ${v.name}`, () => {
|
|
37
|
+
expect(computeApprovalFingerprint(corpus.key, v.input)).toBe(v.expected);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const v of corpus.coarse) {
|
|
42
|
+
it(`coarse vector: ${v.name}`, () => {
|
|
43
|
+
expect(computeCoarseApprovalFingerprint(corpus.key, v.input)).toBe(v.expected);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("fingerprint invariants", () => {
|
|
49
|
+
const key = "test-key";
|
|
50
|
+
|
|
51
|
+
it("tags every fingerprint with the version", () => {
|
|
52
|
+
const fp = computeApprovalFingerprint(key, { toolName: "read" });
|
|
53
|
+
expect(fp.startsWith(`${APPROVAL_FINGERPRINT_VERSION}:`)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("is stable for the same action (an approval re-driven is the same lease)", () => {
|
|
57
|
+
const input: ToolActionInput = { toolName: "Write", paths: ["src/a.ts"], args: { contents: "x" } };
|
|
58
|
+
expect(computeApprovalFingerprint(key, input)).toBe(computeApprovalFingerprint(key, input));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("changes when the action changes (a drifted action is re-asked, never reused)", () => {
|
|
62
|
+
const a = computeApprovalFingerprint(key, { toolName: "Write", paths: ["src/a.ts"] });
|
|
63
|
+
const b = computeApprovalFingerprint(key, { toolName: "Write", paths: ["src/b.ts"] });
|
|
64
|
+
expect(a).not.toBe(b);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("is key-bound (a fingerprint from one key cannot satisfy another)", () => {
|
|
68
|
+
const input: ToolActionInput = { toolName: "Shell", shellCommand: "rm -rf /" };
|
|
69
|
+
expect(computeApprovalFingerprint("key-a", input)).not.toBe(computeApprovalFingerprint("key-b", input));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("coarse projection (Cursor substrate)", () => {
|
|
74
|
+
it("collapses the write/edit taxonomies onto one identity", () => {
|
|
75
|
+
// Same logical edit named in the hook taxonomy (Write) and the stream
|
|
76
|
+
// taxonomy (edit) over the same resource -> one fingerprint. This is the
|
|
77
|
+
// March-2026 failure class, prevented by construction.
|
|
78
|
+
const hookSide: ToolActionInput = { toolName: "Write", paths: ["a.txt"] };
|
|
79
|
+
const streamSide: ToolActionInput = { toolName: "edit", paths: ["a.txt"] };
|
|
80
|
+
expect(coarseToolIdentity(hookSide).tool).toBe("write");
|
|
81
|
+
expect(coarseToolIdentity(streamSide).tool).toBe("write");
|
|
82
|
+
expect(computeCoarseApprovalFingerprint("k", hookSide)).toBe(
|
|
83
|
+
computeCoarseApprovalFingerprint("k", streamSide),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("isolates categories (approving a write never satisfies a shell)", () => {
|
|
88
|
+
const write: ToolActionInput = { toolName: "Write", paths: ["a.txt"] };
|
|
89
|
+
const shell: ToolActionInput = { toolName: "Shell", shellCommand: "a.txt" };
|
|
90
|
+
expect(computeCoarseApprovalFingerprint("k", write)).not.toBe(
|
|
91
|
+
computeCoarseApprovalFingerprint("k", shell),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keys MCP tools on tool name with an empty salient", () => {
|
|
96
|
+
const id = coarseToolIdentity({ toolName: "create_issue", mcpServerSlug: "GitHub" });
|
|
97
|
+
expect(id).toEqual({ tool: "create_issue", mcpServerSlug: "github", salient: "" });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("per-execution key derivation", () => {
|
|
102
|
+
it("is stable for the same execution (gateway approves and enforces across re-invocations)", () => {
|
|
103
|
+
const master = "master-secret";
|
|
104
|
+
expect(deriveExecutionFingerprintKey(master, "exec-1")).toEqual(
|
|
105
|
+
deriveExecutionFingerprintKey(master, "exec-1"),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("isolates executions (a key from one execution cannot serve another)", () => {
|
|
110
|
+
const master = "master-secret";
|
|
111
|
+
expect(deriveExecutionFingerprintKey(master, "exec-1")).not.toEqual(
|
|
112
|
+
deriveExecutionFingerprintKey(master, "exec-2"),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -11,38 +11,69 @@ import {
|
|
|
11
11
|
mergeApprovalPolicies,
|
|
12
12
|
lookupMcpToolPolicy,
|
|
13
13
|
resolveApprovalMessage,
|
|
14
|
-
|
|
14
|
+
deriveActiveLeases,
|
|
15
|
+
resolveApprovalProvenance,
|
|
16
|
+
toProtoPolicySource,
|
|
17
|
+
type ActiveLeases,
|
|
18
|
+
type MergedToolPolicy,
|
|
15
19
|
} from "../approval-policy.js";
|
|
20
|
+
import type { ToolApprovalCategory } from "../tool-kind.js";
|
|
16
21
|
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
17
22
|
import type { ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
18
23
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
19
|
-
import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
24
|
+
import { ApprovalAction, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
25
|
+
|
|
26
|
+
/** No active leases — the default gate-everything state used by most merge tests. */
|
|
27
|
+
const NO_LEASES: ActiveLeases = { global: false, categories: new Set(), servers: new Set() };
|
|
28
|
+
|
|
29
|
+
/** ActiveLeases with the given scopes; `global` defaults to false. */
|
|
30
|
+
function leases(opts: {
|
|
31
|
+
global?: boolean;
|
|
32
|
+
categories?: Array<"write" | "delete" | "shell">;
|
|
33
|
+
servers?: string[];
|
|
34
|
+
}): ActiveLeases {
|
|
35
|
+
return {
|
|
36
|
+
global: opts.global ?? false,
|
|
37
|
+
categories: new Set(opts.categories ?? []),
|
|
38
|
+
servers: new Set(opts.servers ?? []),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A single tool call shaped just enough for deriveActiveLeases. */
|
|
43
|
+
interface TestToolCall {
|
|
44
|
+
approvalAction: ApprovalAction;
|
|
45
|
+
name?: string;
|
|
46
|
+
mcpServerSlug?: string;
|
|
47
|
+
}
|
|
20
48
|
|
|
21
49
|
/**
|
|
22
|
-
* Builds a minimal AgentExecution shaped just enough for
|
|
23
|
-
*
|
|
24
|
-
* root and sub-agent messages.
|
|
50
|
+
* Builds a minimal AgentExecution shaped just enough for deriveActiveLeases,
|
|
51
|
+
* which reads each tool call's approval action plus its name / mcp_server_slug
|
|
52
|
+
* (the scope inputs) on root and sub-agent messages, and spec.auto_approve_all.
|
|
25
53
|
*/
|
|
26
54
|
function makeExecution(opts: {
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
rootCalls?: TestToolCall[];
|
|
56
|
+
subAgentCalls?: TestToolCall[];
|
|
57
|
+
autoApproveAll?: boolean;
|
|
29
58
|
hasStatus?: boolean;
|
|
30
59
|
}): AgentExecution {
|
|
31
60
|
const hasStatus = opts.hasStatus ?? true;
|
|
61
|
+
const spec = { autoApproveAll: opts.autoApproveAll ?? false };
|
|
32
62
|
if (!hasStatus) {
|
|
33
|
-
return { status: undefined } as unknown as AgentExecution;
|
|
63
|
+
return { spec, status: undefined } as unknown as AgentExecution;
|
|
34
64
|
}
|
|
65
|
+
const toCalls = (calls: TestToolCall[] = []) =>
|
|
66
|
+
calls.map(c => ({
|
|
67
|
+
approvalAction: c.approvalAction,
|
|
68
|
+
name: c.name ?? "",
|
|
69
|
+
mcpServerSlug: c.mcpServerSlug ?? "",
|
|
70
|
+
}));
|
|
35
71
|
return {
|
|
72
|
+
spec,
|
|
36
73
|
status: {
|
|
37
|
-
messages: [
|
|
38
|
-
{ toolCalls: (opts.rootActions ?? []).map(a => ({ approvalAction: a })) },
|
|
39
|
-
],
|
|
74
|
+
messages: [{ toolCalls: toCalls(opts.rootCalls) }],
|
|
40
75
|
subAgentExecutions: [
|
|
41
|
-
{
|
|
42
|
-
messages: [
|
|
43
|
-
{ toolCalls: (opts.subAgentActions ?? []).map(a => ({ approvalAction: a })) },
|
|
44
|
-
],
|
|
45
|
-
},
|
|
76
|
+
{ messages: [{ toolCalls: toCalls(opts.subAgentCalls) }] },
|
|
46
77
|
],
|
|
47
78
|
},
|
|
48
79
|
} as unknown as AgentExecution;
|
|
@@ -69,19 +100,29 @@ function makeServer(
|
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
describe("mergeApprovalPolicies", () => {
|
|
72
|
-
it("returns empty map
|
|
103
|
+
it("returns empty map under a global lease (spec.auto_approve_all)", () => {
|
|
73
104
|
const servers = [makeServer("github", [{ toolName: "push" }])];
|
|
74
|
-
const result = mergeApprovalPolicies(servers, [], true);
|
|
105
|
+
const result = mergeApprovalPolicies(servers, [], leases({ global: true }));
|
|
75
106
|
expect(result.size).toBe(0);
|
|
76
107
|
});
|
|
77
108
|
|
|
109
|
+
it("drops a leased server's tools while keeping other servers gated", () => {
|
|
110
|
+
const servers = [
|
|
111
|
+
makeServer("github", [{ toolName: "push" }]),
|
|
112
|
+
makeServer("database", [{ toolName: "drop_table" }]),
|
|
113
|
+
];
|
|
114
|
+
const result = mergeApprovalPolicies(servers, [], leases({ servers: ["github"] }));
|
|
115
|
+
expect(result.has("github/push")).toBe(false);
|
|
116
|
+
expect(result.has("database/drop_table")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
78
119
|
it("creates policies from toolApprovals", () => {
|
|
79
120
|
const servers = [
|
|
80
121
|
makeServer("github", [
|
|
81
122
|
{ toolName: "create_issue", message: "Create issue?" },
|
|
82
123
|
]),
|
|
83
124
|
];
|
|
84
|
-
const result = mergeApprovalPolicies(servers, [],
|
|
125
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
85
126
|
|
|
86
127
|
expect(result.size).toBe(1);
|
|
87
128
|
const policy = result.get("github/create_issue")!;
|
|
@@ -98,7 +139,7 @@ describe("mergeApprovalPolicies", () => {
|
|
|
98
139
|
[{ toolName: "push", message: "pinned message" }],
|
|
99
140
|
),
|
|
100
141
|
];
|
|
101
|
-
const result = mergeApprovalPolicies(servers, [],
|
|
142
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
102
143
|
|
|
103
144
|
const policy = result.get("github/push")!;
|
|
104
145
|
expect(policy.approvalMessage).toBe("pinned message");
|
|
@@ -114,7 +155,7 @@ describe("mergeApprovalPolicies", () => {
|
|
|
114
155
|
message: "",
|
|
115
156
|
}] as any[];
|
|
116
157
|
|
|
117
|
-
const result = mergeApprovalPolicies(servers, overrides,
|
|
158
|
+
const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
|
|
118
159
|
expect(result.size).toBe(0);
|
|
119
160
|
});
|
|
120
161
|
|
|
@@ -126,15 +167,57 @@ describe("mergeApprovalPolicies", () => {
|
|
|
126
167
|
message: "Really delete?",
|
|
127
168
|
}] as any[];
|
|
128
169
|
|
|
129
|
-
const result = mergeApprovalPolicies(servers, overrides,
|
|
170
|
+
const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
|
|
130
171
|
expect(result.has("github/delete_repo")).toBe(true);
|
|
131
172
|
});
|
|
132
173
|
|
|
174
|
+
describe("provenance (source stamping)", () => {
|
|
175
|
+
it("stamps classifier_default for a layer-1 tool approval", () => {
|
|
176
|
+
const servers = [makeServer("github", [{ toolName: "create_issue" }])];
|
|
177
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
178
|
+
expect(result.get("github/create_issue")!.source).toBe("classifier_default");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("stamps pinned_override when a pinned approval wins", () => {
|
|
182
|
+
const servers = [
|
|
183
|
+
makeServer("github", [{ toolName: "push" }], [{ toolName: "push" }]),
|
|
184
|
+
];
|
|
185
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
186
|
+
expect(result.get("github/push")!.source).toBe("pinned_override");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("stamps agent_override when a per-agent override adds a gate", () => {
|
|
190
|
+
const servers = [makeServer("github", [])];
|
|
191
|
+
const overrides: ToolApprovalOverride[] = [{
|
|
192
|
+
toolName: "delete_repo",
|
|
193
|
+
requiresApproval: true,
|
|
194
|
+
message: "Really delete?",
|
|
195
|
+
}] as any[];
|
|
196
|
+
const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
|
|
197
|
+
expect(result.get("github/delete_repo")!.source).toBe("agent_override");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("stamps agent_override when it overrides a classifier/pinned entry", () => {
|
|
201
|
+
const servers = [
|
|
202
|
+
makeServer("github", [{ toolName: "push", message: "classifier" }]),
|
|
203
|
+
];
|
|
204
|
+
const overrides: ToolApprovalOverride[] = [{
|
|
205
|
+
toolName: "push",
|
|
206
|
+
requiresApproval: true,
|
|
207
|
+
message: "agent says gate",
|
|
208
|
+
}] as any[];
|
|
209
|
+
const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
|
|
210
|
+
const policy = result.get("github/push")!;
|
|
211
|
+
expect(policy.source).toBe("agent_override");
|
|
212
|
+
expect(policy.approvalMessage).toBe("agent says gate");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
133
216
|
it("skips tools without names", () => {
|
|
134
217
|
const servers = [
|
|
135
218
|
makeServer("github", [{ toolName: "" }]),
|
|
136
219
|
];
|
|
137
|
-
const result = mergeApprovalPolicies(servers, [],
|
|
220
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
138
221
|
expect(result.size).toBe(0);
|
|
139
222
|
});
|
|
140
223
|
|
|
@@ -142,7 +225,7 @@ describe("mergeApprovalPolicies", () => {
|
|
|
142
225
|
const servers = [
|
|
143
226
|
makeServer("github", [{ toolName: "push" }]),
|
|
144
227
|
];
|
|
145
|
-
const result = mergeApprovalPolicies(servers, [],
|
|
228
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
146
229
|
expect(result.get("github/push")!.approvalMessage).toContain("Execute tool: push");
|
|
147
230
|
});
|
|
148
231
|
|
|
@@ -151,50 +234,100 @@ describe("mergeApprovalPolicies", () => {
|
|
|
151
234
|
makeServer("github", [{ toolName: "push" }]),
|
|
152
235
|
makeServer("database", [{ toolName: "drop_table" }]),
|
|
153
236
|
];
|
|
154
|
-
const result = mergeApprovalPolicies(servers, [],
|
|
237
|
+
const result = mergeApprovalPolicies(servers, [], NO_LEASES);
|
|
155
238
|
expect(result.size).toBe(2);
|
|
156
239
|
expect(result.has("github/push")).toBe(true);
|
|
157
240
|
expect(result.has("database/drop_table")).toBe(true);
|
|
158
241
|
});
|
|
159
242
|
});
|
|
160
243
|
|
|
161
|
-
describe("
|
|
162
|
-
it("returns
|
|
163
|
-
|
|
244
|
+
describe("deriveActiveLeases", () => {
|
|
245
|
+
it("returns no leases when there is no status", () => {
|
|
246
|
+
const result = deriveActiveLeases(makeExecution({ hasStatus: false }));
|
|
247
|
+
expect(result.global).toBe(false);
|
|
248
|
+
expect(result.categories.size).toBe(0);
|
|
249
|
+
expect(result.servers.size).toBe(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("returns no scoped leases when no tool call carries APPROVE_ALL", () => {
|
|
253
|
+
const exec = makeExecution({
|
|
254
|
+
rootCalls: [
|
|
255
|
+
{ approvalAction: ApprovalAction.APPROVE, name: "shell" },
|
|
256
|
+
{ approvalAction: ApprovalAction.SKIP, name: "write" },
|
|
257
|
+
],
|
|
258
|
+
subAgentCalls: [{ approvalAction: ApprovalAction.REJECT, name: "delete" }],
|
|
259
|
+
});
|
|
260
|
+
const result = deriveActiveLeases(exec);
|
|
261
|
+
expect(result.categories.size).toBe(0);
|
|
262
|
+
expect(result.servers.size).toBe(0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("reflects the global pre-arm from spec.auto_approve_all", () => {
|
|
266
|
+
const result = deriveActiveLeases(makeExecution({ autoApproveAll: true }));
|
|
267
|
+
expect(result.global).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("derives a built-in CATEGORY lease from an APPROVE_ALL on a built-in", () => {
|
|
271
|
+
const exec = makeExecution({
|
|
272
|
+
rootCalls: [{ approvalAction: ApprovalAction.APPROVE_ALL, name: "shell" }],
|
|
273
|
+
});
|
|
274
|
+
const result = deriveActiveLeases(exec);
|
|
275
|
+
expect([...result.categories]).toEqual(["shell"]);
|
|
276
|
+
expect(result.servers.size).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("collapses FILE_WRITE and FILE_EDIT to a single 'write' category lease", () => {
|
|
280
|
+
const exec = makeExecution({
|
|
281
|
+
rootCalls: [
|
|
282
|
+
{ approvalAction: ApprovalAction.APPROVE_ALL, name: "write_file" },
|
|
283
|
+
{ approvalAction: ApprovalAction.APPROVE_ALL, name: "edit_file" },
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
expect([...deriveActiveLeases(exec).categories]).toEqual(["write"]);
|
|
164
287
|
});
|
|
165
288
|
|
|
166
|
-
it("
|
|
289
|
+
it("derives a SERVER lease from an APPROVE_ALL on an MCP tool", () => {
|
|
167
290
|
const exec = makeExecution({
|
|
168
|
-
|
|
169
|
-
|
|
291
|
+
rootCalls: [
|
|
292
|
+
{ approvalAction: ApprovalAction.APPROVE_ALL, name: "create_issue", mcpServerSlug: "github" },
|
|
293
|
+
],
|
|
170
294
|
});
|
|
171
|
-
|
|
295
|
+
const result = deriveActiveLeases(exec);
|
|
296
|
+
expect([...result.servers]).toEqual(["github"]);
|
|
297
|
+
expect(result.categories.size).toBe(0);
|
|
172
298
|
});
|
|
173
299
|
|
|
174
|
-
it("
|
|
300
|
+
it("derives leases from sub-agent tool calls too", () => {
|
|
175
301
|
const exec = makeExecution({
|
|
176
|
-
|
|
302
|
+
subAgentCalls: [
|
|
303
|
+
{ approvalAction: ApprovalAction.APPROVE_ALL, name: "delete" },
|
|
304
|
+
{ approvalAction: ApprovalAction.APPROVE_ALL, name: "drop", mcpServerSlug: "database" },
|
|
305
|
+
],
|
|
177
306
|
});
|
|
178
|
-
|
|
307
|
+
const result = deriveActiveLeases(exec);
|
|
308
|
+
expect([...result.categories]).toEqual(["delete"]);
|
|
309
|
+
expect([...result.servers]).toEqual(["database"]);
|
|
179
310
|
});
|
|
180
311
|
|
|
181
|
-
it("
|
|
312
|
+
it("does not lease a read-only built-in (no category) on APPROVE_ALL", () => {
|
|
182
313
|
const exec = makeExecution({
|
|
183
|
-
|
|
184
|
-
subAgentActions: [ApprovalAction.APPROVE_ALL],
|
|
314
|
+
rootCalls: [{ approvalAction: ApprovalAction.APPROVE_ALL, name: "read" }],
|
|
185
315
|
});
|
|
186
|
-
|
|
316
|
+
const result = deriveActiveLeases(exec);
|
|
317
|
+
expect(result.categories.size).toBe(0);
|
|
318
|
+
expect(result.servers.size).toBe(0);
|
|
187
319
|
});
|
|
188
320
|
});
|
|
189
321
|
|
|
190
322
|
describe("lookupMcpToolPolicy", () => {
|
|
191
323
|
it("finds policy by server/tool key", () => {
|
|
192
|
-
const policies = new Map([
|
|
324
|
+
const policies = new Map<string, MergedToolPolicy>([
|
|
193
325
|
["github/push", {
|
|
194
326
|
toolName: "push",
|
|
195
327
|
mcpServerSlug: "github",
|
|
196
328
|
requiresApproval: true,
|
|
197
329
|
approvalMessage: "Push?",
|
|
330
|
+
source: "classifier_default",
|
|
198
331
|
}],
|
|
199
332
|
]);
|
|
200
333
|
|
|
@@ -254,3 +387,104 @@ describe("resolveApprovalMessage", () => {
|
|
|
254
387
|
.toBe("Simple message");
|
|
255
388
|
});
|
|
256
389
|
});
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// resolveApprovalProvenance — the read-side seam that stamps
|
|
393
|
+
// ToolCall.approval_policy_source. Each branch must map to the right
|
|
394
|
+
// PolicySource so the persisted provenance is correct per layer.
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
describe("resolveApprovalProvenance", () => {
|
|
398
|
+
const NO_CATEGORIES: ReadonlySet<ToolApprovalCategory> = new Set();
|
|
399
|
+
|
|
400
|
+
function mcpPolicy(source: MergedToolPolicy["source"]): ReadonlyMap<string, MergedToolPolicy> {
|
|
401
|
+
return new Map([
|
|
402
|
+
[
|
|
403
|
+
"github/create_issue",
|
|
404
|
+
{
|
|
405
|
+
toolName: "create_issue",
|
|
406
|
+
mcpServerSlug: "github",
|
|
407
|
+
requiresApproval: true,
|
|
408
|
+
approvalMessage: "Create issue?",
|
|
409
|
+
source,
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
it("returns auto_approve_all when the whole-run global bypass is armed", () => {
|
|
416
|
+
expect(
|
|
417
|
+
resolveApprovalProvenance("delete", "", new Map(), NO_CATEGORIES, true),
|
|
418
|
+
).toBe("auto_approve_all");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("surfaces the merged MCP policy's layer for a gated MCP tool", () => {
|
|
422
|
+
expect(
|
|
423
|
+
resolveApprovalProvenance("create_issue", "github", mcpPolicy("agent_override"), NO_CATEGORIES, false),
|
|
424
|
+
).toBe("agent_override");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("surfaces annotation_destructive_tighten for a destructive-hint-gated MCP tool", () => {
|
|
428
|
+
expect(
|
|
429
|
+
resolveApprovalProvenance(
|
|
430
|
+
"create_issue",
|
|
431
|
+
"github",
|
|
432
|
+
mcpPolicy("annotation_destructive_tighten"),
|
|
433
|
+
NO_CATEGORIES,
|
|
434
|
+
false,
|
|
435
|
+
),
|
|
436
|
+
).toBe("annotation_destructive_tighten");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("reads classifier_default for an MCP tool the chain cleared (no entry)", () => {
|
|
440
|
+
expect(
|
|
441
|
+
resolveApprovalProvenance("list_issues", "github", new Map(), NO_CATEGORIES, false),
|
|
442
|
+
).toBe("classifier_default");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("returns builtin_category for a mutating built-in with no lease", () => {
|
|
446
|
+
expect(
|
|
447
|
+
resolveApprovalProvenance("write", "", new Map(), NO_CATEGORIES, false),
|
|
448
|
+
).toBe("builtin_category");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("returns approval_lease for a mutating built-in whose category is leased", () => {
|
|
452
|
+
expect(
|
|
453
|
+
resolveApprovalProvenance("write", "", new Map(), new Set<ToolApprovalCategory>(["write"]), false),
|
|
454
|
+
).toBe("approval_lease");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("returns undefined for a read-only built-in no layer governs", () => {
|
|
458
|
+
expect(
|
|
459
|
+
resolveApprovalProvenance("read", "", new Map(), NO_CATEGORIES, false),
|
|
460
|
+
).toBeUndefined();
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// toProtoPolicySource — the runner-union -> proto-enum mapping. The
|
|
466
|
+
// cross-edition corpus (policy-source-corpus.test.ts) pins the numbers; these
|
|
467
|
+
// cases assert the mapping is total and that undefined collapses to UNSPECIFIED.
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
describe("toProtoPolicySource", () => {
|
|
471
|
+
it("maps undefined (no governing layer) to UNSPECIFIED", () => {
|
|
472
|
+
expect(toProtoPolicySource(undefined)).toBe(ApprovalPolicySource.UNSPECIFIED);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("maps every union member to a distinct non-UNSPECIFIED enum value", () => {
|
|
476
|
+
const sources = [
|
|
477
|
+
"classifier_default",
|
|
478
|
+
"pinned_override",
|
|
479
|
+
"agent_override",
|
|
480
|
+
"auto_approve_all",
|
|
481
|
+
"approval_lease",
|
|
482
|
+
"builtin_category",
|
|
483
|
+
"annotation_destructive_tighten",
|
|
484
|
+
] as const;
|
|
485
|
+
const mapped = sources.map((s) => toProtoPolicySource(s));
|
|
486
|
+
// All non-UNSPECIFIED and all distinct.
|
|
487
|
+
expect(mapped.every((v) => v !== ApprovalPolicySource.UNSPECIFIED)).toBe(true);
|
|
488
|
+
expect(new Set(mapped).size).toBe(sources.length);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the shared args-preview sanitizers.
|
|
3
|
+
*
|
|
4
|
+
* The Cursor gate path depends on {@link buildElidedArgsPreview} producing a
|
|
5
|
+
* SMALL, ALWAYS-VALID, salient-preserving preview: the resumed turn re-parses it
|
|
6
|
+
* to rebuild an approval grant's identity, and the persist-time size backstop
|
|
7
|
+
* would replace an oversized preview with an unparseable marker. These pin those
|
|
8
|
+
* invariants so a large write can never silently break the resume round-trip.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
buildElidedArgsPreview,
|
|
14
|
+
sanitizeArgsPreview,
|
|
15
|
+
MAX_ARGS_PREVIEW_LENGTH,
|
|
16
|
+
} from "../args-preview.js";
|
|
17
|
+
|
|
18
|
+
describe("sanitizeArgsPreview (native)", () => {
|
|
19
|
+
it("redacts sensitive keys", () => {
|
|
20
|
+
const out = JSON.parse(sanitizeArgsPreview({ api_key: "sk-123", path: "a.txt" }));
|
|
21
|
+
expect(out.api_key).toBe("[REDACTED]");
|
|
22
|
+
expect(out.path).toBe("a.txt");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("truncates the whole string past the cap", () => {
|
|
26
|
+
const preview = sanitizeArgsPreview({ contents: "x".repeat(MAX_ARGS_PREVIEW_LENGTH * 2) });
|
|
27
|
+
expect(preview.length).toBeLessThanOrEqual(MAX_ARGS_PREVIEW_LENGTH + 1);
|
|
28
|
+
expect(preview.endsWith("…")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("buildElidedArgsPreview (cursor gate)", () => {
|
|
33
|
+
const SALIENT = ["file_path", "path", "target_notebook", "command"] as const;
|
|
34
|
+
|
|
35
|
+
it("stays valid JSON and small for a large write, eliding the content value", () => {
|
|
36
|
+
const content = "line\n".repeat(100_000); // ~500 KB
|
|
37
|
+
const preview = buildElidedArgsPreview(
|
|
38
|
+
{ path: "src/big.ts", contents: content },
|
|
39
|
+
SALIENT,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Parseable — the resume round-trip depends on this.
|
|
43
|
+
const parsed = JSON.parse(preview) as Record<string, unknown>;
|
|
44
|
+
// Bounded — nowhere near the raw content size.
|
|
45
|
+
expect(preview.length).toBeLessThan(1_000);
|
|
46
|
+
// The salient path survives verbatim (grant identity), content is elided.
|
|
47
|
+
expect(parsed.path).toBe("src/big.ts");
|
|
48
|
+
expect(parsed.contents).toBe(`[${content.length} chars]`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("never elides a salient field, even a very long shell command", () => {
|
|
52
|
+
const command = "echo " + "a".repeat(5_000);
|
|
53
|
+
const preview = buildElidedArgsPreview({ command, cwd: "/x" }, SALIENT);
|
|
54
|
+
|
|
55
|
+
const parsed = JSON.parse(preview) as Record<string, unknown>;
|
|
56
|
+
// The grant token is base64(category\ncommand) — the command MUST be whole.
|
|
57
|
+
expect(parsed.command).toBe(command);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("redacts secret keys", () => {
|
|
61
|
+
const preview = buildElidedArgsPreview(
|
|
62
|
+
{ path: "a.txt", authorization: "Bearer abc" },
|
|
63
|
+
SALIENT,
|
|
64
|
+
);
|
|
65
|
+
const parsed = JSON.parse(preview) as Record<string, unknown>;
|
|
66
|
+
expect(parsed.authorization).toBe("[REDACTED]");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("preserves small non-salient values unchanged", () => {
|
|
70
|
+
const preview = buildElidedArgsPreview(
|
|
71
|
+
{ path: "a.txt", old_string: "alpha", new_string: "beta" },
|
|
72
|
+
SALIENT,
|
|
73
|
+
);
|
|
74
|
+
const parsed = JSON.parse(preview) as Record<string, unknown>;
|
|
75
|
+
expect(parsed.old_string).toBe("alpha");
|
|
76
|
+
expect(parsed.new_string).toBe("beta");
|
|
77
|
+
});
|
|
78
|
+
});
|