@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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the shape-aware run.wait() error extraction (oss#299).
|
|
3
|
+
*
|
|
4
|
+
* A bare String() on a structured error value yields "[object Object]",
|
|
5
|
+
* which end users saw verbatim AND which shadowed every lower-priority
|
|
6
|
+
* classifier source (stream, rejection, conversation introspection) because
|
|
7
|
+
* classification stops at the first non-empty source. These tests pin:
|
|
8
|
+
*
|
|
9
|
+
* - the extractRunErrorSources shape matrix (strings, Errors, field objects,
|
|
10
|
+
* hopeless values)
|
|
11
|
+
* - first-USABLE-candidate chain order (a hopeless object no longer hides a
|
|
12
|
+
* usable string one field later; an empty string no longer short-circuits)
|
|
13
|
+
* - end-to-end: structured errors classify and re-enable fresh-agent retry;
|
|
14
|
+
* hopeless extraction yields to the introspection sources
|
|
15
|
+
* - the "[object Object]" defense-in-depth guard in classifyFromSources
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
19
|
+
import {
|
|
20
|
+
extractRunErrorSources,
|
|
21
|
+
synthesizeError,
|
|
22
|
+
shouldRetryWithFreshAgent,
|
|
23
|
+
} from "../error-classifier.js";
|
|
24
|
+
|
|
25
|
+
const FALLBACK = { model: "default", mode: "cloud", agentId: "agent-1" };
|
|
26
|
+
|
|
27
|
+
function base() {
|
|
28
|
+
return {
|
|
29
|
+
sdkResultFields: undefined,
|
|
30
|
+
streamErrorMessage: undefined,
|
|
31
|
+
capturedRejection: undefined,
|
|
32
|
+
isResumedHandle: false,
|
|
33
|
+
fallbackContext: FALLBACK,
|
|
34
|
+
} as const;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A run.wait()-shaped error result carrying the given error-detail fields. */
|
|
38
|
+
function errorResult(fields: Record<string, unknown>): unknown {
|
|
39
|
+
return { id: "run-1", status: "error", ...fields };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const NOTHING = { sdkError: undefined, sdkResultFields: undefined };
|
|
43
|
+
|
|
44
|
+
describe("extractRunErrorSources shape matrix", () => {
|
|
45
|
+
it("routes a plain string to sdkResultFields", () => {
|
|
46
|
+
expect(extractRunErrorSources(errorResult({ result: "rate limit exceeded" })))
|
|
47
|
+
.toEqual({ sdkError: undefined, sdkResultFields: "rate limit exceeded" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("lifts an Error instance into the structured channel", () => {
|
|
51
|
+
expect(extractRunErrorSources(errorResult({ result: new Error("connection lost") })))
|
|
52
|
+
.toEqual({ sdkError: { message: "connection lost" }, sdkResultFields: undefined });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("lifts an Error carrying a code (Node/SDK error shape)", () => {
|
|
56
|
+
const err = Object.assign(new Error("stream torn down"), { code: "unavailable" });
|
|
57
|
+
expect(extractRunErrorSources(errorResult({ result: err })))
|
|
58
|
+
.toEqual({ sdkError: { code: "unavailable", message: "stream torn down" }, sdkResultFields: undefined });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("lifts { code, status, message } from a plain object", () => {
|
|
62
|
+
expect(extractRunErrorSources(errorResult({ error: { code: "unauthenticated", status: 401, message: "bad token" } })))
|
|
63
|
+
.toEqual({
|
|
64
|
+
sdkError: { code: "unauthenticated", status: 401, message: "bad token" },
|
|
65
|
+
sdkResultFields: undefined,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("lifts a message-only object", () => {
|
|
70
|
+
expect(extractRunErrorSources(errorResult({ error: { message: "boom" } })))
|
|
71
|
+
.toEqual({ sdkError: { message: "boom" }, sdkResultFields: undefined });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("lifts a code-only object", () => {
|
|
75
|
+
expect(extractRunErrorSources(errorResult({ error: { code: "resource_exhausted" } })))
|
|
76
|
+
.toEqual({ sdkError: { code: "resource_exhausted" }, sdkResultFields: undefined });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("yields nothing for an object with no recognizable fields (no JSON.stringify junk)", () => {
|
|
80
|
+
expect(extractRunErrorSources(errorResult({ result: { weird: "shape" } }))).toEqual(NOTHING);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("yields nothing for a circular object", () => {
|
|
84
|
+
const circular: Record<string, unknown> = {};
|
|
85
|
+
circular.self = circular;
|
|
86
|
+
expect(extractRunErrorSources(errorResult({ result: circular }))).toEqual(NOTHING);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("refuses the '[object Object]' junk string itself", () => {
|
|
90
|
+
expect(extractRunErrorSources(errorResult({ result: "[object Object]" }))).toEqual(NOTHING);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("stringifies non-string primitives losslessly", () => {
|
|
94
|
+
expect(extractRunErrorSources(errorResult({ result: 503 })))
|
|
95
|
+
.toEqual({ sdkError: undefined, sdkResultFields: "503" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("yields nothing when no candidate field is present", () => {
|
|
99
|
+
expect(extractRunErrorSources(errorResult({}))).toEqual(NOTHING);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("yields nothing for non-object results", () => {
|
|
103
|
+
expect(extractRunErrorSources(undefined)).toEqual(NOTHING);
|
|
104
|
+
expect(extractRunErrorSources(null)).toEqual(NOTHING);
|
|
105
|
+
expect(extractRunErrorSources("not-a-result-object")).toEqual(NOTHING);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("extractRunErrorSources chain order (first USABLE candidate wins)", () => {
|
|
110
|
+
it("a hopeless object in result no longer hides a usable string in message", () => {
|
|
111
|
+
const extracted = extractRunErrorSources(
|
|
112
|
+
errorResult({ result: { weird: "shape" }, message: "the real reason" }),
|
|
113
|
+
);
|
|
114
|
+
expect(extracted).toEqual({ sdkError: undefined, sdkResultFields: "the real reason" });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("an empty string in result no longer short-circuits the chain", () => {
|
|
118
|
+
const extracted = extractRunErrorSources(
|
|
119
|
+
errorResult({ result: "", reason: "torn down mid-stream" }),
|
|
120
|
+
);
|
|
121
|
+
expect(extracted).toEqual({ sdkError: undefined, sdkResultFields: "torn down mid-stream" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("respects the documented field order: result before error before message before reason", () => {
|
|
125
|
+
const extracted = extractRunErrorSources(
|
|
126
|
+
errorResult({ result: "from-result", error: "from-error", message: "from-message" }),
|
|
127
|
+
);
|
|
128
|
+
expect(extracted.sdkResultFields).toBe("from-result");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("yields nothing when every candidate is hopeless", () => {
|
|
132
|
+
const extracted = extractRunErrorSources(
|
|
133
|
+
errorResult({ result: {}, error: "", message: "[object Object]" }),
|
|
134
|
+
);
|
|
135
|
+
expect(extracted).toEqual(NOTHING);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("end-to-end through synthesizeError", () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
142
|
+
});
|
|
143
|
+
afterEach(() => {
|
|
144
|
+
vi.restoreAllMocks();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("a structured retryable error classifies and re-enables fresh-agent recovery", () => {
|
|
148
|
+
// The regression at the heart of oss#299: String() turned this into
|
|
149
|
+
// "[object Object]" -> category=unknown, retryable=false -> the
|
|
150
|
+
// poisoned-handle retry could never fire for a plain network flake.
|
|
151
|
+
const extracted = extractRunErrorSources(
|
|
152
|
+
errorResult({ error: { code: "unavailable", message: "upstream connect error" } }),
|
|
153
|
+
);
|
|
154
|
+
const classified = synthesizeError({ ...base(), ...extracted });
|
|
155
|
+
|
|
156
|
+
expect(classified.source).toBe("sdk");
|
|
157
|
+
expect(classified.category).toBe("network");
|
|
158
|
+
expect(classified.message).toBe("upstream connect error");
|
|
159
|
+
expect(classified.retryable).toBe(true);
|
|
160
|
+
expect(shouldRetryWithFreshAgent(classified)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("hopeless extraction yields to the conversation introspection source", () => {
|
|
164
|
+
const extracted = extractRunErrorSources(errorResult({ result: { weird: "shape" } }));
|
|
165
|
+
const classified = synthesizeError({
|
|
166
|
+
...base(),
|
|
167
|
+
...extracted,
|
|
168
|
+
conversationErrorText: "grpc-status 12: routing failure",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(classified.source).toBe("conversation");
|
|
172
|
+
expect(classified.message).toBe("grpc-status 12: routing failure");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("hopeless extraction yields to the captured rejection source", () => {
|
|
176
|
+
const extracted = extractRunErrorSources(errorResult({ result: { weird: "shape" } }));
|
|
177
|
+
const classified = synthesizeError({
|
|
178
|
+
...base(),
|
|
179
|
+
...extracted,
|
|
180
|
+
capturedRejection: { code: "unavailable", message: "socket hang up", timestamp: Date.now() },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(classified.source).toBe("rejection");
|
|
184
|
+
expect(classified.message).toContain("socket hang up");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("classifyFromSources '[object Object]' defense-in-depth guard", () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
191
|
+
});
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
vi.restoreAllMocks();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("treats a leaked '[object Object]' sdkResultFields as absent", () => {
|
|
197
|
+
// Extraction never emits it, but any other producer of the junk string
|
|
198
|
+
// must not shadow the sources below it.
|
|
199
|
+
const classified = synthesizeError({
|
|
200
|
+
...base(),
|
|
201
|
+
sdkResultFields: "[object Object]",
|
|
202
|
+
streamErrorMessage: "fetch failed",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(classified.source).toBe("stream");
|
|
206
|
+
expect(classified.category).toBe("network");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type { Config } from "../../../config.js";
|
|
3
|
+
|
|
4
|
+
vi.mock("../../../shared/model-registry.js", () => ({
|
|
5
|
+
getEconomyModel: vi.fn().mockResolvedValue("gpt-4o-mini"),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const mockInvoke = vi.fn();
|
|
9
|
+
const mockWithStructuredOutput = vi.fn().mockReturnValue({ invoke: mockInvoke });
|
|
10
|
+
|
|
11
|
+
vi.mock("../../../shared/model-client.js", () => ({
|
|
12
|
+
buildChatModel: vi.fn().mockResolvedValue({
|
|
13
|
+
model: { withStructuredOutput: (...args: unknown[]) => mockWithStructuredOutput(...args) },
|
|
14
|
+
provider: "openai",
|
|
15
|
+
apiModelId: "gpt-4o-mini",
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// llm-backend.js and llm-proxy.js stay real: the pre-check behavior under
|
|
20
|
+
// test IS their composition, and both are pure modules.
|
|
21
|
+
|
|
22
|
+
const SCHEMA = { type: "object", properties: { answer: { type: "string" } } };
|
|
23
|
+
|
|
24
|
+
function makeConfig(overrides: Partial<Config> = {}): Config {
|
|
25
|
+
return {
|
|
26
|
+
proxyEndpoint: null,
|
|
27
|
+
stigmerToken: null,
|
|
28
|
+
...overrides,
|
|
29
|
+
} as Config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("extractStructuredOutput", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
// Deterministic regardless of the developer's shell: blank reads as
|
|
36
|
+
// missing, and backend vars must not leak in from outside.
|
|
37
|
+
vi.stubEnv("OPENAI_API_KEY", "");
|
|
38
|
+
vi.stubEnv("ANTHROPIC_API_KEY", "");
|
|
39
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "");
|
|
40
|
+
vi.stubEnv("STIGMER_OPENAI_BACKEND", "");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.unstubAllEnvs();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("direct mode with a key builds a direct-mode model (no endpoint threaded)", async () => {
|
|
48
|
+
vi.stubEnv("OPENAI_API_KEY", "sk-direct");
|
|
49
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
50
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
51
|
+
mockInvoke.mockResolvedValueOnce({ answer: "42" });
|
|
52
|
+
|
|
53
|
+
const result = await extractStructuredOutput("the answer is 42", SCHEMA, makeConfig(), "gpt-4.1");
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual({ answer: "42" });
|
|
56
|
+
// The regression pin: the gRPC control-plane endpoint must never
|
|
57
|
+
// reappear here as a stand-in LLM proxy.
|
|
58
|
+
expect(buildChatModel).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({ proxyEndpoint: undefined }),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("throws the credential message before any construction when no path exists", async () => {
|
|
64
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
65
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
66
|
+
|
|
67
|
+
await expect(
|
|
68
|
+
extractStructuredOutput("text", SCHEMA, makeConfig(), "gpt-4.1"),
|
|
69
|
+
).rejects.toThrow(/'gpt-4o-mini'.*OPENAI_API_KEY/s);
|
|
70
|
+
expect(buildChatModel).not.toHaveBeenCalled();
|
|
71
|
+
expect(mockInvoke).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("proxy mode threads the proxy endpoint and token, consulting no keys", async () => {
|
|
75
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
76
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
77
|
+
mockInvoke.mockResolvedValueOnce({ answer: "ok" });
|
|
78
|
+
|
|
79
|
+
const result = await extractStructuredOutput(
|
|
80
|
+
"text", SCHEMA,
|
|
81
|
+
makeConfig({ proxyEndpoint: "https://api.stigmer.ai", stigmerToken: "tok" }),
|
|
82
|
+
"gpt-4.1",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({ answer: "ok" });
|
|
86
|
+
expect(buildChatModel).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
proxyEndpoint: "https://api.stigmer.ai",
|
|
89
|
+
stigmerToken: "tok",
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("defers an un-inferable extraction model to buildChatModel's own error", async () => {
|
|
95
|
+
// The registry-empty fallback returns the primary model verbatim; when
|
|
96
|
+
// its provider can't be inferred the pre-check must not guess — the
|
|
97
|
+
// construction path owns the precise message.
|
|
98
|
+
const { getEconomyModel } = await import("../../../shared/model-registry.js");
|
|
99
|
+
vi.mocked(getEconomyModel).mockResolvedValueOnce("mystery-model");
|
|
100
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
101
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
102
|
+
mockInvoke.mockResolvedValueOnce({ answer: "ok" });
|
|
103
|
+
|
|
104
|
+
await extractStructuredOutput("text", SCHEMA, makeConfig(), "mystery-model");
|
|
105
|
+
|
|
106
|
+
expect(buildChatModel).toHaveBeenCalledWith(
|
|
107
|
+
expect.objectContaining({ modelName: "mystery-model" }),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("normalizes an empty extraction result to null", async () => {
|
|
112
|
+
vi.stubEnv("OPENAI_API_KEY", "sk-direct");
|
|
113
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
114
|
+
mockInvoke.mockResolvedValueOnce(undefined);
|
|
115
|
+
|
|
116
|
+
const result = await extractStructuredOutput("text", SCHEMA, makeConfig(), "gpt-4.1");
|
|
117
|
+
|
|
118
|
+
expect(result).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -245,6 +245,99 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
|
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
+
// The enabled_tools capability manifest (issue #350): mcpServerEnabledTools
|
|
249
|
+
// holds ONLY restricted servers; the hook denies a listed server's
|
|
250
|
+
// non-listed tool with the non-pausing, permanent "disabled" kind — BEFORE
|
|
251
|
+
// autoApproveAll and the grant checks, because a manifest is not an
|
|
252
|
+
// approval gate (nothing may resurrect a disabled tool). hookMcp payloads
|
|
253
|
+
// carry mcp_server_name "srv".
|
|
254
|
+
describe("MCP enabled_tools manifest (beforeMCPExecution, issue #350)", () => {
|
|
255
|
+
it("denies a non-enabled tool with kind disabled (content-free, single record) and the manifest message", () => {
|
|
256
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps"] } });
|
|
257
|
+
|
|
258
|
+
const res = h.decide(hookMcp("click", { app: "Slack" }));
|
|
259
|
+
|
|
260
|
+
expect(res.permission).toBe("deny");
|
|
261
|
+
// Permanent-denial framing, never the approval promise: the model must
|
|
262
|
+
// adapt, not wait for a resume that will never come.
|
|
263
|
+
expect(res.raw).toContain("not enabled for this agent");
|
|
264
|
+
expect(res.raw).not.toContain("submitted to the user for approval");
|
|
265
|
+
const ledger = h.ledger();
|
|
266
|
+
expect(ledger).toHaveLength(1);
|
|
267
|
+
expect(ledger[0].kind).toBe("disabled");
|
|
268
|
+
// Attributable under the MCP name-token (the identity the stream row
|
|
269
|
+
// computes), content-free like every non-approval kind.
|
|
270
|
+
expect(ledger[0].token).toBe(grantToken("click", ""));
|
|
271
|
+
expect(ledger[0]).not.toHaveProperty("input");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("allows an enabled tool on a restricted server", () => {
|
|
275
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps"] } });
|
|
276
|
+
expect(h.decide(hookMcp("list_apps")).permission).toBe("allow");
|
|
277
|
+
expect(h.ledger()).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("denies even under autoApproveAll (a manifest is not an approval gate)", () => {
|
|
281
|
+
const h = setup({
|
|
282
|
+
autoApproveAll: true,
|
|
283
|
+
mcpServerEnabledTools: { srv: ["list_apps"] },
|
|
284
|
+
});
|
|
285
|
+
const res = h.decide(hookMcp("click"));
|
|
286
|
+
expect(res.permission).toBe("deny");
|
|
287
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("denies even when the tool holds a reinvocation grant (no approval may resurrect it)", () => {
|
|
291
|
+
const h = setup({
|
|
292
|
+
mcpServerEnabledTools: { srv: ["list_apps"] },
|
|
293
|
+
grants: [{ toolName: "click", mcpServerSlug: "srv", key: "click", salient: "", contentDigest: "", sourceToolCallId: "consent-1" }],
|
|
294
|
+
});
|
|
295
|
+
const res = h.decide(hookMcp("click"));
|
|
296
|
+
expect(res.permission).toBe("deny");
|
|
297
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("stays kind disabled under unattended mode (mode-independent, like secret)", () => {
|
|
301
|
+
const h = setup({
|
|
302
|
+
unattendedSkip: true,
|
|
303
|
+
mcpServerEnabledTools: { srv: ["list_apps"] },
|
|
304
|
+
});
|
|
305
|
+
const res = h.decide(hookMcp("click"));
|
|
306
|
+
expect(res.permission).toBe("deny");
|
|
307
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("an enabled tool still flows into the normal approval arm (manifest and gate compose)", () => {
|
|
311
|
+
const h = setup({
|
|
312
|
+
mcpPolicies: { click: { requiresApproval: true, message: "Approve click?" } },
|
|
313
|
+
mcpServerEnabledTools: { srv: ["click"] },
|
|
314
|
+
});
|
|
315
|
+
const res = h.decide(hookMcp("click"));
|
|
316
|
+
expect(res.permission).toBe("deny");
|
|
317
|
+
expect(res.raw).toContain("Approve click?");
|
|
318
|
+
expect(h.ledger()[0].kind).toBe("approval");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("a restriction on ANOTHER server never narrows this one (server-scoped matching)", () => {
|
|
322
|
+
const h = setup({ mcpServerEnabledTools: { other: ["something_else"] } });
|
|
323
|
+
expect(h.decide(hookMcp("click")).permission).toBe("allow");
|
|
324
|
+
expect(h.ledger()).toEqual([]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("quoted-name matching is exact — an enabled name never allows its prefix-sibling", () => {
|
|
328
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps_extended"] } });
|
|
329
|
+
const res = h.decide(hookMcp("list_apps"));
|
|
330
|
+
expect(res.permission).toBe("deny");
|
|
331
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("never gates a preToolUse (built-in) payload — the manifest arm is MCP-event-scoped", () => {
|
|
335
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps"] } });
|
|
336
|
+
expect(h.decide(hookRead("/x/a.txt")).permission).toBe("allow");
|
|
337
|
+
expect(h.ledger()).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
248
341
|
// The hook captures the COMPLETE tool_input on every denial (base64(JSON)),
|
|
249
342
|
// so the runner can overlay the proposed change onto the gated tool call for
|
|
250
343
|
// the approval preview — the cursor analog of the native harness reading args
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { resolveMcpServers } from "../mcp-resolver.js";
|
|
3
|
+
|
|
4
|
+
function makeUsage(
|
|
5
|
+
slug: string,
|
|
6
|
+
enabledTools: string[] = [],
|
|
7
|
+
toolApprovalOverrides: Array<{ toolName: string; requiresApproval: boolean }> = [],
|
|
8
|
+
) {
|
|
9
|
+
return {
|
|
10
|
+
mcpServerRef: { slug, org: "test-org", kind: 0 },
|
|
11
|
+
enabledTools,
|
|
12
|
+
toolApprovalOverrides,
|
|
13
|
+
} as any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function httpMcpServer(slug: string, defaultEnabledTools: string[] = []) {
|
|
17
|
+
return {
|
|
18
|
+
metadata: { id: `id-${slug}`, slug },
|
|
19
|
+
spec: {
|
|
20
|
+
serverType: { case: "http", value: { url: "https://mcp.example.com/mcp", headers: {} } },
|
|
21
|
+
env: {},
|
|
22
|
+
defaultEnabledTools,
|
|
23
|
+
},
|
|
24
|
+
status: undefined,
|
|
25
|
+
} as any;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clientReturning(serversBySlug: Record<string, unknown>) {
|
|
29
|
+
return {
|
|
30
|
+
getMcpServerByReference: vi.fn(async (ref: { slug: string }) => {
|
|
31
|
+
const server = serversBySlug[ref.slug];
|
|
32
|
+
if (!server) throw new Error(`not found: ${ref.slug}`);
|
|
33
|
+
return server;
|
|
34
|
+
}),
|
|
35
|
+
} as any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The effective-list semantics live in shared/mcp-enabled-tools.ts (tested
|
|
39
|
+
// there); these tests pin the CURSOR resolver's threading — the near-duplicate
|
|
40
|
+
// of shared/mcp-resolver.ts that must mirror it until oss#387 consolidates.
|
|
41
|
+
describe("resolveMcpServers (cursor) — enabled_tools threading (issue #350)", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("carries the usage's enabled_tools as the effective allow-list", async () => {
|
|
48
|
+
const client = clientReturning({ github: httpMcpServer("github") });
|
|
49
|
+
|
|
50
|
+
const result = await resolveMcpServers(
|
|
51
|
+
client, [makeUsage("github", ["create_pr"])], {}, "stdio-forbidden",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(result.resolvedServers[0].enabledTools).toEqual(["create_pr"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("falls back to default_enabled_tools for an empty usage list", async () => {
|
|
58
|
+
const client = clientReturning({
|
|
59
|
+
github: httpMcpServer("github", ["search_code"]),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await resolveMcpServers(
|
|
63
|
+
client, [makeUsage("github")], {}, "stdio-forbidden",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result.resolvedServers[0].enabledTools).toEqual(["search_code"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("resolves unrestricted (absent field) when both lists are empty", async () => {
|
|
70
|
+
const client = clientReturning({ github: httpMcpServer("github") });
|
|
71
|
+
|
|
72
|
+
const result = await resolveMcpServers(
|
|
73
|
+
client, [makeUsage("github")], {}, "stdio-forbidden",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result.resolvedServers[0].enabledTools).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("never narrows the Cursor SDK config — the SDK has no allow-list field; enforcement is the hook's disabled arm", async () => {
|
|
80
|
+
const client = clientReturning({ github: httpMcpServer("github") });
|
|
81
|
+
|
|
82
|
+
const result = await resolveMcpServers(
|
|
83
|
+
client, [makeUsage("github", ["create_pr"])], {}, "stdio-forbidden",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(result.cursorConfig.github).toEqual({
|
|
87
|
+
type: "http",
|
|
88
|
+
url: "https://mcp.example.com/mcp",
|
|
89
|
+
headers: undefined,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("resolveMcpServers (cursor) — tool_approval_overrides threading (issue #349)", () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
vi.restoreAllMocks();
|
|
97
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("carries the usage's overrides on its own resolved server only", async () => {
|
|
101
|
+
// Riding the server is the scoping mechanism: an override can no longer
|
|
102
|
+
// reach a same-named tool on another server, because it never exists
|
|
103
|
+
// anywhere but its own server's object.
|
|
104
|
+
const client = clientReturning({
|
|
105
|
+
github: httpMcpServer("github"),
|
|
106
|
+
slack: httpMcpServer("slack"),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = await resolveMcpServers(
|
|
110
|
+
client,
|
|
111
|
+
[
|
|
112
|
+
makeUsage("github", [], [{ toolName: "delete_item", requiresApproval: false }]),
|
|
113
|
+
makeUsage("slack"),
|
|
114
|
+
],
|
|
115
|
+
{},
|
|
116
|
+
"stdio-forbidden",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const bySlug = new Map(result.resolvedServers.map((s) => [s.slug, s]));
|
|
120
|
+
expect(bySlug.get("github")!.toolApprovalOverrides).toEqual([
|
|
121
|
+
{ toolName: "delete_item", requiresApproval: false },
|
|
122
|
+
]);
|
|
123
|
+
expect(bySlug.get("slack")!.toolApprovalOverrides).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getPricingTable,
|
|
4
|
+
_resetPricingCache,
|
|
5
|
+
DEFAULT_PRICING,
|
|
6
|
+
} from "../model-pricing-data.js";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Verifies the cursor-runner display estimate resolves Cursor speed variants
|
|
@@ -110,3 +115,59 @@ describe("getCursorModelPricing — speed variant resolution", () => {
|
|
|
110
115
|
expect(p.inputPricePerMillion).toBe(5.0);
|
|
111
116
|
});
|
|
112
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("getPricingTable failure caching (#468)", () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
_resetPricingCache();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
_resetPricingCache();
|
|
126
|
+
vi.useRealTimers();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("retries after the short failure TTL instead of pinning DEFAULT_PRICING for an hour", async () => {
|
|
130
|
+
// The model-registry.ts failure-cache policy, applied here: wrong default
|
|
131
|
+
// rates for cost tracking must not persist a full success TTL.
|
|
132
|
+
vi.useFakeTimers();
|
|
133
|
+
const registryResponse = () =>
|
|
134
|
+
new Response(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
models: [
|
|
137
|
+
{
|
|
138
|
+
id: "composer-2.5",
|
|
139
|
+
displayName: "Composer 2.5",
|
|
140
|
+
provider: "cursor",
|
|
141
|
+
harness: "cursor",
|
|
142
|
+
costTier: "economy",
|
|
143
|
+
pricing: {
|
|
144
|
+
inputPricePerMillion: 0.5,
|
|
145
|
+
outputPricePerMillion: 2.5,
|
|
146
|
+
cacheWritePricePerMillion: 0,
|
|
147
|
+
cacheReadPricePerMillion: 0.2,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
}),
|
|
152
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
153
|
+
);
|
|
154
|
+
const fetchSpy = vi
|
|
155
|
+
.fn<typeof fetch>()
|
|
156
|
+
.mockRejectedValueOnce(new Error("network error"))
|
|
157
|
+
.mockResolvedValueOnce(registryResponse());
|
|
158
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
159
|
+
|
|
160
|
+
// First call fails and degrades to the default table.
|
|
161
|
+
expect((await getPricingTable())[0]).toBe(DEFAULT_PRICING);
|
|
162
|
+
|
|
163
|
+
// Within the failure TTL the fallback stays cached (no refetch).
|
|
164
|
+
vi.advanceTimersByTime(30_000);
|
|
165
|
+
expect((await getPricingTable())[0]).toBe(DEFAULT_PRICING);
|
|
166
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
167
|
+
|
|
168
|
+
// Past the failure TTL the registry is refetched and real rates recover.
|
|
169
|
+
vi.advanceTimersByTime(31_000);
|
|
170
|
+
expect((await getPricingTable())[0]?.model).toBe("composer-2.5");
|
|
171
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
172
|
+
});
|
|
173
|
+
});
|