@stigmer/runner 3.10.0 → 3.11.1-dev.20260812192248
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/README.md +14 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +39 -0
- package/dist/activities/discover-mcp-server.js +165 -28
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +14 -2
- package/dist/activities/emit-event.js +52 -17
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +59 -3
- package/dist/activities/execute-cursor/error-classifier.js +115 -6
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +51 -11
- package/dist/activities/execute-cursor/index.js +177 -93
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
- package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
- package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +51 -4
- package/dist/activities/execute-cursor/prompt-builder.js +49 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
- package/dist/activities/execute-cursor/turn-recovery.js +193 -0
- package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +65 -24
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
- package/dist/activities/execute-deep-agent/shell-env.js +7 -9
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +15 -2
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +27 -6
- package/dist/activities/execute-deep-agent/subagent-wiring.js +26 -5
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -3
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/run-command.d.ts +5 -2
- package/dist/activities/run-command.js +22 -10
- package/dist/activities/run-command.js.map +1 -1
- package/dist/activities/run-env.d.ts +40 -0
- package/dist/activities/run-env.js +66 -0
- package/dist/activities/run-env.js.map +1 -0
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -21
- package/dist/client/stigmer-client.js +106 -29
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +64 -0
- package/dist/encryption/config.js +108 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +57 -0
- package/dist/middleware/path-normalization.js +109 -0
- package/dist/middleware/path-normalization.js.map +1 -0
- package/dist/middleware/types.d.ts +8 -0
- package/dist/payload-codecs.d.ts +17 -0
- package/dist/payload-codecs.js +42 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +21 -17
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -18
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +23 -1
- package/dist/shared/artifact-storage.js +51 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +26 -1
- package/dist/shared/checkpointer/http-saver.js +54 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/conversation-catchup.d.ts +9 -2
- package/dist/shared/conversation-catchup.js +39 -6
- package/dist/shared/conversation-catchup.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +5 -1
- package/dist/shared/grpc-retry.js +5 -1
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/http-retry.d.ts +85 -0
- package/dist/shared/http-retry.js +101 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +11 -5
- package/dist/shared/mcp-manager.js +47 -8
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
- package/dist/shared/mcp-schema-sanitizer.js +197 -0
- package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
- package/dist/shared/model-client.d.ts +18 -5
- package/dist/shared/model-client.js +149 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/model-pricing-data.d.ts +2 -0
- package/dist/shared/model-pricing-data.js +13 -3
- package/dist/shared/model-pricing-data.js.map +1 -1
- package/dist/shared/model-registry.js +3 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +33 -0
- package/dist/shared/plan-mode-permissions.js +35 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/shared/registry-endpoint.d.ts +20 -0
- package/dist/shared/registry-endpoint.js +25 -0
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +24 -0
- package/dist/shared/runner-credential-keys.js +47 -0
- package/dist/shared/runner-credential-keys.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/resolve.d.ts +19 -1
- package/dist/workflow-engine/resolve.js +37 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +9 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +9 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +287 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +65 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +242 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +203 -31
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +151 -16
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +131 -7
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +209 -109
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +85 -9
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +178 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +85 -29
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +30 -2
- package/src/activities/execute-deep-agent/subagent-wiring.ts +39 -6
- package/src/activities/hydrate-workflow-execution.ts +8 -3
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/bootstrap.ts +5 -0
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +110 -11
- package/src/client/stigmer-client.ts +150 -37
- package/src/encryption/config.ts +155 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/middleware/__tests__/path-normalization.test.ts +140 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/path-normalization.ts +125 -0
- package/src/middleware/types.ts +11 -0
- package/src/payload-codecs.ts +62 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +26 -26
- package/src/runner.ts +12 -27
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/artifact-storage.test.ts +145 -2
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +68 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +137 -15
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +200 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-pricing.test.ts +62 -2
- package/src/shared/__tests__/model-registry.test.ts +22 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/artifact-storage.ts +75 -10
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +71 -8
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +139 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +56 -8
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/model-client.ts +179 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +37 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +9 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
- package/src/workflows/types.ts +8 -0
- package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
- package/dist/activities/execute-cursor/mcp-config.js +0 -39
- package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
|
@@ -219,6 +219,7 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
219
219
|
// path shared across files would serialize parallel test workers for real.
|
|
220
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-history" },
|
|
221
221
|
mcpConnection: null,
|
|
222
|
+
datastoreToolsMissing: [],
|
|
222
223
|
mergedEnvVars: {},
|
|
223
224
|
secretKeys: new Set<string>(),
|
|
224
225
|
modelName: "claude-sonnet",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that
|
|
3
|
+
* a plan-mode PARENT graph accepts workspace-relative paths (issue #429).
|
|
4
|
+
*
|
|
5
|
+
* Before the fix, deepagents' permission enforcement canonicalized every
|
|
6
|
+
* filesystem tool-call path BEFORE any rule ran and refused non-absolute
|
|
7
|
+
* shapes, so on a rule-bearing graph a workspace-relative call — reads
|
|
8
|
+
* included — died with `path must be absolute` instead of just working. The
|
|
9
|
+
* path-normalization middleware (middleware/path-normalization.ts) rewrites
|
|
10
|
+
* relative paths to workspace-absolute at our seam, before enforcement sees
|
|
11
|
+
* them.
|
|
12
|
+
*
|
|
13
|
+
* The graph here is composed exactly the way setup.ts composes the parent:
|
|
14
|
+
* the PRODUCTION buildMiddlewareStack (pathNormalization present, the
|
|
15
|
+
* rule-bearing shape) + the CAS capture backend + the PRODUCTION
|
|
16
|
+
* PLAN_MODE_PERMISSIONS constant. These tests are also the empirical proof
|
|
17
|
+
* that langchain's wrapToolCall seam delivers rewritten args to the tool —
|
|
18
|
+
* if it did not, the relative read below could never succeed.
|
|
19
|
+
*
|
|
20
|
+
* The sub-agent twin of this contract is pinned in
|
|
21
|
+
* subagent-plan-mode-permissions.test.ts (issue #255 wiring).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
25
|
+
import { mkdtemp, mkdir, rm, readFile, writeFile, access } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { HumanMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
|
|
29
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
30
|
+
import { createDeepAgent } from "deepagents";
|
|
31
|
+
|
|
32
|
+
import { PLAN_MODE_PERMISSIONS } from "../../../shared/plan-mode-permissions.js";
|
|
33
|
+
import { buildMiddlewareStack } from "../../../middleware/index.js";
|
|
34
|
+
import { createCasCaptureBackend } from "../cas-capture-backend.js";
|
|
35
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
36
|
+
import { ScriptedModel, type ScriptSelector } from "../__test-utils__/scripted-model.js";
|
|
37
|
+
|
|
38
|
+
const SEEDED_CONTENT = "PLAN_MODE_README_TOKEN: hello from the seeded file";
|
|
39
|
+
|
|
40
|
+
describe("plan-mode parent path normalization (issue #429)", () => {
|
|
41
|
+
let root: string;
|
|
42
|
+
let observer: CasCaptureObserver;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
root = await mkdtemp(join(tmpdir(), "plan-path-"));
|
|
46
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
47
|
+
await writeFile(join(root, "src/notes.md"), SEEDED_CONTENT);
|
|
48
|
+
observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await rm(root, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a parent graph the way setup.ts does in plan mode: the production
|
|
57
|
+
* middleware stack with pathNormalization set (derived, like the graph's
|
|
58
|
+
* permissions, from the plan-mode rules), a filesystem-only CAS capture
|
|
59
|
+
* backend (no shellEnv — plan mode clears it), and PLAN_MODE_PERMISSIONS
|
|
60
|
+
* on the graph.
|
|
61
|
+
*/
|
|
62
|
+
async function buildPlanModeParent(script: ScriptSelector) {
|
|
63
|
+
const { middleware } = buildMiddlewareStack({
|
|
64
|
+
approvalGate: null,
|
|
65
|
+
pathNormalization: { rootDir: root },
|
|
66
|
+
});
|
|
67
|
+
const backend = await createCasCaptureBackend({ rootDir: root, observer });
|
|
68
|
+
return createDeepAgent({
|
|
69
|
+
model: new ScriptedModel(script),
|
|
70
|
+
checkpointer: new MemorySaver() as never,
|
|
71
|
+
backend,
|
|
72
|
+
middleware: middleware as never[],
|
|
73
|
+
permissions: PLAN_MODE_PERMISSIONS,
|
|
74
|
+
} as Parameters<typeof createDeepAgent>[0]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolResultById(messages: BaseMessage[], toolCallId: string): string {
|
|
78
|
+
const match = messages.find(
|
|
79
|
+
(m): m is ToolMessage => m instanceof ToolMessage && m.tool_call_id === toolCallId,
|
|
80
|
+
);
|
|
81
|
+
expect(match, `expected a tool result for call '${toolCallId}'`).toBeDefined();
|
|
82
|
+
return typeof match!.content === "string" ? match!.content : JSON.stringify(match!.content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function invokeOnce(script: ScriptSelector, threadId: string) {
|
|
86
|
+
const agent = await buildPlanModeParent(script);
|
|
87
|
+
return (await agent.invoke(
|
|
88
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
89
|
+
{ configurable: { thread_id: threadId }, recursionLimit: 50 },
|
|
90
|
+
)) as { messages: BaseMessage[] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it("a prompt-compliant relative read succeeds on the first tool round", async () => {
|
|
94
|
+
const result = await invokeOnce(
|
|
95
|
+
() => ({
|
|
96
|
+
toolCalls: [
|
|
97
|
+
{ name: "read_file", args: { file_path: "src/notes.md" }, id: "c_read_rel" },
|
|
98
|
+
],
|
|
99
|
+
done: "done",
|
|
100
|
+
}),
|
|
101
|
+
"t_read_rel",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const readResult = toolResultById(result.messages, "c_read_rel");
|
|
105
|
+
expect(readResult).toContain("PLAN_MODE_README_TOKEN");
|
|
106
|
+
expect(readResult).not.toMatch(/path must be absolute/i);
|
|
107
|
+
expect(readResult).not.toMatch(/permission denied/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("a relative write is refused by the RULES — permission denied, not a path-shape error", async () => {
|
|
111
|
+
const result = await invokeOnce(
|
|
112
|
+
() => ({
|
|
113
|
+
toolCalls: [
|
|
114
|
+
{ name: "write_file", args: { file_path: "src/out.txt", content: "x" }, id: "c_write_rel" },
|
|
115
|
+
],
|
|
116
|
+
done: "done",
|
|
117
|
+
}),
|
|
118
|
+
"t_write_rel",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// The deny-all-writes rule fires on the normalized path — the honest
|
|
122
|
+
// plan-mode refusal the model can reason about, instead of the
|
|
123
|
+
// pre-fix validation error that taught it to abandon relative paths.
|
|
124
|
+
expect(toolResultById(result.messages, "c_write_rel")).toMatch(/permission denied for write/i);
|
|
125
|
+
expect(toolResultById(result.messages, "c_write_rel")).not.toMatch(/path must be absolute/i);
|
|
126
|
+
await expect(access(join(root, "src/out.txt"))).rejects.toThrow();
|
|
127
|
+
expect(observer.before.size).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("an escaping relative read is still refused — normalization grants no new reachability", async () => {
|
|
131
|
+
// Outside the root, reachable only if `..` were naively joined away.
|
|
132
|
+
const result = await invokeOnce(
|
|
133
|
+
() => ({
|
|
134
|
+
toolCalls: [
|
|
135
|
+
{ name: "read_file", args: { file_path: "../escape.txt" }, id: "c_read_escape" },
|
|
136
|
+
],
|
|
137
|
+
done: "done",
|
|
138
|
+
}),
|
|
139
|
+
"t_read_escape",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Left raw by the middleware, refused by upstream validation exactly as
|
|
143
|
+
// before the fix.
|
|
144
|
+
expect(toolResultById(result.messages, "c_read_escape")).toMatch(/must not contain|path must be absolute/i);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("absolute in-workspace paths keep working unchanged", async () => {
|
|
148
|
+
const result = await invokeOnce(
|
|
149
|
+
() => ({
|
|
150
|
+
toolCalls: [
|
|
151
|
+
{ name: "read_file", args: { file_path: join(root, "src/notes.md") }, id: "c_read_abs" },
|
|
152
|
+
],
|
|
153
|
+
done: "done",
|
|
154
|
+
}),
|
|
155
|
+
"t_read_abs",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(toolResultById(result.messages, "c_read_abs")).toContain("PLAN_MODE_README_TOKEN");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("relative ls flows through normalization to list the workspace directory", async () => {
|
|
162
|
+
const result = await invokeOnce(
|
|
163
|
+
() => ({
|
|
164
|
+
toolCalls: [{ name: "ls", args: { path: "src" }, id: "c_ls_rel" }],
|
|
165
|
+
done: "done",
|
|
166
|
+
}),
|
|
167
|
+
"t_ls_rel",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const lsResult = toolResultById(result.messages, "c_ls_rel");
|
|
171
|
+
expect(lsResult).toContain("notes.md");
|
|
172
|
+
expect(lsResult).not.toMatch(/path must be absolute/i);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -142,6 +142,10 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
it("includes injected files section with size info", () => {
|
|
145
|
+
// The section renders the injector's own result type (sizeBytes). A local
|
|
146
|
+
// structural twin with a `size` field used to live in the prompt builder,
|
|
147
|
+
// and production wiring silently dropped the size — this pin holds the
|
|
148
|
+
// real field name so that drift class cannot return.
|
|
145
149
|
const prompt = buildEnhancedSystemPrompt({
|
|
146
150
|
instructions: "Test",
|
|
147
151
|
provisionResults: [],
|
|
@@ -150,15 +154,40 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
150
154
|
workspaceFileRefs: [],
|
|
151
155
|
workspaceRoot: "/workspace",
|
|
152
156
|
injectedFiles: [
|
|
153
|
-
{ filename: "data.csv", path: ".stigmer/inputs/data.csv",
|
|
154
|
-
{ filename: "notes.md", path: ".stigmer/inputs/notes.md",
|
|
157
|
+
{ filename: "data.csv", path: ".stigmer/inputs/data.csv", sizeBytes: 1024 },
|
|
158
|
+
{ filename: "notes.md", path: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
155
159
|
],
|
|
156
160
|
});
|
|
157
161
|
|
|
158
162
|
expect(prompt).toContain("## Input Files");
|
|
159
163
|
expect(prompt).toContain("`.stigmer/inputs/data.csv` (1024 bytes)");
|
|
160
|
-
expect(prompt).toContain("`.stigmer/inputs/notes.md`");
|
|
161
|
-
|
|
164
|
+
expect(prompt).toContain("`.stigmer/inputs/notes.md` (12 bytes)");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("discloses a duplicate-renamed injected file's original name", () => {
|
|
168
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
169
|
+
instructions: "Test",
|
|
170
|
+
provisionResults: [],
|
|
171
|
+
containerRoot: "",
|
|
172
|
+
skillsPromptSection: "",
|
|
173
|
+
workspaceFileRefs: [],
|
|
174
|
+
workspaceRoot: "/workspace",
|
|
175
|
+
injectedFiles: [
|
|
176
|
+
{ filename: "report.pdf", path: ".stigmer/inputs/report.pdf", sizeBytes: 10 },
|
|
177
|
+
{
|
|
178
|
+
filename: "report-2.pdf",
|
|
179
|
+
path: ".stigmer/inputs/report-2.pdf",
|
|
180
|
+
sizeBytes: 20,
|
|
181
|
+
renamedFrom: "report.pdf",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(prompt).toContain(
|
|
187
|
+
"`.stigmer/inputs/report-2.pdf` (20 bytes) (renamed from duplicate 'report.pdf')",
|
|
188
|
+
);
|
|
189
|
+
// The first file keeps a clean entry — no disclosure noise.
|
|
190
|
+
expect(prompt).toContain("`.stigmer/inputs/report.pdf` (10 bytes)\n");
|
|
162
191
|
});
|
|
163
192
|
|
|
164
193
|
it("produces a git repo description for git workspace entries", () => {
|
|
@@ -361,7 +390,7 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
361
390
|
const planFile = {
|
|
362
391
|
filename: "plan.md",
|
|
363
392
|
path: ".stigmer/inputs/plan.md",
|
|
364
|
-
|
|
393
|
+
sizeBytes: 1024,
|
|
365
394
|
};
|
|
366
395
|
|
|
367
396
|
it("appends the implement-plan directive pointing at the injected plan", () => {
|
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { buildShellEnv, SHELL_ENV_DENYLIST } from "../shell-env.js";
|
|
3
|
+
import { RUNNER_CREDENTIAL_ENV_KEYS } from "../../../shared/runner-credential-keys.js";
|
|
3
4
|
|
|
4
5
|
describe("buildShellEnv", () => {
|
|
5
|
-
it("strips runner
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
6
|
+
it("strips every runner credential from the base env", () => {
|
|
7
|
+
// Plant a value for every name in the source-of-truth list, so a key
|
|
8
|
+
// added to runner-credential-keys.ts is covered here automatically.
|
|
9
|
+
const base: NodeJS.ProcessEnv = { PATH: "/usr/bin", HOME: "/home/runner" };
|
|
10
|
+
for (const key of RUNNER_CREDENTIAL_ENV_KEYS) {
|
|
11
|
+
base[key] = `leaked-${key}`;
|
|
12
|
+
}
|
|
13
13
|
|
|
14
14
|
const env = buildShellEnv({}, base);
|
|
15
15
|
|
|
16
16
|
expect(env.PATH).toBe("/usr/bin");
|
|
17
17
|
expect(env.HOME).toBe("/home/runner");
|
|
18
|
-
for (const key of
|
|
19
|
-
expect(env[key]).toBeUndefined();
|
|
18
|
+
for (const key of RUNNER_CREDENTIAL_ENV_KEYS) {
|
|
19
|
+
expect(env[key], `runner credential '${key}' must not reach the shell env`).toBeUndefined();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("denies the full credential list, not just the original three (issue #385)", () => {
|
|
24
|
+
// Pins the five names #385 added. The parameterized test above would
|
|
25
|
+
// pass even if the list regressed to three; this one cannot.
|
|
26
|
+
for (const key of [
|
|
27
|
+
"ANTHROPIC_API_KEY",
|
|
28
|
+
"OPENAI_API_KEY",
|
|
29
|
+
"STIGMER_AUTH_TOKEN",
|
|
30
|
+
"ANTHROPIC_FOUNDRY_API_KEY",
|
|
31
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
32
|
+
]) {
|
|
33
|
+
expect(SHELL_ENV_DENYLIST, `'${key}' must be in the shell denylist`).toContain(key);
|
|
20
34
|
}
|
|
21
35
|
});
|
|
22
36
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that a
|
|
3
|
+
* plan-mode SUB-AGENT is read-only BY CONSTRUCTION (issue #255).
|
|
4
|
+
*
|
|
5
|
+
* The parent graph receives PLAN_MODE_PERMISSIONS in setup.ts, but deepagents'
|
|
6
|
+
* parent-permission inheritance covers only spec-style sub-agents — our
|
|
7
|
+
* pre-built CompiledSubAgents bypass `normalizeSubagentSpec` entirely, so
|
|
8
|
+
* `compileSubagents` must bake the rules into each sub-agent graph itself.
|
|
9
|
+
* These tests exercise that exact wiring with NO approval gate installed (the
|
|
10
|
+
* auto-approve-all shape): any denial must come from the permission rules
|
|
11
|
+
* alone, proving plan mode's "read-only by construction, not by gate"
|
|
12
|
+
* contract at the sub-agent level. A contrast case pins that without the
|
|
13
|
+
* rules (act mode) the same write flows — this change is a plan-mode-only
|
|
14
|
+
* delta.
|
|
15
|
+
*
|
|
16
|
+
* The rule under test is the PRODUCTION constant from
|
|
17
|
+
* shared/plan-mode-permissions.ts (the same one setup.ts applies to both
|
|
18
|
+
* graph sites), not a test copy, so a change to the plan-mode permission set
|
|
19
|
+
* re-proves sub-agent coverage automatically.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
23
|
+
import { mkdtemp, mkdir, rm, readFile, writeFile, access } from "node:fs/promises";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { HumanMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
|
|
27
|
+
|
|
28
|
+
import { PLAN_MODE_PERMISSIONS } from "../../../shared/plan-mode-permissions.js";
|
|
29
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
30
|
+
import { compileSubagents } from "../subagent-transformer.js";
|
|
31
|
+
import { ScriptedModel, type ScriptSelector } from "../__test-utils__/scripted-model.js";
|
|
32
|
+
|
|
33
|
+
const SEEDED_CONTENT = "PLAN_MODE_README_TOKEN: hello from the seeded file";
|
|
34
|
+
|
|
35
|
+
describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
36
|
+
let root: string;
|
|
37
|
+
let observer: CasCaptureObserver;
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
root = await mkdtemp(join(tmpdir(), "sa-plan-"));
|
|
41
|
+
await mkdir(join(root, "dist"), { recursive: true });
|
|
42
|
+
await writeFile(join(root, "notes.md"), SEEDED_CONTENT);
|
|
43
|
+
// Everything is gitignored from the observer's view — the strictest case:
|
|
44
|
+
// every write would be CAS-observed if it ever reached the backend.
|
|
45
|
+
observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await rm(root, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compile a worker through the production path with NO approval gate, so the
|
|
54
|
+
* permission rules are the only thing standing between the model and the
|
|
55
|
+
* filesystem. `permissions` present = the plan-mode wiring under test;
|
|
56
|
+
* absent = the act-mode contrast.
|
|
57
|
+
*/
|
|
58
|
+
async function compileWorker(script: ScriptSelector, planMode: boolean) {
|
|
59
|
+
const compiled = await compileSubagents(
|
|
60
|
+
[{ name: "worker", description: "test worker", systemPrompt: "do the work", tools: [] }],
|
|
61
|
+
{
|
|
62
|
+
approvalGate: null,
|
|
63
|
+
parentModelName: "test-model",
|
|
64
|
+
workspaceRootDir: root,
|
|
65
|
+
casObserver: observer,
|
|
66
|
+
modelFactory: async () => new ScriptedModel(script),
|
|
67
|
+
...(planMode ? { permissions: PLAN_MODE_PERMISSIONS } : {}),
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
expect(compiled).toHaveLength(1);
|
|
71
|
+
return compiled[0];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toolResultById(messages: BaseMessage[], toolCallId: string): string {
|
|
75
|
+
const match = messages.find(
|
|
76
|
+
(m): m is ToolMessage => m instanceof ToolMessage && m.tool_call_id === toolCallId,
|
|
77
|
+
);
|
|
78
|
+
expect(match, `expected a tool result for call '${toolCallId}'`).toBeDefined();
|
|
79
|
+
return typeof match!.content === "string" ? match!.content : JSON.stringify(match!.content);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
it("denies write_file and edit_file at the tool level, while read_file still works", async () => {
|
|
83
|
+
// One turn, three tool calls: both mutation tools must be denied by the
|
|
84
|
+
// permission rules; the read must pass through untouched (the deny rule
|
|
85
|
+
// covers the `write` operation class only).
|
|
86
|
+
// Absolute workspace paths — the canonical shape after enforcement
|
|
87
|
+
// canonicalization, and what the single-workspace prompt's prose (the
|
|
88
|
+
// absolute rootDir) steers models toward. The workspace-relative shape
|
|
89
|
+
// is pinned separately below (issue #429).
|
|
90
|
+
const script: ScriptSelector = () => ({
|
|
91
|
+
toolCalls: [
|
|
92
|
+
{ name: "write_file", args: { file_path: join(root, "dist/out.txt"), content: "new file" }, id: "c_write" },
|
|
93
|
+
{ name: "edit_file", args: { file_path: join(root, "notes.md"), old_string: "hello", new_string: "HACKED" }, id: "c_edit" },
|
|
94
|
+
{ name: "read_file", args: { file_path: join(root, "notes.md") }, id: "c_read" },
|
|
95
|
+
],
|
|
96
|
+
done: "worker done",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const worker = await compileWorker(script, true);
|
|
100
|
+
const result = (await worker.runnable.invoke(
|
|
101
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
102
|
+
{ configurable: { thread_id: "t1" }, recursionLimit: 50 },
|
|
103
|
+
)) as { messages: BaseMessage[] };
|
|
104
|
+
|
|
105
|
+
// Both mutations denied with deepagents' permission error, and nothing on disk:
|
|
106
|
+
// the new file was never created and the seeded file is byte-identical.
|
|
107
|
+
expect(toolResultById(result.messages, "c_write")).toMatch(/permission denied for write/i);
|
|
108
|
+
expect(toolResultById(result.messages, "c_edit")).toMatch(/permission denied for write/i);
|
|
109
|
+
await expect(access(join(root, "dist/out.txt"))).rejects.toThrow();
|
|
110
|
+
expect(await readFile(join(root, "notes.md"), "utf8")).toBe(SEEDED_CONTENT);
|
|
111
|
+
|
|
112
|
+
// The read flowed: its result carries the seeded content, not a denial.
|
|
113
|
+
const readResult = toolResultById(result.messages, "c_read");
|
|
114
|
+
expect(readResult).toContain("PLAN_MODE_README_TOKEN");
|
|
115
|
+
expect(readResult).not.toMatch(/permission denied/i);
|
|
116
|
+
|
|
117
|
+
// The denial fired INSIDE the tool handler, before any backend was reached:
|
|
118
|
+
// the CAS observer (which records pre-write bytes on the backend's write/edit
|
|
119
|
+
// seams) saw neither mutation.
|
|
120
|
+
expect(observer.before.size).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("act-mode contrast: without the permissions option the same write lands (zero-delta outside plan mode)", async () => {
|
|
124
|
+
const script: ScriptSelector = () => ({
|
|
125
|
+
toolCalls: [
|
|
126
|
+
{ name: "write_file", args: { file_path: join(root, "dist/out.txt"), content: "new file" }, id: "c_write" },
|
|
127
|
+
],
|
|
128
|
+
done: "worker done",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const worker = await compileWorker(script, false);
|
|
132
|
+
const result = (await worker.runnable.invoke(
|
|
133
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
134
|
+
{ configurable: { thread_id: "t2" }, recursionLimit: 50 },
|
|
135
|
+
)) as { messages: BaseMessage[] };
|
|
136
|
+
|
|
137
|
+
expect(toolResultById(result.messages, "c_write")).not.toMatch(/permission denied/i);
|
|
138
|
+
expect(await readFile(join(root, "dist/out.txt"), "utf8")).toBe("new file");
|
|
139
|
+
// And the backend WAS reached this time — the observer recorded a write.
|
|
140
|
+
// (Key shape is DD-19 territory, deliberately not pinned here.)
|
|
141
|
+
expect(observer.before.size).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("relative-path writes are refused by the RULES, and relative reads flow (issue #429)", async () => {
|
|
145
|
+
// The multi-workspace prompt mandates workspace-relative paths, and
|
|
146
|
+
// models routinely choose them in single-workspace sessions too. Before
|
|
147
|
+
// the #429 fix, deepagents' permission canonicalization refused the
|
|
148
|
+
// shape outright — every relative call, reads included, died with
|
|
149
|
+
// "path must be absolute" before any rule ran. compileSubagents now
|
|
150
|
+
// derives a path-normalization shim from the same `permissions` it
|
|
151
|
+
// bakes into the graph, so the relative shape reaches the rules in
|
|
152
|
+
// canonical form: writes get the honest plan-mode denial, reads just
|
|
153
|
+
// work. Read-only-by-construction (issue #255) holds throughout.
|
|
154
|
+
const script: ScriptSelector = () => ({
|
|
155
|
+
toolCalls: [
|
|
156
|
+
{ name: "write_file", args: { file_path: "dist/out.txt", content: "new file" }, id: "c_write_rel" },
|
|
157
|
+
{ name: "read_file", args: { file_path: "notes.md" }, id: "c_read_rel" },
|
|
158
|
+
],
|
|
159
|
+
done: "worker done",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const worker = await compileWorker(script, true);
|
|
163
|
+
const result = (await worker.runnable.invoke(
|
|
164
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
165
|
+
{ configurable: { thread_id: "t3" }, recursionLimit: 50 },
|
|
166
|
+
)) as { messages: BaseMessage[] };
|
|
167
|
+
|
|
168
|
+
const writeResult = toolResultById(result.messages, "c_write_rel");
|
|
169
|
+
expect(writeResult).toMatch(/permission denied for write/i);
|
|
170
|
+
expect(writeResult).not.toMatch(/path must be absolute/i);
|
|
171
|
+
await expect(access(join(root, "dist/out.txt"))).rejects.toThrow();
|
|
172
|
+
expect(observer.before.size).toBe(0);
|
|
173
|
+
|
|
174
|
+
const readResult = toolResultById(result.messages, "c_read_rel");
|
|
175
|
+
expect(readResult).toContain("PLAN_MODE_README_TOKEN");
|
|
176
|
+
expect(readResult).not.toMatch(/path must be absolute/i);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -7,10 +7,11 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
7
7
|
it("returns the correct middleware order without cost cap", () => {
|
|
8
8
|
const stack = buildSubAgentMiddleware();
|
|
9
9
|
|
|
10
|
-
expect(stack).toHaveLength(
|
|
10
|
+
expect(stack).toHaveLength(4);
|
|
11
11
|
expect(stack[0].name).toBe("LoopDetectionMiddleware");
|
|
12
12
|
expect(stack[1].name).toBe("ExecutionBudgetMiddleware");
|
|
13
13
|
expect(stack[2].name).toBe("ToolTruncationMiddleware");
|
|
14
|
+
expect(stack[3].name).toBe("ErrorHintsMiddleware");
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
it("includes cost cap view when parent cost cap is provided", () => {
|
|
@@ -24,8 +25,9 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
24
25
|
|
|
25
26
|
const stack = buildSubAgentMiddleware({ costCap: parentCostCap });
|
|
26
27
|
|
|
27
|
-
expect(stack).toHaveLength(
|
|
28
|
+
expect(stack).toHaveLength(5);
|
|
28
29
|
expect(stack[3].name).toBe("CostCapSubAgentView");
|
|
30
|
+
expect(stack[4].name).toBe("ErrorHintsMiddleware");
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
it("sub-agent cost cap view shares parent state", () => {
|
|
@@ -75,8 +77,8 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
75
77
|
approvalGate: { policies: new Map(), toolServerMap: new Map() },
|
|
76
78
|
});
|
|
77
79
|
|
|
78
|
-
// loop, budget, truncation, approval gate
|
|
79
|
-
expect(stack).toHaveLength(
|
|
80
|
+
// loop, budget, truncation, approval gate, error hints
|
|
81
|
+
expect(stack).toHaveLength(5);
|
|
80
82
|
expect(stack[3].name).toBe("ApprovalGateMiddleware");
|
|
81
83
|
expect(stack[3].wrapToolCall).toBeDefined();
|
|
82
84
|
});
|
|
@@ -84,7 +86,7 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
84
86
|
it("omits the approval gate when approvalGate is null (auto-approve-all parity)", () => {
|
|
85
87
|
const stack = buildSubAgentMiddleware({ approvalGate: null });
|
|
86
88
|
|
|
87
|
-
expect(stack).toHaveLength(
|
|
89
|
+
expect(stack).toHaveLength(4);
|
|
88
90
|
expect(stack.some((m) => m.name === "ApprovalGateMiddleware")).toBe(false);
|
|
89
91
|
});
|
|
90
92
|
|
|
@@ -102,10 +104,13 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
102
104
|
approvalGate: { policies: new Map(), toolServerMap: new Map() },
|
|
103
105
|
});
|
|
104
106
|
|
|
105
|
-
// loop, budget, truncation, approval gate, cost cap view
|
|
106
|
-
|
|
107
|
+
// loop, budget, truncation, approval gate, cost cap view, error hints.
|
|
108
|
+
// Hints AFTER the gate matches the parent nesting: the gate's HITL
|
|
109
|
+
// interrupt stays outside the hints' try/catch (issue #255).
|
|
110
|
+
expect(stack).toHaveLength(6);
|
|
107
111
|
expect(stack[3].name).toBe("ApprovalGateMiddleware");
|
|
108
112
|
expect(stack[4].name).toBe("CostCapSubAgentView");
|
|
113
|
+
expect(stack[5].name).toBe("ErrorHintsMiddleware");
|
|
109
114
|
});
|
|
110
115
|
|
|
111
116
|
// captureIgnored is the structural coupling that makes sub-agent gitignored
|