@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
- package/dist/activities/execute-deep-agent/hitl.js +5 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +370 -56
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
- package/dist/activities/execute-deep-agent/status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +15 -1
- package/src/activities/execute-deep-agent/index.ts +434 -64
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +19 -7
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
6
|
+
import { CasCaptureFilesystemBackend } from "../cas-capture-backend.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The observer is the single, shared owner of one turn's CAS-capture state. It
|
|
10
|
+
* records the PRE-TURN bytes of first-touched gitignored paths (gate-independently,
|
|
11
|
+
* at the mutation point) and the gitignored paths the gate hard-blocked as
|
|
12
|
+
* secret-like. These tests assert the recording contract — only gitignored paths,
|
|
13
|
+
* only the first touch, never a post-write baseline — AND the cross-instance
|
|
14
|
+
* guarantee that makes sub-agent capture safe: the parent and every sub-agent
|
|
15
|
+
* backend share ONE observer, so first-touch-wins holds even when concurrent
|
|
16
|
+
* graphs touch the same path (Session 26, DD-19).
|
|
17
|
+
*/
|
|
18
|
+
describe("CasCaptureObserver", () => {
|
|
19
|
+
let root: string;
|
|
20
|
+
|
|
21
|
+
/** Paths under `ignored/` are gitignored; everything else is git-tracked. */
|
|
22
|
+
const ignoredPredicate = (relPath: string): boolean => relPath.startsWith("ignored/");
|
|
23
|
+
|
|
24
|
+
function makeObserver(isIgnored = async (p: string) => ignoredPredicate(p)): CasCaptureObserver {
|
|
25
|
+
return new CasCaptureObserver({ rootDir: root, isIgnored });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
root = await mkdtemp(join(tmpdir(), "cas-observer-"));
|
|
30
|
+
await mkdir(join(root, "ignored"), { recursive: true });
|
|
31
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await rm(root, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("recordBefore", () => {
|
|
39
|
+
it("records before=null for a not-yet-existing gitignored path (an ADD)", async () => {
|
|
40
|
+
const observer = makeObserver();
|
|
41
|
+
|
|
42
|
+
await observer.recordBefore("ignored/new.txt");
|
|
43
|
+
|
|
44
|
+
expect(observer.before.has("ignored/new.txt")).toBe(true);
|
|
45
|
+
expect(observer.before.get("ignored/new.txt")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("records the pre-turn bytes for an existing gitignored file (a MODIFY)", async () => {
|
|
49
|
+
await writeFile(join(root, "ignored/cfg.txt"), "v0");
|
|
50
|
+
const observer = makeObserver();
|
|
51
|
+
|
|
52
|
+
await observer.recordBefore("ignored/cfg.txt");
|
|
53
|
+
|
|
54
|
+
const before = observer.before.get("ignored/cfg.txt");
|
|
55
|
+
expect(before).not.toBeNull();
|
|
56
|
+
expect(Buffer.from(before!).toString("utf8")).toBe("v0");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("first-touch-wins: a later touch does not overwrite the recorded baseline", async () => {
|
|
60
|
+
await writeFile(join(root, "ignored/log.txt"), "a");
|
|
61
|
+
const observer = makeObserver();
|
|
62
|
+
|
|
63
|
+
await observer.recordBefore("ignored/log.txt");
|
|
64
|
+
// Simulate the file changing mid-turn, then a second mutation touches it.
|
|
65
|
+
await writeFile(join(root, "ignored/log.txt"), "b");
|
|
66
|
+
await observer.recordBefore("ignored/log.txt");
|
|
67
|
+
|
|
68
|
+
expect(Buffer.from(observer.before.get("ignored/log.txt")!).toString("utf8")).toBe("a");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does NOT record git-tracked paths (the boundary git diff owns those)", async () => {
|
|
72
|
+
const observer = makeObserver();
|
|
73
|
+
|
|
74
|
+
await observer.recordBefore("src/app.ts");
|
|
75
|
+
|
|
76
|
+
expect(observer.before.has("src/app.ts")).toBe(false);
|
|
77
|
+
expect(observer.before.size).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("normalizes virtual-root ('/'-prefixed) paths to workspace-relative keys", async () => {
|
|
81
|
+
const observer = makeObserver();
|
|
82
|
+
|
|
83
|
+
await observer.recordBefore("/ignored/rooted.txt");
|
|
84
|
+
|
|
85
|
+
expect(observer.before.has("ignored/rooted.txt")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("memoizes the gitignore classification (one check per distinct path)", async () => {
|
|
89
|
+
const isIgnored = vi.fn(async (p: string) => ignoredPredicate(p));
|
|
90
|
+
const observer = makeObserver(isIgnored);
|
|
91
|
+
|
|
92
|
+
await observer.recordBefore("ignored/a.txt");
|
|
93
|
+
await observer.recordBefore("ignored/a.txt");
|
|
94
|
+
await observer.recordBefore("ignored/b.txt");
|
|
95
|
+
|
|
96
|
+
expect(isIgnored).toHaveBeenCalledTimes(2); // a.txt once, b.txt once
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("recordBlockedSecret", () => {
|
|
101
|
+
it("resolves the raw path to a workspace-relative key and dedupes", () => {
|
|
102
|
+
const observer = makeObserver();
|
|
103
|
+
|
|
104
|
+
observer.recordBlockedSecret("/ignored/.env");
|
|
105
|
+
observer.recordBlockedSecret("ignored/.env");
|
|
106
|
+
|
|
107
|
+
expect([...observer.blockedSecretPaths]).toEqual(["ignored/.env"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("blockedSecretPaths is empty until something is recorded", () => {
|
|
111
|
+
expect([...makeObserver().blockedSecretPaths]).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("shared across backends (cross-instance first-touch)", () => {
|
|
116
|
+
/** Two backends on the SAME observer — the parent + sub-agent shape. */
|
|
117
|
+
function twoBackends(observer: CasCaptureObserver): [CasCaptureFilesystemBackend, CasCaptureFilesystemBackend] {
|
|
118
|
+
return [
|
|
119
|
+
new CasCaptureFilesystemBackend({ rootDir: root }, { observer }),
|
|
120
|
+
new CasCaptureFilesystemBackend({ rootDir: root }, { observer }),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
it("a write through either backend feeds the one shared before-map", async () => {
|
|
125
|
+
const observer = makeObserver();
|
|
126
|
+
const [parent, sub] = twoBackends(observer);
|
|
127
|
+
|
|
128
|
+
await parent.write("ignored/from-parent.txt", "p");
|
|
129
|
+
await sub.write("ignored/from-sub.txt", "s");
|
|
130
|
+
|
|
131
|
+
expect(observer.before.get("ignored/from-parent.txt")).toBeNull();
|
|
132
|
+
expect(observer.before.get("ignored/from-sub.txt")).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("keeps the first backend's pre-turn baseline when a second backend touches the same path", async () => {
|
|
136
|
+
await writeFile(join(root, "ignored/shared.txt"), "orig");
|
|
137
|
+
const observer = makeObserver();
|
|
138
|
+
const [parent, sub] = twoBackends(observer);
|
|
139
|
+
|
|
140
|
+
await parent.edit("ignored/shared.txt", "orig", "parent");
|
|
141
|
+
await sub.edit("ignored/shared.txt", "parent", "sub");
|
|
142
|
+
|
|
143
|
+
expect(Buffer.from(observer.before.get("ignored/shared.txt")!).toString("utf8")).toBe("orig");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("under CONCURRENT writes from two backends to one path, records the pre-turn baseline exactly once", async () => {
|
|
147
|
+
await writeFile(join(root, "ignored/race.txt"), "orig");
|
|
148
|
+
const observer = makeObserver();
|
|
149
|
+
const [a, b] = twoBackends(observer);
|
|
150
|
+
|
|
151
|
+
// The synchronous shared reservation must ensure only the pre-turn bytes
|
|
152
|
+
// are recorded — never a post-write value — even across backend instances
|
|
153
|
+
// with separate identities (the sub-agent concurrency case).
|
|
154
|
+
await Promise.allSettled([
|
|
155
|
+
a.edit("ignored/race.txt", "orig", "A"),
|
|
156
|
+
b.edit("ignored/race.txt", "orig", "B"),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
expect(observer.before.size).toBe(1);
|
|
160
|
+
expect(Buffer.from(observer.before.get("ignored/race.txt")!).toString("utf8")).toBe("orig");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -41,10 +41,11 @@ describe("HITL approval integration", () => {
|
|
|
41
41
|
mcpServerSlug: "my-server",
|
|
42
42
|
requiresApproval: true,
|
|
43
43
|
approvalMessage: "Execute dangerous_tool",
|
|
44
|
+
source: "classifier_default",
|
|
44
45
|
}],
|
|
45
46
|
]),
|
|
46
47
|
toolServerMap: new Map([["dangerous_tool", "my-server"]]),
|
|
47
|
-
|
|
48
|
+
globalBypass: false,
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
sb.processEvent({
|
|
@@ -83,7 +84,7 @@ describe("HITL approval integration", () => {
|
|
|
83
84
|
sb.setApprovalProvider({
|
|
84
85
|
policies: new Map(),
|
|
85
86
|
toolServerMap: new Map([["safe_tool", "my-server"]]),
|
|
86
|
-
|
|
87
|
+
globalBypass: false,
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
sb.processEvent({
|
|
@@ -110,7 +111,7 @@ describe("HITL approval integration", () => {
|
|
|
110
111
|
expect(tc?.requiresApproval).toBe(false);
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
it("does NOT set WAITING_FOR_APPROVAL
|
|
114
|
+
it("does NOT set WAITING_FOR_APPROVAL under the global bypass", () => {
|
|
114
115
|
const status = create(AgentExecutionStatusSchema, {});
|
|
115
116
|
const sb = new StatusBuilder("exec-3", status);
|
|
116
117
|
|
|
@@ -121,10 +122,11 @@ describe("HITL approval integration", () => {
|
|
|
121
122
|
mcpServerSlug: "srv",
|
|
122
123
|
requiresApproval: true,
|
|
123
124
|
approvalMessage: "msg",
|
|
125
|
+
source: "classifier_default",
|
|
124
126
|
}],
|
|
125
127
|
]),
|
|
126
128
|
toolServerMap: new Map([["tool", "srv"]]),
|
|
127
|
-
|
|
129
|
+
globalBypass: true,
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
sb.processEvent({
|
|
@@ -159,10 +161,11 @@ describe("HITL approval integration", () => {
|
|
|
159
161
|
mcpServerSlug: "srv",
|
|
160
162
|
requiresApproval: true,
|
|
161
163
|
approvalMessage: "Run tool_x",
|
|
164
|
+
source: "classifier_default",
|
|
162
165
|
}],
|
|
163
166
|
]),
|
|
164
167
|
toolServerMap: new Map([["tool_x", "srv"]]),
|
|
165
|
-
|
|
168
|
+
globalBypass: false,
|
|
166
169
|
});
|
|
167
170
|
|
|
168
171
|
sb.processEvent({
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic regression test (triangulation — RUNNER boundary).
|
|
3
|
+
*
|
|
4
|
+
* Reproduces the user-reported scenario exactly: a gated `getAppState` call
|
|
5
|
+
* (open-computer-use MCP) is approved with APPROVE_ALL, which leases the whole
|
|
6
|
+
* server so the resume runs `getAppState` AND every following computer-use tool
|
|
7
|
+
* to completion in ONE durable-checkpoint resume — no further interrupts.
|
|
8
|
+
*
|
|
9
|
+
* The reported symptom is that the LEADING thinking block and the FIRST tool
|
|
10
|
+
* call (`getAppState`) vanish from the transcript after approve-all, while the
|
|
11
|
+
* later auto-executed tools remain. This test pins what the RUNNER persists at
|
|
12
|
+
* its single gRPC chokepoint (`updateStatus`). If the runner is faithful, the
|
|
13
|
+
* final status it sends must be a SUPERSET of run-1's transcript: the leading
|
|
14
|
+
* thinking block survives, the gated tool call is reconciled in place (exactly
|
|
15
|
+
* one copy, now COMPLETED), and the leased follow-up tool is appended.
|
|
16
|
+
*
|
|
17
|
+
* Models the cloud path deterministically, mirroring hitl-resume-history.test.ts:
|
|
18
|
+
* - the persisted execution already holds the run-1 transcript
|
|
19
|
+
* (THINKING + AI(getAppState WAITING_APPROVAL, approvalAction=APPROVE_ALL));
|
|
20
|
+
* - the durable checkpoint exists (getState reports prior values + a pending
|
|
21
|
+
* interrupt), so streamEvents re-emits ONLY post-interrupt events;
|
|
22
|
+
* - the lease drops open-computer-use from the policy map, so the follow-up
|
|
23
|
+
* `click` is NOT re-gated (its key is absent from approvalPolicies) and the
|
|
24
|
+
* resume reaches a terminal COMPLETED phase.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
28
|
+
import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
|
|
29
|
+
import { create, clone } from "@bufbuild/protobuf";
|
|
30
|
+
import {
|
|
31
|
+
AgentExecutionStatusSchema,
|
|
32
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
33
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
34
|
+
import {
|
|
35
|
+
AgentMessageSchema,
|
|
36
|
+
ToolCallSchema,
|
|
37
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
38
|
+
import {
|
|
39
|
+
ApprovalAction,
|
|
40
|
+
ExecutionPhase,
|
|
41
|
+
MessageType,
|
|
42
|
+
ToolCallStatus,
|
|
43
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
44
|
+
import type { V3ProtocolEvent } from "../v3-event-recorder.js";
|
|
45
|
+
import type { Config } from "../../../config.js";
|
|
46
|
+
import type { SetupResult } from "../setup.js";
|
|
47
|
+
|
|
48
|
+
// The Temporal activity context is unavailable outside a worker.
|
|
49
|
+
vi.mock("@temporalio/activity", () => ({
|
|
50
|
+
Context: {
|
|
51
|
+
current: () => ({
|
|
52
|
+
cancellationSignal: { aborted: false },
|
|
53
|
+
heartbeat: vi.fn(),
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
CancelledFailure: class CancelledFailure extends Error {},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.mock("../../../idle-watchdog.js", () => ({
|
|
60
|
+
activityStarted: vi.fn(),
|
|
61
|
+
activityFinished: vi.fn(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Capture every persisted status (the gRPC chokepoint) for assertions.
|
|
65
|
+
const { persistedStatuses } = vi.hoisted(() => ({
|
|
66
|
+
persistedStatuses: [] as AgentExecutionStatus[],
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
vi.mock("../../../client/stigmer-client.js", () => ({
|
|
70
|
+
StigmerClient: vi.fn().mockImplementation(() => ({
|
|
71
|
+
updateStatus: vi.fn(async (_id: string, status: AgentExecutionStatus) => {
|
|
72
|
+
// Snapshot point-in-time: persistStatus mutates the same object in place.
|
|
73
|
+
persistedStatuses.push(clone(AgentExecutionStatusSchema, status));
|
|
74
|
+
return { signal: 0 };
|
|
75
|
+
}),
|
|
76
|
+
getExecution: vi.fn(),
|
|
77
|
+
})),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// performSetup is mocked per-test to return a fake durable-resume SetupResult.
|
|
81
|
+
vi.mock("../setup.js", () => ({ performSetup: vi.fn() }));
|
|
82
|
+
|
|
83
|
+
import { createDeepAgentActivities } from "../index.js";
|
|
84
|
+
import { performSetup } from "../setup.js";
|
|
85
|
+
|
|
86
|
+
const MCP_SLUG = "open-computer-use";
|
|
87
|
+
const GATED_TOOL_CALL_ID = "toolu_getappstate_01";
|
|
88
|
+
const LEASED_TOOL_CALL_ID = "toolu_click_01";
|
|
89
|
+
const THINKING_TEXT = "Let me capture the current app state before clicking anything.";
|
|
90
|
+
const PRE_RESUME_AI_TEXT = "I'll capture the app state first.";
|
|
91
|
+
const POST_RESUME_AI_TEXT = "Captured the app state and clicked the element.";
|
|
92
|
+
|
|
93
|
+
function v3Event(
|
|
94
|
+
seq: number,
|
|
95
|
+
method: string,
|
|
96
|
+
data: unknown,
|
|
97
|
+
namespace: string[] = [],
|
|
98
|
+
): V3ProtocolEvent {
|
|
99
|
+
return {
|
|
100
|
+
type: "event",
|
|
101
|
+
seq,
|
|
102
|
+
method,
|
|
103
|
+
params: { namespace, timestamp: Date.now(), data },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toolMessageOutput(content: string, toolCallId: string): unknown {
|
|
108
|
+
return {
|
|
109
|
+
lc: 1,
|
|
110
|
+
type: "constructor",
|
|
111
|
+
id: ["langchain_core", "messages", "ToolMessage"],
|
|
112
|
+
kwargs: { content, tool_call_id: toolCallId },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The events a durable resume re-emits after APPROVE_ALL: the gated tool
|
|
118
|
+
* executes (reconciled in place), then the leased follow-up tool auto-runs,
|
|
119
|
+
* then the assistant produces a final turn. No interrupts — the lease dropped
|
|
120
|
+
* open-computer-use from the policy map.
|
|
121
|
+
*/
|
|
122
|
+
function resumeEvents(): V3ProtocolEvent[] {
|
|
123
|
+
return [
|
|
124
|
+
// 1. Gated getAppState now runs (reconciled to the seeded WAITING call).
|
|
125
|
+
v3Event(0, "tools", {
|
|
126
|
+
event: "tool-started",
|
|
127
|
+
tool_call_id: GATED_TOOL_CALL_ID,
|
|
128
|
+
tool_name: "getAppState",
|
|
129
|
+
input: {},
|
|
130
|
+
}, [`tools:${GATED_TOOL_CALL_ID}`]),
|
|
131
|
+
v3Event(1, "tools", {
|
|
132
|
+
event: "tool-finished",
|
|
133
|
+
tool_call_id: GATED_TOOL_CALL_ID,
|
|
134
|
+
output: toolMessageOutput("app-state-captured: 3 windows", GATED_TOOL_CALL_ID),
|
|
135
|
+
}, [`tools:${GATED_TOOL_CALL_ID}`]),
|
|
136
|
+
// 2. The model decides to click; the leased tool auto-runs (no gate).
|
|
137
|
+
v3Event(2, "messages", { event: "message-start", run_id: "r2" }),
|
|
138
|
+
v3Event(3, "tools", {
|
|
139
|
+
event: "tool-started",
|
|
140
|
+
tool_call_id: LEASED_TOOL_CALL_ID,
|
|
141
|
+
tool_name: "click",
|
|
142
|
+
input: { x: 10, y: 20 },
|
|
143
|
+
}, [`tools:${LEASED_TOOL_CALL_ID}`]),
|
|
144
|
+
v3Event(4, "tools", {
|
|
145
|
+
event: "tool-finished",
|
|
146
|
+
tool_call_id: LEASED_TOOL_CALL_ID,
|
|
147
|
+
output: toolMessageOutput("clicked at (10,20)", LEASED_TOOL_CALL_ID),
|
|
148
|
+
}, [`tools:${LEASED_TOOL_CALL_ID}`]),
|
|
149
|
+
// 3. Final assistant turn.
|
|
150
|
+
v3Event(5, "messages", { event: "message-start", run_id: "r3" }),
|
|
151
|
+
v3Event(6, "messages", {
|
|
152
|
+
event: "content-block-delta",
|
|
153
|
+
index: 0,
|
|
154
|
+
delta: { type: "text-delta", text: POST_RESUME_AI_TEXT },
|
|
155
|
+
run_id: "r3",
|
|
156
|
+
}),
|
|
157
|
+
v3Event(7, "messages", { event: "message-finish", run_id: "r3" }),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function mockV3Run(events: V3ProtocolEvent[]) {
|
|
162
|
+
return {
|
|
163
|
+
[Symbol.asyncIterator]: async function* () {
|
|
164
|
+
for (const e of events) yield e;
|
|
165
|
+
},
|
|
166
|
+
output: Promise.resolve({ messages: [] }),
|
|
167
|
+
abort: vi.fn(),
|
|
168
|
+
signal: new AbortController().signal,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The run-1 transcript already persisted on the execution: a leading thinking
|
|
174
|
+
* block, then the gated getAppState call the user approved with APPROVE_ALL.
|
|
175
|
+
*/
|
|
176
|
+
function persistedRunOneStatus(): AgentExecutionStatus {
|
|
177
|
+
return create(AgentExecutionStatusSchema, {
|
|
178
|
+
phase: ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
179
|
+
startedAt: "2026-06-15T00:00:00.000Z",
|
|
180
|
+
messages: [
|
|
181
|
+
create(AgentMessageSchema, {
|
|
182
|
+
type: MessageType.MESSAGE_THINKING,
|
|
183
|
+
content: THINKING_TEXT,
|
|
184
|
+
timestamp: "2026-06-15T00:00:01.000Z",
|
|
185
|
+
}),
|
|
186
|
+
create(AgentMessageSchema, {
|
|
187
|
+
type: MessageType.MESSAGE_AI,
|
|
188
|
+
content: PRE_RESUME_AI_TEXT,
|
|
189
|
+
timestamp: "2026-06-15T00:00:02.000Z",
|
|
190
|
+
toolCalls: [
|
|
191
|
+
create(ToolCallSchema, {
|
|
192
|
+
id: GATED_TOOL_CALL_ID,
|
|
193
|
+
name: "getAppState",
|
|
194
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
195
|
+
requiresApproval: true,
|
|
196
|
+
approvalMessage: "Execute getAppState tool",
|
|
197
|
+
approvalAction: ApprovalAction.APPROVE_ALL,
|
|
198
|
+
mcpServerSlug: MCP_SLUG,
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function fakeDurableResumeSetup(): SetupResult {
|
|
207
|
+
let streamed = false;
|
|
208
|
+
|
|
209
|
+
const pendingInterruptState = {
|
|
210
|
+
values: { messages: [{ role: "assistant", content: PRE_RESUME_AI_TEXT }] },
|
|
211
|
+
tasks: [{
|
|
212
|
+
id: "task-1",
|
|
213
|
+
interrupts: [{
|
|
214
|
+
value: {
|
|
215
|
+
tool_call_id: GATED_TOOL_CALL_ID,
|
|
216
|
+
tool_name: "getAppState",
|
|
217
|
+
mcp_server_slug: MCP_SLUG,
|
|
218
|
+
message: "Execute getAppState tool",
|
|
219
|
+
},
|
|
220
|
+
resumeValue: undefined,
|
|
221
|
+
}],
|
|
222
|
+
}],
|
|
223
|
+
};
|
|
224
|
+
const resolvedState = {
|
|
225
|
+
values: { messages: [{ role: "assistant", content: PRE_RESUME_AI_TEXT }] },
|
|
226
|
+
tasks: [],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const agentGraph = {
|
|
230
|
+
// Before the resumed stream: a pending interrupt awaits the decision.
|
|
231
|
+
// After the stream: the interrupt is resolved (no pending approvals).
|
|
232
|
+
getState: vi.fn(async () => (streamed ? resolvedState : pendingInterruptState)),
|
|
233
|
+
streamEvents: vi.fn(async () => {
|
|
234
|
+
streamed = true;
|
|
235
|
+
return mockV3Run(resumeEvents());
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Approve-all leased open-computer-use: the resume's policy map no longer
|
|
240
|
+
// contains the leased server's tools, so the follow-up `click` is NOT gated.
|
|
241
|
+
// Only the originally-gated getAppState policy remains for resume resolution.
|
|
242
|
+
const approvalPolicies = new Map([
|
|
243
|
+
[`${MCP_SLUG}/getAppState`, {
|
|
244
|
+
toolName: "getAppState",
|
|
245
|
+
mcpServerSlug: MCP_SLUG,
|
|
246
|
+
requiresApproval: true,
|
|
247
|
+
approvalMessage: "Execute getAppState tool",
|
|
248
|
+
}],
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
agentGraph,
|
|
253
|
+
langgraphConfig: { configurable: { thread_id: "thread-1" } },
|
|
254
|
+
langgraphInput: { messages: [{ role: "user", content: "Self-DM me" }] },
|
|
255
|
+
execution: {
|
|
256
|
+
spec: { message: "Self-DM me to confirm you can." },
|
|
257
|
+
status: persistedRunOneStatus(),
|
|
258
|
+
},
|
|
259
|
+
agent: {},
|
|
260
|
+
session: { spec: { workspaceEntries: [] } },
|
|
261
|
+
workspaceBackend: { rootDir: "/tmp/ws" },
|
|
262
|
+
mcpConnection: null,
|
|
263
|
+
mergedEnvVars: {},
|
|
264
|
+
secretKeys: new Set<string>(),
|
|
265
|
+
modelName: "claude-sonnet",
|
|
266
|
+
gracefulStop: undefined,
|
|
267
|
+
artifactStorage: makeInMemoryArtifactStorage({ urlBase: "https://artifacts.local/" }).storage,
|
|
268
|
+
provisionResults: [],
|
|
269
|
+
approvalPolicies,
|
|
270
|
+
// Both computer-use tools belong to the leased server.
|
|
271
|
+
toolServerMap: new Map([["getAppState", MCP_SLUG], ["click", MCP_SLUG]]),
|
|
272
|
+
autoApproveAll: false,
|
|
273
|
+
hasStructuredOutput: false,
|
|
274
|
+
streamVersion: "v3",
|
|
275
|
+
} as unknown as SetupResult;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const baseConfig: Config = {
|
|
279
|
+
taskQueue: "test-queue",
|
|
280
|
+
temporalAddress: "localhost:7233",
|
|
281
|
+
temporalNamespace: "default",
|
|
282
|
+
stigmerBackendEndpoint: "http://localhost:7234",
|
|
283
|
+
stigmerToken: null,
|
|
284
|
+
cursorApiKey: "",
|
|
285
|
+
workspaceRootDir: "/tmp/test",
|
|
286
|
+
mode: "cloud",
|
|
287
|
+
proxyEndpoint: "http://localhost:7234",
|
|
288
|
+
maxConcurrentActivities: 5,
|
|
289
|
+
idleTimeoutSeconds: null,
|
|
290
|
+
cloudModeEnabled: true,
|
|
291
|
+
checkpointerType: "http",
|
|
292
|
+
checkpointerProxyEndpoint: "http://localhost:7234",
|
|
293
|
+
primaryModel: "claude-sonnet",
|
|
294
|
+
cursorStreamStallTimeoutMs: 180000,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
describe("ExecuteDeepAgent — APPROVE_ALL durable resume preserves leading thinking + first tool call", () => {
|
|
298
|
+
beforeEach(() => {
|
|
299
|
+
persistedStatuses.length = 0;
|
|
300
|
+
vi.mocked(performSetup).mockResolvedValue(fakeDurableResumeSetup());
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("keeps the leading thinking block and the gated getAppState call, and appends the leased tool", async () => {
|
|
304
|
+
const activities = createDeepAgentActivities(baseConfig);
|
|
305
|
+
await activities.ExecuteDeepAgent("exec-1", "thread-1");
|
|
306
|
+
|
|
307
|
+
expect(persistedStatuses.length).toBeGreaterThan(0);
|
|
308
|
+
const finalStatus = persistedStatuses.at(-1)!;
|
|
309
|
+
|
|
310
|
+
// 1. The LEADING thinking block survives the resume (the reported symptom).
|
|
311
|
+
const thinkingMessages = finalStatus.messages.filter(
|
|
312
|
+
m => m.type === MessageType.MESSAGE_THINKING,
|
|
313
|
+
);
|
|
314
|
+
expect(thinkingMessages).toHaveLength(1);
|
|
315
|
+
expect(thinkingMessages[0].content).toBe(THINKING_TEXT);
|
|
316
|
+
|
|
317
|
+
// 2. The thinking block stays at the FRONT — it is still messages[0], proving
|
|
318
|
+
// the transcript was not front-truncated and rebuilt from a later turn.
|
|
319
|
+
expect(finalStatus.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
|
|
320
|
+
|
|
321
|
+
// 3. The FIRST tool call (getAppState) is reconciled in place: exactly one
|
|
322
|
+
// copy, now COMPLETED with its result — not dropped, not duplicated.
|
|
323
|
+
const getAppStateCalls = finalStatus.messages
|
|
324
|
+
.flatMap(m => m.toolCalls)
|
|
325
|
+
.filter(tc => tc.id === GATED_TOOL_CALL_ID);
|
|
326
|
+
expect(getAppStateCalls).toHaveLength(1);
|
|
327
|
+
expect(getAppStateCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
328
|
+
expect(getAppStateCalls[0].result).toContain("app-state-captured");
|
|
329
|
+
|
|
330
|
+
// 4. The leased follow-up tool is appended.
|
|
331
|
+
const clickCalls = finalStatus.messages
|
|
332
|
+
.flatMap(m => m.toolCalls)
|
|
333
|
+
.filter(tc => tc.id === LEASED_TOOL_CALL_ID);
|
|
334
|
+
expect(clickCalls).toHaveLength(1);
|
|
335
|
+
expect(clickCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
336
|
+
|
|
337
|
+
// 5. The post-resume assistant turn is appended and the run completes.
|
|
338
|
+
const allText = finalStatus.messages.map(m => m.content).join("\n");
|
|
339
|
+
expect(allText).toContain(POST_RESUME_AI_TEXT);
|
|
340
|
+
expect(finalStatus.phase).toBe(ExecutionPhase.EXECUTION_COMPLETED);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
21
|
+
import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
|
|
21
22
|
import { create, clone } from "@bufbuild/protobuf";
|
|
22
23
|
import {
|
|
23
24
|
AgentExecutionStatusSchema,
|
|
@@ -218,11 +219,7 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
218
219
|
secretKeys: new Set<string>(),
|
|
219
220
|
modelName: "claude-sonnet",
|
|
220
221
|
gracefulStop: undefined,
|
|
221
|
-
artifactStorage: {
|
|
222
|
-
upload: vi.fn(async (key: string) => key),
|
|
223
|
-
getDownloadUrl: vi.fn(async (key: string) => `https://artifacts.local/${key}`),
|
|
224
|
-
exists: vi.fn(async () => false),
|
|
225
|
-
},
|
|
222
|
+
artifactStorage: makeInMemoryArtifactStorage({ urlBase: "https://artifacts.local/" }).storage,
|
|
226
223
|
provisionResults: [],
|
|
227
224
|
approvalPolicies,
|
|
228
225
|
toolServerMap: new Map([["echo", "test-mcp"]]),
|
|
@@ -10,6 +10,7 @@ import { InlinePublisher } from "../inline-publisher.js";
|
|
|
10
10
|
import { StatusBuilder } from "../status-builder.js";
|
|
11
11
|
import { LocalWorkspaceBackend } from "../../../shared/workspace/local-backend.js";
|
|
12
12
|
import type { ArtifactStorage } from "../../../shared/artifact-storage.js";
|
|
13
|
+
import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
|
|
13
14
|
import type { WorkspaceBackend } from "../../../shared/workspace/types.js";
|
|
14
15
|
|
|
15
16
|
function makeStatusBuilder(): StatusBuilder {
|
|
@@ -35,19 +36,16 @@ function mockArtifactStorage(): ArtifactStorage & {
|
|
|
35
36
|
uploadedKeys: string[];
|
|
36
37
|
uploadedContent: Map<string, Buffer>;
|
|
37
38
|
} {
|
|
39
|
+
// Canonical double; `uploadedKeys`/`uploadedContent` mirror the backing store
|
|
40
|
+
// so the existing assertions keep working and `download` reads back uploads.
|
|
41
|
+
const { storage, blobs } = makeInMemoryArtifactStorage({ urlBase: "http://localhost:7235/" });
|
|
38
42
|
const uploadedKeys: string[] = [];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
uploadedContent.set(key, content);
|
|
46
|
-
return key;
|
|
47
|
-
}),
|
|
48
|
-
getDownloadUrl: vi.fn(async (key: string) => `http://localhost:7235/${key}`),
|
|
49
|
-
exists: vi.fn(async () => false),
|
|
50
|
-
};
|
|
43
|
+
storage.upload.mockImplementation(async (key: string, content: Buffer) => {
|
|
44
|
+
uploadedKeys.push(key);
|
|
45
|
+
blobs.set(key, Buffer.from(content));
|
|
46
|
+
return key;
|
|
47
|
+
});
|
|
48
|
+
return Object.assign(storage, { uploadedKeys, uploadedContent: blobs });
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function sha256(content: string): string {
|
|
@@ -83,7 +81,6 @@ describe("InlinePublisher", () => {
|
|
|
83
81
|
expect(artifact.sandboxPath).toBe("src/main.ts");
|
|
84
82
|
expect(artifact.kind).toBe(ExecutionArtifactKind.FILE);
|
|
85
83
|
expect(artifact.storageKey).toBe("artifacts/exec-123/main.ts");
|
|
86
|
-
expect(artifact.downloadUrl).toBe("http://localhost:7235/artifacts/exec-123/main.ts");
|
|
87
84
|
expect(artifact.contentHash).toBe(sha256("console.log('hello');"));
|
|
88
85
|
expect(Number(artifact.sizeBytes)).toBeGreaterThan(0);
|
|
89
86
|
});
|
|
@@ -164,6 +161,27 @@ describe("InlinePublisher", () => {
|
|
|
164
161
|
expect(sb.currentStatus.artifacts[0].contentHash).toBe(expected);
|
|
165
162
|
});
|
|
166
163
|
|
|
164
|
+
it("never publishes a secret-like file to artifact storage (design doc 12, D4)", async () => {
|
|
165
|
+
// Under the global bypass a secret write is not blocked up front, so it would
|
|
166
|
+
// otherwise be uploaded here. The publisher must withhold it: no read, no
|
|
167
|
+
// upload, no registered artifact — the secret's bytes never reach storage.
|
|
168
|
+
const secretBackend = mockWorkspaceBackend({ ".env": "API_KEY=super-secret-value" });
|
|
169
|
+
const readSpy = secretBackend.readFile as ReturnType<typeof vi.fn>;
|
|
170
|
+
const pub = new InlinePublisher({
|
|
171
|
+
workspaceBackend: secretBackend,
|
|
172
|
+
artifactStorage: storage,
|
|
173
|
+
statusWriter: sb,
|
|
174
|
+
executionId: "exec-secret",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await pub.publish(".env");
|
|
178
|
+
|
|
179
|
+
expect(storage.uploadedKeys).toHaveLength(0);
|
|
180
|
+
expect(sb.currentStatus.artifacts).toHaveLength(0);
|
|
181
|
+
expect(readSpy).not.toHaveBeenCalled(); // withheld before the file is even read
|
|
182
|
+
expect(pub.publishedPaths.size).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
167
185
|
it("guesses content type for common extensions", async () => {
|
|
168
186
|
const jsonBackend = mockWorkspaceBackend({ "data.json": '{"key":"val"}' });
|
|
169
187
|
const pub = new InlinePublisher({
|