@stigmer/runner 3.11.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +6 -4
- 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/attachment-resolver.d.ts +11 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
- 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 +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +19 -0
- package/dist/activities/execute-cursor/error-classifier.js +34 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +52 -12
- package/dist/activities/execute-cursor/index.js +141 -67
- package/dist/activities/execute-cursor/index.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 +48 -11
- package/dist/activities/execute-cursor/prompt-builder.js +57 -10
- 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-deep-agent/attachment-injector.d.ts +11 -0
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +0 -15
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
- package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js +14 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
- package/dist/activities/execute-deep-agent/setup.js +22 -49
- 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.js +7 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +17 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +14 -2
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -5
- 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/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -26
- package/dist/client/stigmer-client.js +88 -24
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.js.map +1 -1
- package/dist/encryption/config.d.ts +41 -9
- package/dist/encryption/config.js +63 -23
- package/dist/encryption/config.js.map +1 -1
- 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 +2 -1
- package/dist/payload-codecs.js +7 -3
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +17 -3
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +7 -3
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +34 -1
- package/dist/shared/artifact-storage.js +53 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-download-urls.d.ts +62 -0
- package/dist/shared/attachment-download-urls.js +79 -0
- package/dist/shared/attachment-download-urls.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +4 -20
- package/dist/shared/checkpointer/http-saver.js +15 -57
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.d.ts +2 -3
- package/dist/shared/conversation-attachment.js +2 -3
- 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/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 +51 -9
- package/dist/shared/http-retry.js +60 -9
- package/dist/shared/http-retry.js.map +1 -1
- package/dist/shared/mcp-manager.d.ts +8 -4
- package/dist/shared/mcp-manager.js +30 -4
- package/dist/shared/mcp-manager.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/mcp-transport-guard.d.ts +2 -2
- package/dist/shared/mcp-transport-guard.js +2 -2
- package/dist/shared/model-client.d.ts +6 -0
- package/dist/shared/model-client.js +14 -3
- package/dist/shared/model-client.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 +7 -0
- package/dist/shared/plan-mode-permissions.js +7 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- 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/shared/synthesized-attachment.d.ts +4 -4
- package/dist/shared/synthesized-attachment.js +4 -4
- 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/workflows/connect-mcp-server.js +2 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +3 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/fake-artifact-storage.ts +10 -1
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/encryption-codec.test.ts +53 -0
- package/src/__tests__/preflight.test.ts +15 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +39 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/discover-mcp-server.ts +16 -2
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +173 -31
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
- package/src/activities/execute-cursor/error-classifier.ts +40 -3
- package/src/activities/execute-cursor/index.ts +175 -74
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +89 -22
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +105 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
- 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 +71 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
- 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 +22 -17
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -1
- package/src/activities/execute-deep-agent/index.ts +0 -19
- package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
- package/src/activities/execute-deep-agent/prompt-builder.ts +27 -15
- package/src/activities/execute-deep-agent/setup.ts +22 -73
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +7 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +26 -3
- package/src/activities/hydrate-workflow-execution.ts +8 -5
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/bootstrap.ts +5 -0
- package/src/client/__tests__/stigmer-client.test.ts +104 -5
- package/src/client/stigmer-client.ts +128 -29
- package/src/config.ts +6 -5
- package/src/encryption/config.ts +87 -23
- 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 +8 -2
- package/src/runner-manager.ts +21 -3
- package/src/runner.ts +7 -3
- package/src/shared/__tests__/artifact-storage.test.ts +154 -2
- package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
- package/src/shared/__tests__/channel-attachment.test.ts +2 -2
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/http-retry.test.ts +5 -4
- package/src/shared/__tests__/mcp-manager.test.ts +53 -13
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +46 -0
- 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 +19 -41
- package/src/shared/artifact-storage.ts +86 -10
- package/src/shared/attachment-download-urls.ts +98 -0
- package/src/shared/channel-attachment.ts +1 -1
- package/src/shared/checkpointer/http-saver.ts +15 -60
- package/src/shared/conversation-attachment.ts +2 -3
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +98 -9
- package/src/shared/mcp-manager.ts +35 -4
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/mcp-transport-guard.ts +2 -2
- package/src/shared/model-client.ts +21 -3
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +7 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/shared/synthesized-attachment.ts +4 -4
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/connect-mcp-server.ts +2 -0
- 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/dist/shared/datastore-attachment.d.ts +0 -99
- package/dist/shared/datastore-attachment.js +0 -184
- package/dist/shared/datastore-attachment.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
- package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
- package/src/shared/datastore-attachment.ts +0 -209
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest globalSetup: refuse to run the suite on a Node that cannot run the
|
|
3
|
+
* runner.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: the durable checkpointer imports Node's built-in
|
|
6
|
+
* `node:sqlite` at module load time, so on a Node without it (below 22.13,
|
|
7
|
+
* or 23.0–23.3 — the 23.x line only gained it unflagged in 23.4) the
|
|
8
|
+
* sqlite-importing test files die at COLLECTION with a raw
|
|
9
|
+
* `ERR_UNKNOWN_BUILTIN_MODULE` while the rest of the suite grinds on
|
|
10
|
+
* (oss#257). globalSetup runs once in the main process before any file is
|
|
11
|
+
* collected, so the whole run fails immediately with the same actionable
|
|
12
|
+
* message the runner itself prints at boot (src/preflight.ts).
|
|
13
|
+
*
|
|
14
|
+
* Deliberately fails the WHOLE suite rather than skipping the sqlite tests:
|
|
15
|
+
* a Node that cannot load `node:sqlite` cannot run the runner at all, and a
|
|
16
|
+
* green-with-skips suite on such a Node would be a false signal. The gate is
|
|
17
|
+
* a capability probe, not a version check — on Nodes where the builtin is
|
|
18
|
+
* flag-gated (22.5–22.12, 23.0–23.3), `NODE_OPTIONS=--experimental-sqlite`
|
|
19
|
+
* satisfies the probe and the suite runs (vitest workers inherit
|
|
20
|
+
* NODE_OPTIONS, so the flag reaches every fork).
|
|
21
|
+
*
|
|
22
|
+
* Lives in src/__test-utils__/ so `tsc --noEmit` covers it (the dev tsconfig
|
|
23
|
+
* only includes src/) while tsconfig.build.json's exclusion keeps it out of
|
|
24
|
+
* dist/.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { preflightNodeRuntime } from "../preflight.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Throw an actionable error when this Node cannot run the runner (and hence
|
|
31
|
+
* its test suite). The preflight message is reused verbatim — single source
|
|
32
|
+
* of the diagnosis, contract pinned by __tests__/preflight.test.ts — with a
|
|
33
|
+
* test-context fix hint appended (the check-runner-node Makefile idiom).
|
|
34
|
+
*/
|
|
35
|
+
export function assertNodeCanRunSuite(
|
|
36
|
+
preflight: () => string | null = preflightNodeRuntime,
|
|
37
|
+
): void {
|
|
38
|
+
const failure = preflight();
|
|
39
|
+
if (failure === null) return;
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${failure}\n` +
|
|
42
|
+
` fix: nvm use (the repo's .nvmrc pins a supported Node), or on a Node\n` +
|
|
43
|
+
` where node:sqlite is flag-gated (22.5-22.12, 23.0-23.3) run the suite\n` +
|
|
44
|
+
` with NODE_OPTIONS=--experimental-sqlite.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function globalSetup(): void {
|
|
49
|
+
assertNodeCanRunSuite();
|
|
50
|
+
}
|
|
@@ -161,6 +161,49 @@ describe("resolveRunnerBootstrap", () => {
|
|
|
161
161
|
);
|
|
162
162
|
|
|
163
163
|
expect(result.runnerAccessToken).toBeUndefined();
|
|
164
|
+
expect(result.payloadEncryption).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("surfaces server-managed payload-encryption keys from the discovery branch", async () => {
|
|
168
|
+
const factory: BootstrapClientFactory = async () => ({
|
|
169
|
+
getRunnerBootstrapConfig: () =>
|
|
170
|
+
Promise.resolve({
|
|
171
|
+
temporalAddress: "t:7233",
|
|
172
|
+
temporalNamespace: "default",
|
|
173
|
+
payloadEncryption: {
|
|
174
|
+
key: "a".repeat(44),
|
|
175
|
+
keyId: "identity-key-v2",
|
|
176
|
+
secondaryKey: "b".repeat(44),
|
|
177
|
+
secondaryKeyId: "identity-key-v1",
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await resolveRunnerBootstrap(
|
|
183
|
+
{ token: "tok", stigmerEndpoint: "https://api.stigmer.ai" },
|
|
184
|
+
factory,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(result.payloadEncryption).toEqual({
|
|
188
|
+
key: "a".repeat(44),
|
|
189
|
+
keyId: "identity-key-v2",
|
|
190
|
+
secondaryKey: "b".repeat(44),
|
|
191
|
+
secondaryKeyId: "identity-key-v1",
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("omits payload-encryption keys when the server does not manage them", async () => {
|
|
196
|
+
const factory: BootstrapClientFactory = async () => ({
|
|
197
|
+
getRunnerBootstrapConfig: () =>
|
|
198
|
+
Promise.resolve({ temporalAddress: "t:7233", temporalNamespace: "default" }),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const result = await resolveRunnerBootstrap(
|
|
202
|
+
{ token: "tok", stigmerEndpoint: "https://api.stigmer.ai" },
|
|
203
|
+
factory,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(result.payloadEncryption).toBeUndefined();
|
|
164
207
|
});
|
|
165
208
|
});
|
|
166
209
|
|
|
@@ -195,6 +195,59 @@ describe("loadPayloadEncryptionConfig", () => {
|
|
|
195
195
|
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY_ID = "k1";
|
|
196
196
|
expect(() => loadPayloadEncryptionConfig()).toThrow(/must decode to 32 bytes/);
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
// Server-managed key material from getRunnerBootstrapConfig (stigmer#398):
|
|
200
|
+
// desktop-class runners receive a per-identity key at bootstrap instead of
|
|
201
|
+
// env config. The env key is the operator's explicit choice and must win.
|
|
202
|
+
describe("bootstrap-delivered keys", () => {
|
|
203
|
+
const bootstrapKeys = () => ({
|
|
204
|
+
key: randomBytes(32).toString("base64"),
|
|
205
|
+
keyId: "identity-key-v1",
|
|
206
|
+
secondaryKey: randomBytes(32).toString("base64"),
|
|
207
|
+
secondaryKeyId: "identity-key-v0",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("enables encryption from bootstrap material when no env key is set", () => {
|
|
211
|
+
const config = loadPayloadEncryptionConfig(bootstrapKeys());
|
|
212
|
+
expect(config?.primary.keyId).toBe("identity-key-v1");
|
|
213
|
+
expect(config?.primary.key.length).toBe(32);
|
|
214
|
+
expect(config?.secondary?.keyId).toBe("identity-key-v0");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("env-configured key wins over bootstrap material", () => {
|
|
218
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY = randomBytes(32).toString("base64");
|
|
219
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY_ID = "env-key";
|
|
220
|
+
|
|
221
|
+
const config = loadPayloadEncryptionConfig(bootstrapKeys());
|
|
222
|
+
expect(config?.primary.keyId).toBe("env-key");
|
|
223
|
+
expect(config?.secondary).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("fails the boot on a bootstrap key without its id (server contract violation)", () => {
|
|
227
|
+
expect(() =>
|
|
228
|
+
loadPayloadEncryptionConfig({ key: randomBytes(32).toString("base64") }),
|
|
229
|
+
).toThrow(/payload_encryption_key_id/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("fails the boot on a malformed bootstrap key rather than running plaintext", () => {
|
|
233
|
+
expect(() =>
|
|
234
|
+
loadPayloadEncryptionConfig({
|
|
235
|
+
key: randomBytes(16).toString("base64"),
|
|
236
|
+
keyId: "identity-key-v1",
|
|
237
|
+
}),
|
|
238
|
+
).toThrow(/must decode to 32 bytes/);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("fails the boot on a bootstrap secondary key without its id", () => {
|
|
242
|
+
expect(() =>
|
|
243
|
+
loadPayloadEncryptionConfig({
|
|
244
|
+
key: randomBytes(32).toString("base64"),
|
|
245
|
+
keyId: "identity-key-v1",
|
|
246
|
+
secondaryKey: randomBytes(32).toString("base64"),
|
|
247
|
+
}),
|
|
248
|
+
).toThrow(/payload_encryption_secondary_key_id/);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
198
251
|
});
|
|
199
252
|
|
|
200
253
|
describe("cross-language conformance fixture", () => {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* the desktop Run dialog via the IPC handshake.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
14
15
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
15
16
|
import {
|
|
16
17
|
isNodeSqliteAvailable,
|
|
@@ -42,6 +43,20 @@ describe("preflightNodeRuntime", () => {
|
|
|
42
43
|
// case BOTH sides must report unsupported.
|
|
43
44
|
expect(preflightNodeRuntime() === null).toBe(isNodeSqliteAvailable());
|
|
44
45
|
});
|
|
46
|
+
|
|
47
|
+
it("package.json engines matches the floors this preflight documents", () => {
|
|
48
|
+
// The published engines range and the preflight message describe the same
|
|
49
|
+
// two floors (node:sqlite is unflagged from 22.13 in the 22.x line and
|
|
50
|
+
// only from 23.4 after that). engines is the only one of the two that a
|
|
51
|
+
// future edit can silently loosen — `>=22.13` once re-admitted the
|
|
52
|
+
// 23.0–23.3 gap (oss#257) — so pin it byte-for-byte to the range that
|
|
53
|
+
// excludes that gap. If the floors genuinely move, update the preflight
|
|
54
|
+
// message, this pin, and package.json together.
|
|
55
|
+
const pkg = JSON.parse(
|
|
56
|
+
readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
|
|
57
|
+
) as { engines: { node: string } };
|
|
58
|
+
expect(pkg.engines.node).toBe("^22.13.0 || >=23.4.0");
|
|
59
|
+
});
|
|
45
60
|
});
|
|
46
61
|
|
|
47
62
|
describe("assertLlmBackendsPreflight", () => {
|
|
@@ -416,7 +416,45 @@ describe("DiscoverMcpServer activity", () => {
|
|
|
416
416
|
);
|
|
417
417
|
|
|
418
418
|
expect(result.tools).toEqual([]);
|
|
419
|
-
|
|
419
|
+
// No payload-carried token -> the read goes out without a per-call
|
|
420
|
+
// credential (the cloud shape, where the ambient credential applies).
|
|
421
|
+
expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith("ctx-abc", undefined);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("presents the payload-carried EC token on the ExecutionContext read (oss#535)", async () => {
|
|
425
|
+
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
426
|
+
|
|
427
|
+
const mockClient = makeMockStigmerClient({
|
|
428
|
+
mcpServer: makeMcpServer({
|
|
429
|
+
metadata: { slug: "with-token" },
|
|
430
|
+
spec: makeStdioSpec("npx", ["server"]),
|
|
431
|
+
}),
|
|
432
|
+
executionContext: {
|
|
433
|
+
spec: {
|
|
434
|
+
data: {
|
|
435
|
+
API_KEY: { value: "secret-123", isSecret: true },
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const mockMcpClient = makeMockMcpClient({ tools: [] });
|
|
442
|
+
mockInitializeConnections.mockResolvedValue({});
|
|
443
|
+
mockGetClient.mockResolvedValue(mockMcpClient);
|
|
444
|
+
|
|
445
|
+
await discoverMcpServer(
|
|
446
|
+
{
|
|
447
|
+
mcpServerId: "mcp-env",
|
|
448
|
+
executionContextId: "ctx-abc",
|
|
449
|
+
executionContextToken: "scoped-ec-token",
|
|
450
|
+
},
|
|
451
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-allowed" },
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith(
|
|
455
|
+
"ctx-abc",
|
|
456
|
+
"scoped-ec-token",
|
|
457
|
+
);
|
|
420
458
|
});
|
|
421
459
|
|
|
422
460
|
it("discovers a caller-identity-templating server via the anonymous sentinel", async () => {
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
synthesizeError,
|
|
12
|
+
formatClassifiedError,
|
|
13
|
+
shouldRetryWithFreshAgent,
|
|
14
|
+
DETAIL_FREE_FALLBACK_USER_PREFIX,
|
|
15
|
+
TRANSPORT_TIMEOUT_USER_PREFIX,
|
|
16
|
+
} from "../execute-cursor/error-classifier.js";
|
|
11
17
|
|
|
12
18
|
describe("synthesizeError", () => {
|
|
13
19
|
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
@@ -213,7 +219,7 @@ describe("synthesizeError", () => {
|
|
|
213
219
|
|
|
214
220
|
expect(result.category).toBe("unknown");
|
|
215
221
|
expect(result.source).toBe("fallback");
|
|
216
|
-
expect(result.message).toContain(
|
|
222
|
+
expect(result.message).toContain(DETAIL_FREE_FALLBACK_USER_PREFIX);
|
|
217
223
|
expect(result.message).toContain("claude-sonnet-4");
|
|
218
224
|
});
|
|
219
225
|
|
|
@@ -261,6 +267,92 @@ describe("synthesizeError", () => {
|
|
|
261
267
|
expect(result.message).toContain("agentId=agent-123");
|
|
262
268
|
});
|
|
263
269
|
|
|
270
|
+
// The exact prod shape from oss#492: the Composer 2.5 capacity incident
|
|
271
|
+
// rejected every run with a bare { status: "error" } — all five detail
|
|
272
|
+
// channels empty, fresh agent (the poisoned-handle retry produces this
|
|
273
|
+
// same shape with isResumedHandle: false). End users read this message
|
|
274
|
+
// verbatim in embedded surfaces, so it must lead with actionable copy,
|
|
275
|
+
// keep the diagnostic parenthetical for operators, and be retryable
|
|
276
|
+
// (provider capacity is transient).
|
|
277
|
+
it("detail-free failure on a fresh agent leads with user-facing copy and is retryable (oss#492)", () => {
|
|
278
|
+
const result = synthesizeError({
|
|
279
|
+
sdkError: undefined,
|
|
280
|
+
sdkResultFields: undefined,
|
|
281
|
+
streamErrorMessage: undefined,
|
|
282
|
+
capturedRejection: undefined,
|
|
283
|
+
conversationErrorText: undefined,
|
|
284
|
+
isResumedHandle: false,
|
|
285
|
+
fallbackContext: { model: "composer-2.5", mode: "local", agentId: "agent-abc" },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(result.category).toBe("unknown");
|
|
289
|
+
expect(result.source).toBe("fallback");
|
|
290
|
+
expect(result.retryable).toBe(true);
|
|
291
|
+
expect(result.message.startsWith(DETAIL_FREE_FALLBACK_USER_PREFIX)).toBe(true);
|
|
292
|
+
// Diagnostic context survives for operators (and the env integration
|
|
293
|
+
// test's Model= matcher).
|
|
294
|
+
expect(result.message).toContain("Model=composer-2.5");
|
|
295
|
+
expect(result.message).toContain("mode=local");
|
|
296
|
+
expect(result.message).toContain("agentId=agent-abc");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// sdk-react's isInterruptedError reframes any error containing the phrase
|
|
300
|
+
// "retry or resume" as a neutral resumable notice instead of a failure
|
|
301
|
+
// alert. A capacity failure must render as a failure — guard the copy
|
|
302
|
+
// (formatted end-to-end, tail included) against ever matching that regex.
|
|
303
|
+
it("user-facing fallback copy never trips the sdk-react interrupted-error reframe", () => {
|
|
304
|
+
const interruptedReframe = /\[StallTimeoutError\]|execution interrupted|retry or resume/i;
|
|
305
|
+
for (const prefix of [DETAIL_FREE_FALLBACK_USER_PREFIX, TRANSPORT_TIMEOUT_USER_PREFIX]) {
|
|
306
|
+
expect(prefix).not.toMatch(interruptedReframe);
|
|
307
|
+
}
|
|
308
|
+
const formatted = formatClassifiedError(synthesizeError({
|
|
309
|
+
sdkResultFields: undefined,
|
|
310
|
+
streamErrorMessage: undefined,
|
|
311
|
+
capturedRejection: undefined,
|
|
312
|
+
isResumedHandle: false,
|
|
313
|
+
fallbackContext,
|
|
314
|
+
}));
|
|
315
|
+
expect(formatted).not.toMatch(interruptedReframe);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Transport-timeout heuristic (0 messages, ~default SDK timeout)
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
it("classifies a 0-message ~30s failure as retryable network with user-facing copy", () => {
|
|
323
|
+
const result = synthesizeError({
|
|
324
|
+
sdkResultFields: undefined,
|
|
325
|
+
streamErrorMessage: undefined,
|
|
326
|
+
capturedRejection: undefined,
|
|
327
|
+
isResumedHandle: false,
|
|
328
|
+
fallbackContext,
|
|
329
|
+
durationMs: 30012,
|
|
330
|
+
messageCount: 0,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(result.category).toBe("network");
|
|
334
|
+
expect(result.source).toBe("fallback");
|
|
335
|
+
expect(result.retryable).toBe(true);
|
|
336
|
+
expect(result.message.startsWith(TRANSPORT_TIMEOUT_USER_PREFIX)).toBe(true);
|
|
337
|
+
expect(result.message).toContain("30012ms");
|
|
338
|
+
expect(result.message).toContain("Model=claude-sonnet-4");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("does not apply the transport heuristic outside the default-timeout window", () => {
|
|
342
|
+
const result = synthesizeError({
|
|
343
|
+
sdkResultFields: undefined,
|
|
344
|
+
streamErrorMessage: undefined,
|
|
345
|
+
capturedRejection: undefined,
|
|
346
|
+
isResumedHandle: false,
|
|
347
|
+
fallbackContext,
|
|
348
|
+
durationMs: 120000,
|
|
349
|
+
messageCount: 0,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(result.category).toBe("unknown");
|
|
353
|
+
expect(result.message.startsWith(DETAIL_FREE_FALLBACK_USER_PREFIX)).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
264
356
|
// ---------------------------------------------------------------------------
|
|
265
357
|
// Source priority
|
|
266
358
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env contract tests for the workflow `run` task (oss#384).
|
|
3
|
+
*
|
|
4
|
+
* These spawn REAL subprocesses through the RunShell / RunScript
|
|
5
|
+
* activities and inspect the environment the child actually received —
|
|
6
|
+
* the leak-reproduction style established by the #256 fix (PR #383).
|
|
7
|
+
* Subprocess output is env variable NAMES only, never values.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { createRunCommandActivities } from "../run-command.js";
|
|
12
|
+
import { buildRunEnv, RUN_ENV_BASE_KEYS } from "../run-env.js";
|
|
13
|
+
import { RuntimePlaceholderResolutionError } from "../../workflow-engine/resolve.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Credentials planted into the runner process env before each test.
|
|
17
|
+
* Includes a name outside every known denylist to prove the contract
|
|
18
|
+
* is declare-to-receive, not deny-known-names.
|
|
19
|
+
*/
|
|
20
|
+
const PLANTED_SENTINELS: Record<string, string> = {
|
|
21
|
+
STIGMER_RUNNER_HITL_SECRET: "sentinel-hitl",
|
|
22
|
+
STIGMER_TOKEN: "sentinel-token",
|
|
23
|
+
CURSOR_API_KEY: "sentinel-cursor",
|
|
24
|
+
ANTHROPIC_API_KEY: "sentinel-anthropic",
|
|
25
|
+
OPERATOR_PRIVATE_CREDENTIAL: "sentinel-arbitrary",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const PRINT_ENV_KEYS_SHELL = `node -p 'JSON.stringify(Object.keys(process.env))'`;
|
|
29
|
+
const PRINT_ENV_KEYS_SCRIPT = `console.log(JSON.stringify(Object.keys(process.env)))`;
|
|
30
|
+
|
|
31
|
+
const activities = createRunCommandActivities();
|
|
32
|
+
|
|
33
|
+
async function childEnvKeysViaShell(
|
|
34
|
+
environment?: Record<string, string>,
|
|
35
|
+
runtimeEnv: Record<string, unknown> = {},
|
|
36
|
+
): Promise<string[]> {
|
|
37
|
+
const stdout = await activities.RunShell({
|
|
38
|
+
mode: "shell",
|
|
39
|
+
command: PRINT_ENV_KEYS_SHELL,
|
|
40
|
+
environment,
|
|
41
|
+
runtimeEnv,
|
|
42
|
+
});
|
|
43
|
+
return JSON.parse(String(stdout)) as string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function childEnvKeysViaScript(
|
|
47
|
+
environment?: Record<string, string>,
|
|
48
|
+
runtimeEnv: Record<string, unknown> = {},
|
|
49
|
+
): Promise<string[]> {
|
|
50
|
+
const stdout = await activities.RunScript({
|
|
51
|
+
mode: "script",
|
|
52
|
+
language: "js",
|
|
53
|
+
code: PRINT_ENV_KEYS_SCRIPT,
|
|
54
|
+
environment,
|
|
55
|
+
runtimeEnv,
|
|
56
|
+
});
|
|
57
|
+
return JSON.parse(String(stdout)) as string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("run task env contract (oss#384)", () => {
|
|
61
|
+
const saved: Record<string, string | undefined> = {};
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
for (const [key, value] of Object.entries(PLANTED_SENTINELS)) {
|
|
65
|
+
saved[key] = process.env[key];
|
|
66
|
+
process.env[key] = value;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
for (const key of Object.keys(PLANTED_SENTINELS)) {
|
|
72
|
+
if (saved[key] === undefined) {
|
|
73
|
+
delete process.env[key];
|
|
74
|
+
} else {
|
|
75
|
+
process.env[key] = saved[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("RunShell", () => {
|
|
81
|
+
it("does not leak runner process credentials to the child", async () => {
|
|
82
|
+
const keys = await childEnvKeysViaShell();
|
|
83
|
+
|
|
84
|
+
for (const sentinel of Object.keys(PLANTED_SENTINELS)) {
|
|
85
|
+
expect(keys, `runner credential "${sentinel}" leaked to shell child`)
|
|
86
|
+
.not.toContain(sentinel);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("provides the minimal base env and the declared overlay", async () => {
|
|
91
|
+
const keys = await childEnvKeysViaShell({ DECLARED_VAR: "value" });
|
|
92
|
+
|
|
93
|
+
for (const base of RUN_ENV_BASE_KEYS) {
|
|
94
|
+
if (process.env[base] !== undefined) {
|
|
95
|
+
expect(keys, `base key "${base}" missing from shell child`).toContain(base);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
expect(keys).toContain("DECLARED_VAR");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("resolves ${.secrets.KEY} placeholders in declared values just-in-time", async () => {
|
|
102
|
+
const stdout = await activities.RunShell({
|
|
103
|
+
mode: "shell",
|
|
104
|
+
command: `node -p 'process.env.API_KEY'`,
|
|
105
|
+
environment: { API_KEY: "${.secrets.TEST_API_KEY}" },
|
|
106
|
+
runtimeEnv: { TEST_API_KEY: "resolved-secret-value" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(stdout).toBe("resolved-secret-value");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("fails non-retryably, naming the variable, when a placeholder key is missing", async () => {
|
|
113
|
+
await expect(
|
|
114
|
+
activities.RunShell({
|
|
115
|
+
mode: "shell",
|
|
116
|
+
command: "true",
|
|
117
|
+
environment: { API_KEY: "${.secrets.NOT_PROVIDED}" },
|
|
118
|
+
runtimeEnv: {},
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toMatchObject({
|
|
121
|
+
nonRetryable: true,
|
|
122
|
+
type: "RUN_ENV_UNRESOLVED_PLACEHOLDER",
|
|
123
|
+
message: expect.stringContaining("NOT_PROVIDED"),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("RunScript", () => {
|
|
129
|
+
it("does not leak runner process credentials to the child", async () => {
|
|
130
|
+
const keys = await childEnvKeysViaScript();
|
|
131
|
+
|
|
132
|
+
for (const sentinel of Object.keys(PLANTED_SENTINELS)) {
|
|
133
|
+
expect(keys, `runner credential "${sentinel}" leaked to script child`)
|
|
134
|
+
.not.toContain(sentinel);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("provides the minimal base env and the declared overlay", async () => {
|
|
139
|
+
const keys = await childEnvKeysViaScript({ DECLARED_VAR: "value" });
|
|
140
|
+
|
|
141
|
+
for (const base of RUN_ENV_BASE_KEYS) {
|
|
142
|
+
if (process.env[base] !== undefined) {
|
|
143
|
+
expect(keys, `base key "${base}" missing from script child`).toContain(base);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
expect(keys).toContain("DECLARED_VAR");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("resolves ${.env_vars.KEY} placeholders in declared values just-in-time", async () => {
|
|
150
|
+
const stdout = await activities.RunScript({
|
|
151
|
+
mode: "script",
|
|
152
|
+
language: "js",
|
|
153
|
+
code: "console.log(process.env.REGION)",
|
|
154
|
+
environment: { REGION: "${.env_vars.DEPLOY_REGION}" },
|
|
155
|
+
runtimeEnv: { DEPLOY_REGION: "us-east-1" },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(stdout).toBe("us-east-1");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("buildRunEnv", () => {
|
|
164
|
+
it("copies only the base keys present in the runner env", () => {
|
|
165
|
+
const base = {
|
|
166
|
+
PATH: "/usr/bin",
|
|
167
|
+
HOME: "/home/runner",
|
|
168
|
+
TERM: "xterm",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const env = buildRunEnv(undefined, {}, base);
|
|
172
|
+
|
|
173
|
+
expect(env).toEqual({ PATH: "/usr/bin", HOME: "/home/runner", TERM: "xterm" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("never copies undeclared runner variables — the oss#384 tripwire", () => {
|
|
177
|
+
// Guards against a future reintroduction of `{ ...process.env }`:
|
|
178
|
+
// credentials must be structurally absent, whatever their names.
|
|
179
|
+
const base = {
|
|
180
|
+
PATH: "/usr/bin",
|
|
181
|
+
STIGMER_RUNNER_HITL_SECRET: "secret",
|
|
182
|
+
ANY_FUTURE_CREDENTIAL: "secret",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const env = buildRunEnv({ DECLARED: "yes" }, {}, base);
|
|
186
|
+
|
|
187
|
+
expect(Object.keys(env).sort()).toEqual(["DECLARED", "PATH"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("lets a declared variable override a base variable", () => {
|
|
191
|
+
const base = { PATH: "/usr/bin" };
|
|
192
|
+
|
|
193
|
+
const env = buildRunEnv({ PATH: "/custom/bin" }, {}, base);
|
|
194
|
+
|
|
195
|
+
expect(env.PATH).toBe("/custom/bin");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("passes non-placeholder declared values through untouched", () => {
|
|
199
|
+
const env = buildRunEnv(
|
|
200
|
+
{ LITERAL: "plain-value", TEMPLATED: "prefix-${.secrets.KEY}-suffix" },
|
|
201
|
+
{ KEY: "mid" },
|
|
202
|
+
{},
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(env.LITERAL).toBe("plain-value");
|
|
206
|
+
expect(env.TEMPLATED).toBe("prefix-mid-suffix");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("throws a named error for a missing placeholder key", () => {
|
|
210
|
+
expect(() =>
|
|
211
|
+
buildRunEnv({ API_KEY: "${.secrets.MISSING}" }, {}, {}),
|
|
212
|
+
).toThrowError(RuntimePlaceholderResolutionError);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
buildRunEnv({ API_KEY: "${.secrets.MISSING}" }, {}, {});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
expect((err as RuntimePlaceholderResolutionError).variableName).toBe("MISSING");
|
|
218
|
+
expect((err as Error).message).toContain('environment "API_KEY"');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -81,6 +81,13 @@ const PLATFORM_INJECTABLE_MAP: Record<string, string> = {
|
|
|
81
81
|
export interface DiscoverMcpServerInput {
|
|
82
82
|
mcpServerId: string;
|
|
83
83
|
executionContextId?: string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Execution-scoped token unlocking the connect EC's decrypted credentials
|
|
86
|
+
* (oss#535). Minted by the OSS handler and carried with the work item —
|
|
87
|
+
* discovery has no execution of its own to exchange for one. Absent on
|
|
88
|
+
* cloud, where the ambient connect_sandbox credential decrypts.
|
|
89
|
+
*/
|
|
90
|
+
executionContextToken?: string | null;
|
|
84
91
|
invokerIdentityAccountId?: string | null;
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -280,7 +287,7 @@ export async function discoverMcpServer(
|
|
|
280
287
|
input: DiscoverMcpServerInput,
|
|
281
288
|
deps: DiscoverDeps,
|
|
282
289
|
): Promise<DiscoverMcpServerOutput> {
|
|
283
|
-
const { mcpServerId, executionContextId } = input;
|
|
290
|
+
const { mcpServerId, executionContextId, executionContextToken } = input;
|
|
284
291
|
const { stigmerClient } = deps;
|
|
285
292
|
|
|
286
293
|
console.log(
|
|
@@ -301,6 +308,7 @@ export async function discoverMcpServer(
|
|
|
301
308
|
const envVars = await resolveEnvVarsForDiscovery(
|
|
302
309
|
stigmerClient,
|
|
303
310
|
executionContextId ?? null,
|
|
311
|
+
executionContextToken ?? null,
|
|
304
312
|
slug,
|
|
305
313
|
declaredEnv,
|
|
306
314
|
);
|
|
@@ -382,6 +390,7 @@ export async function discoverMcpServer(
|
|
|
382
390
|
async function resolveEnvVarsForDiscovery(
|
|
383
391
|
client: StigmerClient,
|
|
384
392
|
executionContextId: string | null,
|
|
393
|
+
executionContextToken: string | null,
|
|
385
394
|
slug: string,
|
|
386
395
|
declaredEnv: Record<string, EnvVarDeclaration>,
|
|
387
396
|
): Promise<Record<string, string>> {
|
|
@@ -394,7 +403,12 @@ async function resolveEnvVarsForDiscovery(
|
|
|
394
403
|
|
|
395
404
|
let execCtx: Awaited<ReturnType<typeof client.getExecutionContextByExecutionId>>;
|
|
396
405
|
try {
|
|
397
|
-
|
|
406
|
+
// The payload-carried token authenticates the read on OSS (oss#535);
|
|
407
|
+
// undefined on cloud, where the ambient credential applies instead.
|
|
408
|
+
execCtx = await client.getExecutionContextByExecutionId(
|
|
409
|
+
executionContextId,
|
|
410
|
+
executionContextToken ?? undefined,
|
|
411
|
+
);
|
|
398
412
|
} catch (err) {
|
|
399
413
|
const cause = err instanceof Error ? err.message : String(err);
|
|
400
414
|
if (credentialsExpected) {
|