@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal PayloadCodec that encrypts payloads at rest in workflow
|
|
3
|
+
* history (stigmer-cloud#227).
|
|
4
|
+
*
|
|
5
|
+
* Why: the workflow engine runs inside the Temporal deterministic
|
|
6
|
+
* sandbox, so decrypted execution-context values cross the history
|
|
7
|
+
* boundary in many places — the hydrate activity result, the runtime
|
|
8
|
+
* env passed as an input to every per-task activity, and expression
|
|
9
|
+
* results recorded as local-activity markers. Encrypting at the payload
|
|
10
|
+
* codec layer closes the entire class with one mechanism instead of
|
|
11
|
+
* chasing each crossing.
|
|
12
|
+
*
|
|
13
|
+
* Envelope (cross-SDK contract — the Java decode-only codec in
|
|
14
|
+
* stigmer-cloud's temporal-starter must match it byte-for-byte, pinned
|
|
15
|
+
* by the conformance fixture in __tests__/fixtures/):
|
|
16
|
+
*
|
|
17
|
+
* metadata: encoding = "binary/encrypted"
|
|
18
|
+
* encryption-key-id = <key id that encrypted this payload>
|
|
19
|
+
* data: iv (12 bytes) ‖ AES-256-GCM(ciphertext ‖ tag (16 bytes))
|
|
20
|
+
*
|
|
21
|
+
* The plaintext is the serialized ORIGINAL Payload proto (metadata AND
|
|
22
|
+
* data), so decode restores the payload exactly — including its
|
|
23
|
+
* original encoding — with no side channel.
|
|
24
|
+
*
|
|
25
|
+
* Decode passes through payloads it did not encode. This is what keeps
|
|
26
|
+
* plaintext signals from the Java/Go orchestrators and pre-rollout
|
|
27
|
+
* in-flight histories working with zero migration. Everything else
|
|
28
|
+
* fails closed: unknown key id, missing key id, and ciphertext tampering
|
|
29
|
+
* all throw rather than surfacing bogus payloads.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
33
|
+
import type { Payload, PayloadCodec } from "@temporalio/common";
|
|
34
|
+
// Default-import + destructure, NOT `import { temporal } from …`:
|
|
35
|
+
// @temporalio/proto is CommonJS and its `temporal` export defeats Node's
|
|
36
|
+
// cjs-module-lexer named-export detection, so the named form loads under
|
|
37
|
+
// tsx/vitest (their interop is looser) but crashes plain `node dist/main.js`
|
|
38
|
+
// at startup with "Named export 'temporal' not found" (stigmer/stigmer#399
|
|
39
|
+
// boot regression). Pinned by scripts/verify-dist-boot.mjs in CI.
|
|
40
|
+
import proto from "@temporalio/proto";
|
|
41
|
+
import type { EncryptionKey, PayloadEncryptionConfig } from "./config.js";
|
|
42
|
+
|
|
43
|
+
const { temporal } = proto;
|
|
44
|
+
|
|
45
|
+
const ENCODING_METADATA_KEY = "encoding";
|
|
46
|
+
const ENCRYPTED_ENCODING_VALUE = "binary/encrypted";
|
|
47
|
+
const KEY_ID_METADATA_KEY = "encryption-key-id";
|
|
48
|
+
|
|
49
|
+
/** AES-GCM parameters shared with the Java implementation. */
|
|
50
|
+
const IV_BYTES = 12;
|
|
51
|
+
const AUTH_TAG_BYTES = 16;
|
|
52
|
+
|
|
53
|
+
export class EncryptionPayloadCodec implements PayloadCodec {
|
|
54
|
+
private readonly decryptKeysById: Map<string, Buffer>;
|
|
55
|
+
|
|
56
|
+
constructor(private readonly config: PayloadEncryptionConfig) {
|
|
57
|
+
this.decryptKeysById = new Map([[config.primary.keyId, config.primary.key]]);
|
|
58
|
+
if (config.secondary) {
|
|
59
|
+
this.decryptKeysById.set(config.secondary.keyId, config.secondary.key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async encode(payloads: Payload[]): Promise<Payload[]> {
|
|
64
|
+
return payloads.map((p) => this.encodePayload(p));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async decode(payloads: Payload[]): Promise<Payload[]> {
|
|
68
|
+
return payloads.map((p) => this.decodePayload(p));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private encodePayload(payload: Payload): Payload {
|
|
72
|
+
// Data-less payloads (binary/null from void results) stay as-is:
|
|
73
|
+
// there is nothing to protect, and the cross-language parents that
|
|
74
|
+
// await our workflows as void must be able to read them without a key.
|
|
75
|
+
if (!payload.data || payload.data.length === 0) {
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const plaintext = temporal.api.common.v1.Payload.encode(payload).finish();
|
|
80
|
+
return {
|
|
81
|
+
metadata: {
|
|
82
|
+
[ENCODING_METADATA_KEY]: Buffer.from(ENCRYPTED_ENCODING_VALUE),
|
|
83
|
+
[KEY_ID_METADATA_KEY]: Buffer.from(this.config.primary.keyId),
|
|
84
|
+
},
|
|
85
|
+
data: encrypt(plaintext, this.config.primary),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private decodePayload(payload: Payload): Payload {
|
|
90
|
+
if (!isEncryptedPayload(payload)) {
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const keyIdBytes = payload.metadata?.[KEY_ID_METADATA_KEY];
|
|
95
|
+
if (!keyIdBytes) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Encrypted payload is missing its encryption-key-id metadata — refusing to decode",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const keyId = Buffer.from(keyIdBytes).toString("utf-8");
|
|
101
|
+
const key = this.decryptKeysById.get(keyId);
|
|
102
|
+
if (!key) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Encrypted payload uses unknown key id '${keyId}' — configure it as the ` +
|
|
105
|
+
`primary or secondary payload encryption key (rotation window?)`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const plaintext = decrypt(payload.data!, key, keyId);
|
|
110
|
+
return temporal.api.common.v1.Payload.decode(plaintext);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isEncryptedPayload(payload: Payload): boolean {
|
|
115
|
+
const encoding = payload.metadata?.[ENCODING_METADATA_KEY];
|
|
116
|
+
if (!encoding) return false;
|
|
117
|
+
return Buffer.from(encoding).toString("utf-8") === ENCRYPTED_ENCODING_VALUE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function encrypt(plaintext: Uint8Array, key: EncryptionKey): Buffer {
|
|
121
|
+
const iv = randomBytes(IV_BYTES);
|
|
122
|
+
const cipher = createCipheriv("aes-256-gcm", key.key, iv);
|
|
123
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
124
|
+
// Layout iv ‖ ciphertext ‖ tag matches Java's AES/GCM/NoPadding, whose
|
|
125
|
+
// doFinal() output is ciphertext ‖ tag.
|
|
126
|
+
return Buffer.concat([iv, ciphertext, cipher.getAuthTag()]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function decrypt(data: Uint8Array, key: Buffer, keyId: string): Buffer {
|
|
130
|
+
if (data.length < IV_BYTES + AUTH_TAG_BYTES) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Encrypted payload under key id '${keyId}' is truncated (${data.length} bytes)`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const buf = Buffer.from(data);
|
|
136
|
+
const iv = buf.subarray(0, IV_BYTES);
|
|
137
|
+
const ciphertext = buf.subarray(IV_BYTES, buf.length - AUTH_TAG_BYTES);
|
|
138
|
+
const tag = buf.subarray(buf.length - AUTH_TAG_BYTES);
|
|
139
|
+
|
|
140
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
141
|
+
decipher.setAuthTag(tag);
|
|
142
|
+
try {
|
|
143
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
144
|
+
} catch {
|
|
145
|
+
// GCM auth failure: tampered ciphertext or a key that does not match
|
|
146
|
+
// its advertised id. Never surface partially decrypted bytes.
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Failed to decrypt payload under key id '${keyId}' — ciphertext is ` +
|
|
149
|
+
`corrupt or the configured key does not match`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ToolMessage } from "@langchain/core/messages";
|
|
3
|
+
import {
|
|
4
|
+
createPathNormalizationMiddleware,
|
|
5
|
+
normalizeWorkspacePathArg,
|
|
6
|
+
} from "../path-normalization.js";
|
|
7
|
+
import type { ToolCallRequest } from "../types.js";
|
|
8
|
+
|
|
9
|
+
const ROOT = "/workspace/session-1/repo";
|
|
10
|
+
|
|
11
|
+
function makeRequest(
|
|
12
|
+
name: string,
|
|
13
|
+
args: Record<string, unknown>,
|
|
14
|
+
): ToolCallRequest {
|
|
15
|
+
return {
|
|
16
|
+
toolCall: { id: "tc_1", name, args },
|
|
17
|
+
tool: undefined,
|
|
18
|
+
state: { messages: [] },
|
|
19
|
+
runtime: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Run the middleware and return the args the inner handler observed. */
|
|
24
|
+
async function argsSeenByHandler(
|
|
25
|
+
name: string,
|
|
26
|
+
args: Record<string, unknown>,
|
|
27
|
+
): Promise<Record<string, unknown>> {
|
|
28
|
+
const mw = createPathNormalizationMiddleware({ rootDir: ROOT });
|
|
29
|
+
const handler = vi.fn(async (req: ToolCallRequest) => {
|
|
30
|
+
return new ToolMessage({ content: "ok", tool_call_id: req.toolCall.id, name });
|
|
31
|
+
});
|
|
32
|
+
await mw.wrapToolCall!(makeRequest(name, args), handler);
|
|
33
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
34
|
+
return handler.mock.calls[0][0].toolCall.args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("normalizeWorkspacePathArg", () => {
|
|
38
|
+
it("maps a workspace-relative path to its rootDir-joined absolute form", () => {
|
|
39
|
+
expect(normalizeWorkspacePathArg("src/main.py", ROOT)).toBe(`${ROOT}/src/main.py`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("strips a leading ./ before joining", () => {
|
|
43
|
+
expect(normalizeWorkspacePathArg("./notes.md", ROOT)).toBe(`${ROOT}/notes.md`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles multi-workspace entry-relative paths", () => {
|
|
47
|
+
expect(normalizeWorkspacePathArg("entry-1/src/main.py", ROOT)).toBe(
|
|
48
|
+
`${ROOT}/entry-1/src/main.py`,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("resolves interior .. segments that stay inside the root", () => {
|
|
53
|
+
expect(normalizeWorkspacePathArg("src/../notes.md", ROOT)).toBe(`${ROOT}/notes.md`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("leaves absolute paths untouched", () => {
|
|
57
|
+
expect(normalizeWorkspacePathArg("/etc/hosts", ROOT)).toBeUndefined();
|
|
58
|
+
expect(normalizeWorkspacePathArg(`${ROOT}/src/main.py`, ROOT)).toBeUndefined();
|
|
59
|
+
expect(normalizeWorkspacePathArg("/", ROOT)).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("refuses to rewrite relatives that escape the workspace root", () => {
|
|
63
|
+
// A naive join would resolve `..` away and hand upstream validation a
|
|
64
|
+
// clean out-of-root absolute path — an out-of-root read that today's
|
|
65
|
+
// validation refuses. The guard keeps the refusal.
|
|
66
|
+
expect(normalizeWorkspacePathArg("../sibling/secret.txt", ROOT)).toBeUndefined();
|
|
67
|
+
expect(normalizeWorkspacePathArg("src/../../escape.txt", ROOT)).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("leaves ~-carrying paths untouched (upstream refuses them either way)", () => {
|
|
71
|
+
expect(normalizeWorkspacePathArg("~/notes.md", ROOT)).toBeUndefined();
|
|
72
|
+
expect(normalizeWorkspacePathArg("docs/~/x.md", ROOT)).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("leaves the empty string untouched", () => {
|
|
76
|
+
expect(normalizeWorkspacePathArg("", ROOT)).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("createPathNormalizationMiddleware", () => {
|
|
81
|
+
it("rewrites file_path on the file tools", async () => {
|
|
82
|
+
for (const tool of ["read_file", "write_file", "edit_file"]) {
|
|
83
|
+
const args = await argsSeenByHandler(tool, {
|
|
84
|
+
file_path: "src/main.py",
|
|
85
|
+
content: "x",
|
|
86
|
+
});
|
|
87
|
+
expect(args.file_path).toBe(`${ROOT}/src/main.py`);
|
|
88
|
+
// Sibling args ride along untouched.
|
|
89
|
+
expect(args.content).toBe("x");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rewrites the base path on ls/glob/grep", async () => {
|
|
94
|
+
for (const tool of ["ls", "glob", "grep"]) {
|
|
95
|
+
const args = await argsSeenByHandler(tool, { path: "src", pattern: "**/*.py" });
|
|
96
|
+
expect(args.path).toBe(`${ROOT}/src`);
|
|
97
|
+
// The pattern is never a path — byte-untouched.
|
|
98
|
+
expect(args.pattern).toBe("**/*.py");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("passes absolute paths through byte-untouched", async () => {
|
|
103
|
+
const args = await argsSeenByHandler("read_file", { file_path: "/etc/hosts" });
|
|
104
|
+
expect(args.file_path).toBe("/etc/hosts");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("leaves an omitted base path (backend default) alone", async () => {
|
|
108
|
+
const args = await argsSeenByHandler("ls", {});
|
|
109
|
+
expect(args.path).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("never touches tools outside the built-in filesystem set", async () => {
|
|
113
|
+
// An MCP tool could plausibly carry a same-named arg with different
|
|
114
|
+
// semantics; only the six built-in names are rewritten.
|
|
115
|
+
const args = await argsSeenByHandler("vendor_upload", { file_path: "src/main.py" });
|
|
116
|
+
expect(args.file_path).toBe("src/main.py");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("leaves non-string path args for the tool's own input validation", async () => {
|
|
120
|
+
const args = await argsSeenByHandler("read_file", { file_path: 42 });
|
|
121
|
+
expect(args.file_path).toBe(42);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("preserves the original request identity fields on rewrite", async () => {
|
|
125
|
+
const mw = createPathNormalizationMiddleware({ rootDir: ROOT });
|
|
126
|
+
const handler = vi.fn(async (req: ToolCallRequest) => {
|
|
127
|
+
return new ToolMessage({ content: "ok", tool_call_id: req.toolCall.id, name: req.toolCall.name });
|
|
128
|
+
});
|
|
129
|
+
const original = makeRequest("read_file", { file_path: "notes.md" });
|
|
130
|
+
await mw.wrapToolCall!(original, handler);
|
|
131
|
+
|
|
132
|
+
const seen = handler.mock.calls[0][0];
|
|
133
|
+
expect(seen.toolCall.id).toBe("tc_1");
|
|
134
|
+
expect(seen.toolCall.name).toBe("read_file");
|
|
135
|
+
expect(seen.state).toBe(original.state);
|
|
136
|
+
expect(seen.runtime).toBe(original.runtime);
|
|
137
|
+
// The original request is never mutated in place.
|
|
138
|
+
expect(original.toolCall.args.file_path).toBe("notes.md");
|
|
139
|
+
});
|
|
140
|
+
});
|
package/src/middleware/index.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Assembles the ordered middleware array from a MiddlewareStackConfig.
|
|
5
5
|
* The composition order matches the Python create_deep_agent injection:
|
|
6
6
|
*
|
|
7
|
+
* 0. Path normalization (conditional: only on permission-rule-bearing
|
|
8
|
+
* graphs — FIRST, so every downstream middleware observes canonical
|
|
9
|
+
* workspace-absolute paths)
|
|
7
10
|
* 1. Loop detection (always)
|
|
8
11
|
* 2. Execution budget (always)
|
|
9
12
|
* 3. Tool truncation (always)
|
|
@@ -15,6 +18,7 @@
|
|
|
15
18
|
|
|
16
19
|
import type { StigmerMiddleware, MiddlewareStackConfig } from "./types.js";
|
|
17
20
|
import type { GracefulStopMiddleware } from "./graceful-stop.js";
|
|
21
|
+
import { createPathNormalizationMiddleware } from "./path-normalization.js";
|
|
18
22
|
import { createLoopDetectionMiddleware } from "./loop-detection.js";
|
|
19
23
|
import { createExecutionBudgetMiddleware } from "./execution-budget.js";
|
|
20
24
|
import { createToolTruncationMiddleware } from "./tool-truncation.js";
|
|
@@ -39,6 +43,10 @@ export function buildMiddlewareStack(
|
|
|
39
43
|
): MiddlewareStackResult {
|
|
40
44
|
const stack: StigmerMiddleware[] = [];
|
|
41
45
|
|
|
46
|
+
if (config.pathNormalization) {
|
|
47
|
+
stack.push(createPathNormalizationMiddleware(config.pathNormalization));
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
stack.push(createLoopDetectionMiddleware(config.loopDetection));
|
|
43
51
|
|
|
44
52
|
stack.push(createExecutionBudgetMiddleware(config.executionBudget));
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-relative path normalization for permission-rule-bearing graphs
|
|
3
|
+
* (issue #429).
|
|
4
|
+
*
|
|
5
|
+
* deepagents' permission enforcement canonicalizes tool-call paths BEFORE any
|
|
6
|
+
* rule or backend runs, and its validation refuses non-absolute paths — on
|
|
7
|
+
* EVERY filesystem tool call once a graph carries any permission rules, reads
|
|
8
|
+
* included. Plan mode is the only rule-bearing production configuration
|
|
9
|
+
* (shared/plan-mode-permissions.ts), so there a workspace-relative path —
|
|
10
|
+
* the shape the multi-workspace prompt explicitly mandates, and one models
|
|
11
|
+
* routinely choose in single-workspace sessions — fails with
|
|
12
|
+
* `path must be absolute` instead of just working, burning tool rounds until
|
|
13
|
+
* the model adapts by switching to absolute paths.
|
|
14
|
+
*
|
|
15
|
+
* This middleware repairs the contract at our own seam: it rewrites
|
|
16
|
+
* workspace-relative paths on the deepagents built-in filesystem tools to
|
|
17
|
+
* workspace-absolute before the tool (and therefore enforcement) runs. The
|
|
18
|
+
* write-deny rule then fires as designed and reads succeed on the first try.
|
|
19
|
+
* Rule-less (act-mode) graphs never install it — the legacy backend already
|
|
20
|
+
* resolves relative paths under the workspace root, so act mode keeps a
|
|
21
|
+
* byte-zero delta.
|
|
22
|
+
*
|
|
23
|
+
* Invariant — nothing becomes newly reachable: only paths whose resolution
|
|
24
|
+
* stays INSIDE the workspace root are rewritten. Escaping relatives (`../x`)
|
|
25
|
+
* and `~`-carrying paths are left raw so today's validation refusal keeps
|
|
26
|
+
* speaking (a naive join would resolve `..` away and smuggle an out-of-root
|
|
27
|
+
* read past validation), and absolute paths pass through byte-untouched.
|
|
28
|
+
* The middleware converts false errors into correct behavior — never a
|
|
29
|
+
* refusal into an allowance the rules didn't decide.
|
|
30
|
+
*
|
|
31
|
+
* Tool matching is by bare built-in name, the house doctrine (an MCP server
|
|
32
|
+
* is not expected to shadow a built-in name — see shared/tool-kind.ts); the
|
|
33
|
+
* rewrite touches only the tool's path-bearing argument, never glob/grep
|
|
34
|
+
* patterns. Install FIRST in the stack so every downstream middleware
|
|
35
|
+
* (approval gate, error hints, otel spans) observes canonical paths.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { isAbsolute, relative } from "node:path";
|
|
39
|
+
import { resolveWorkspacePath } from "../shared/file-change.js";
|
|
40
|
+
import type { StigmerMiddleware } from "./types.js";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The path-bearing argument of each deepagents built-in filesystem tool.
|
|
44
|
+
* `ls`/`glob`/`grep` take a base directory `path` (defaulting to "/" when
|
|
45
|
+
* omitted — absolute, so untouched here); the file tools take `file_path`.
|
|
46
|
+
* Confirmed against the installed deepagents' tool definitions.
|
|
47
|
+
*/
|
|
48
|
+
const PATH_ARG_BY_TOOL: ReadonlyMap<string, string> = new Map([
|
|
49
|
+
["ls", "path"],
|
|
50
|
+
["glob", "path"],
|
|
51
|
+
["grep", "path"],
|
|
52
|
+
["read_file", "file_path"],
|
|
53
|
+
["write_file", "file_path"],
|
|
54
|
+
["edit_file", "file_path"],
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export interface PathNormalizationConfig {
|
|
58
|
+
/** The workspace root the graph's filesystem backend resolves against. */
|
|
59
|
+
readonly rootDir: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rewrite `raw` to its workspace-absolute form, or return undefined when the
|
|
64
|
+
* value must be left untouched (absolute already, `~`-carrying, or escaping
|
|
65
|
+
* the workspace root). Exported for direct unit testing of the mapping table.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeWorkspacePathArg(
|
|
68
|
+
raw: string,
|
|
69
|
+
rootDir: string,
|
|
70
|
+
): string | undefined {
|
|
71
|
+
if (raw.length === 0 || isAbsolute(raw)) return undefined;
|
|
72
|
+
// Upstream validation refuses `~` segments even in absolute paths, so a
|
|
73
|
+
// rewrite could not make such a call succeed — leave the raw shape (and
|
|
74
|
+
// therefore the honest refusal) intact.
|
|
75
|
+
if (raw.split("/").includes("~")) return undefined;
|
|
76
|
+
|
|
77
|
+
const { absolutePath } = resolveWorkspacePath(raw, rootDir, false);
|
|
78
|
+
|
|
79
|
+
// No-new-reachability guard: `join` inside the resolver normalizes `..`
|
|
80
|
+
// segments away, so an escaping relative would otherwise pass upstream
|
|
81
|
+
// validation as a clean out-of-root absolute path. Today that shape is
|
|
82
|
+
// refused; keep it that way.
|
|
83
|
+
const rel = relative(rootDir, absolutePath);
|
|
84
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return undefined;
|
|
85
|
+
|
|
86
|
+
return absolutePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create middleware that normalizes workspace-relative paths on the built-in
|
|
91
|
+
* filesystem tools before permission enforcement sees them.
|
|
92
|
+
*
|
|
93
|
+
* Installed only on graphs that carry filesystem permission rules — derive
|
|
94
|
+
* the install condition from the same expression that supplies the rules
|
|
95
|
+
* (setup.ts / compileSubagents) so the rules and their normalization shim
|
|
96
|
+
* cannot drift apart.
|
|
97
|
+
*/
|
|
98
|
+
export function createPathNormalizationMiddleware(
|
|
99
|
+
config: PathNormalizationConfig,
|
|
100
|
+
): StigmerMiddleware {
|
|
101
|
+
const { rootDir } = config;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
name: "PathNormalizationMiddleware",
|
|
105
|
+
|
|
106
|
+
wrapToolCall(request, handler) {
|
|
107
|
+
const argKey = PATH_ARG_BY_TOOL.get(request.toolCall.name);
|
|
108
|
+
if (!argKey) return handler(request);
|
|
109
|
+
|
|
110
|
+
const raw = request.toolCall.args[argKey];
|
|
111
|
+
if (typeof raw !== "string") return handler(request);
|
|
112
|
+
|
|
113
|
+
const normalized = normalizeWorkspacePathArg(raw, rootDir);
|
|
114
|
+
if (normalized === undefined) return handler(request);
|
|
115
|
+
|
|
116
|
+
return handler({
|
|
117
|
+
...request,
|
|
118
|
+
toolCall: {
|
|
119
|
+
...request.toolCall,
|
|
120
|
+
args: { ...request.toolCall.args, [argKey]: normalized },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/middleware/types.ts
CHANGED
|
@@ -100,6 +100,11 @@ export interface OtelSpansConfig {
|
|
|
100
100
|
import type { ApprovalGateConfig } from "./approval-gate.js";
|
|
101
101
|
export type { ApprovalGateConfig };
|
|
102
102
|
|
|
103
|
+
// Same idiom as ApprovalGateConfig above: the normalization middleware
|
|
104
|
+
// consumes it, this module re-exports it for stack-config assembly.
|
|
105
|
+
import type { PathNormalizationConfig } from "./path-normalization.js";
|
|
106
|
+
export type { PathNormalizationConfig };
|
|
107
|
+
|
|
103
108
|
/**
|
|
104
109
|
* Top-level configuration for buildMiddlewareStack().
|
|
105
110
|
* All sections are optional — the factory applies sensible defaults.
|
|
@@ -111,4 +116,10 @@ export interface MiddlewareStackConfig {
|
|
|
111
116
|
readonly costCap?: CostCapConfig | null;
|
|
112
117
|
readonly otelSpans?: Partial<OtelSpansConfig>;
|
|
113
118
|
readonly approvalGate?: ApprovalGateConfig | null;
|
|
119
|
+
/**
|
|
120
|
+
* Workspace-relative path normalization (issue #429). Set iff the graph
|
|
121
|
+
* carries filesystem permission rules — derive from the same expression
|
|
122
|
+
* that supplies the rules so the two cannot drift apart.
|
|
123
|
+
*/
|
|
124
|
+
readonly pathNormalization?: PathNormalizationConfig | null;
|
|
114
125
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single assembly point for the worker's payload codec chain, shared by
|
|
3
|
+
* the standalone runner (runner.ts) and the runner manager
|
|
4
|
+
* (runner-manager.ts) so the two entrypoints cannot drift.
|
|
5
|
+
*
|
|
6
|
+
* Codec order is a correctness property, not a style choice. Temporal
|
|
7
|
+
* applies codecs in array order on encode and in reverse on decode, so
|
|
8
|
+
* with [encryption, claimcheck]:
|
|
9
|
+
*
|
|
10
|
+
* encode: encrypt → claim-check (payloads the claim-check codec
|
|
11
|
+
* relocates to object storage are already ciphertext)
|
|
12
|
+
* decode: claim-check → decrypt (restore the blob, then decrypt)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PayloadCodec } from "@temporalio/common";
|
|
16
|
+
import type { Config } from "./config.js";
|
|
17
|
+
import type { BootstrapKeyMaterial } from "./encryption/config.js";
|
|
18
|
+
|
|
19
|
+
export async function createPayloadCodecs(
|
|
20
|
+
config: Config,
|
|
21
|
+
bootstrapKeys?: BootstrapKeyMaterial,
|
|
22
|
+
): Promise<PayloadCodec[] | undefined> {
|
|
23
|
+
const codecs: PayloadCodec[] = [];
|
|
24
|
+
|
|
25
|
+
const { loadPayloadEncryptionConfig, EncryptionPayloadCodec } = await import(
|
|
26
|
+
"./encryption/index.js"
|
|
27
|
+
);
|
|
28
|
+
// Env-configured keys win outright; server-managed (bootstrap) keys apply
|
|
29
|
+
// only when the env is silent — see encryption/config.ts for the rationale.
|
|
30
|
+
const encryptionConfig = loadPayloadEncryptionConfig(bootstrapKeys);
|
|
31
|
+
if (encryptionConfig) {
|
|
32
|
+
codecs.push(new EncryptionPayloadCodec(encryptionConfig));
|
|
33
|
+
const source = process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY ? "env" : "bootstrap";
|
|
34
|
+
console.log(
|
|
35
|
+
`[runner] Payload encryption enabled (source=${source}, ` +
|
|
36
|
+
`key_id=${encryptionConfig.primary.keyId}` +
|
|
37
|
+
(encryptionConfig.secondary
|
|
38
|
+
? `, secondary_key_id=${encryptionConfig.secondary.keyId})`
|
|
39
|
+
: ")"),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { loadClaimcheckConfig, ClaimcheckPayloadCodec } = await import(
|
|
44
|
+
"./claimcheck/index.js"
|
|
45
|
+
);
|
|
46
|
+
const claimcheckConfig = loadClaimcheckConfig();
|
|
47
|
+
if (claimcheckConfig.enabled) {
|
|
48
|
+
const { loadArtifactStorageConfig, createArtifactStorage } = await import(
|
|
49
|
+
"./shared/artifact-storage.js"
|
|
50
|
+
);
|
|
51
|
+
const storageConfig = loadArtifactStorageConfig(config);
|
|
52
|
+
const storage = createArtifactStorage(storageConfig);
|
|
53
|
+
codecs.push(new ClaimcheckPayloadCodec(storage, claimcheckConfig));
|
|
54
|
+
console.log(
|
|
55
|
+
`[runner] Claimcheck enabled (threshold=${claimcheckConfig.thresholdBytes}B, ` +
|
|
56
|
+
`compression=${claimcheckConfig.compressionEnabled}, ` +
|
|
57
|
+
`storage=${storageConfig.type})`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return codecs.length > 0 ? codecs : undefined;
|
|
62
|
+
}
|
package/src/preflight.ts
CHANGED
|
@@ -21,8 +21,15 @@
|
|
|
21
21
|
* The gate is unconditional (not checkpointer-type-dependent): the eager
|
|
22
22
|
* import chain needs `node:sqlite` to LOAD regardless of configuration, so
|
|
23
23
|
* the gate mirrors the actual load-time requirement.
|
|
24
|
+
*
|
|
25
|
+
* This module also hosts {@link assertLlmBackendsPreflight}, the
|
|
26
|
+
* deployment-config gate the runner factories run at construction. The
|
|
27
|
+
* llm-backend import below is a pure, dependency-free module, so it does
|
|
28
|
+
* not compromise the load-order guarantees above.
|
|
24
29
|
*/
|
|
25
30
|
|
|
31
|
+
import { preflightLlmBackends } from "./shared/llm-backend.js";
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* True when this Node binary provides the built-in `node:sqlite`.
|
|
28
35
|
*
|
|
@@ -57,3 +64,41 @@ export function preflightNodeRuntime(
|
|
|
57
64
|
`Use Node >= 22.13 (22.x line) or >= 23.4 (23.x and later).`
|
|
58
65
|
);
|
|
59
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate LLM backend configuration (STIGMER_ANTHROPIC_BACKEND /
|
|
70
|
+
* STIGMER_OPENAI_BACKEND) at runner construction, throwing an actionable
|
|
71
|
+
* operator message on fatal misconfiguration and logging precedence
|
|
72
|
+
* warnings.
|
|
73
|
+
*
|
|
74
|
+
* Called from the runner factories rather than main.ts because
|
|
75
|
+
* @stigmer/runner is a public library: in-process embedders never execute
|
|
76
|
+
* main.ts, while all of main.ts's process modes construct through the
|
|
77
|
+
* factories. A library must never process.exit, so this throws — manager
|
|
78
|
+
* mode's factory catch converts it to a fatal IPC error for the desktop
|
|
79
|
+
* host, static/pool modes exit through main()'s fatal handler, and
|
|
80
|
+
* embedders get a rejected promise.
|
|
81
|
+
*
|
|
82
|
+
* Backend misconfiguration is deployment-static; failing here means a
|
|
83
|
+
* broken deployment refuses to accept work instead of failing every
|
|
84
|
+
* execution mid-flight (the createClient factories construct lazily on
|
|
85
|
+
* first request, far too late for a good operator experience).
|
|
86
|
+
*
|
|
87
|
+
* @param proxyEndpoint The effective proxy endpoint. Factories receive it
|
|
88
|
+
* via options (env-derived in main.ts flows, caller-supplied for
|
|
89
|
+
* embedders), so it is threaded explicitly instead of re-read from env —
|
|
90
|
+
* an options-supplied proxy must downgrade backend vars to warnings the
|
|
91
|
+
* same way STIGMER_PROXY_ENDPOINT does.
|
|
92
|
+
*/
|
|
93
|
+
export function assertLlmBackendsPreflight(proxyEndpoint?: string | null): void {
|
|
94
|
+
const env = proxyEndpoint
|
|
95
|
+
? { ...process.env, STIGMER_PROXY_ENDPOINT: proxyEndpoint }
|
|
96
|
+
: process.env;
|
|
97
|
+
const { error, warnings } = preflightLlmBackends(env);
|
|
98
|
+
for (const warning of warnings) {
|
|
99
|
+
console.warn(`[runner] ${warning}`);
|
|
100
|
+
}
|
|
101
|
+
if (error !== null) {
|
|
102
|
+
throw new Error(error);
|
|
103
|
+
}
|
|
104
|
+
}
|