@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.
Files changed (239) hide show
  1. package/README.md +2 -0
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/discover-mcp-server.d.ts +7 -0
  4. package/dist/activities/discover-mcp-server.js +6 -4
  5. package/dist/activities/discover-mcp-server.js.map +1 -1
  6. package/dist/activities/emit-event.d.ts +14 -2
  7. package/dist/activities/emit-event.js +52 -17
  8. package/dist/activities/emit-event.js.map +1 -1
  9. package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
  10. package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
  11. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  12. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  13. package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
  14. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  15. package/dist/activities/execute-cursor/error-classifier.d.ts +19 -0
  16. package/dist/activities/execute-cursor/error-classifier.js +34 -3
  17. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  18. package/dist/activities/execute-cursor/index.d.ts +52 -12
  19. package/dist/activities/execute-cursor/index.js +141 -67
  20. package/dist/activities/execute-cursor/index.js.map +1 -1
  21. package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
  22. package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
  23. package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
  24. package/dist/activities/execute-cursor/prompt-builder.d.ts +48 -11
  25. package/dist/activities/execute-cursor/prompt-builder.js +57 -10
  26. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  27. package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
  28. package/dist/activities/execute-cursor/turn-recovery.js +193 -0
  29. package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
  30. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +11 -0
  31. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -1
  32. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/index.js +0 -15
  34. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
  36. package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
  37. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
  39. package/dist/activities/execute-deep-agent/prompt-builder.js +14 -7
  40. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
  42. package/dist/activities/execute-deep-agent/setup.js +22 -49
  43. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
  45. package/dist/activities/execute-deep-agent/shell-env.js +7 -9
  46. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/subagent-transformer.js +7 -1
  48. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  49. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +17 -3
  50. package/dist/activities/execute-deep-agent/subagent-wiring.js +14 -2
  51. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  52. package/dist/activities/hydrate-workflow-execution.js +8 -5
  53. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  54. package/dist/activities/run-command.d.ts +5 -2
  55. package/dist/activities/run-command.js +22 -10
  56. package/dist/activities/run-command.js.map +1 -1
  57. package/dist/activities/run-env.d.ts +40 -0
  58. package/dist/activities/run-env.js +66 -0
  59. package/dist/activities/run-env.js.map +1 -0
  60. package/dist/bootstrap.js +5 -0
  61. package/dist/bootstrap.js.map +1 -1
  62. package/dist/client/stigmer-client.d.ts +83 -26
  63. package/dist/client/stigmer-client.js +88 -24
  64. package/dist/client/stigmer-client.js.map +1 -1
  65. package/dist/config.d.ts +6 -5
  66. package/dist/config.js.map +1 -1
  67. package/dist/encryption/config.d.ts +41 -9
  68. package/dist/encryption/config.js +63 -23
  69. package/dist/encryption/config.js.map +1 -1
  70. package/dist/middleware/index.d.ts +3 -0
  71. package/dist/middleware/index.js +7 -0
  72. package/dist/middleware/index.js.map +1 -1
  73. package/dist/middleware/path-normalization.d.ts +57 -0
  74. package/dist/middleware/path-normalization.js +109 -0
  75. package/dist/middleware/path-normalization.js.map +1 -0
  76. package/dist/middleware/types.d.ts +8 -0
  77. package/dist/payload-codecs.d.ts +2 -1
  78. package/dist/payload-codecs.js +7 -3
  79. package/dist/payload-codecs.js.map +1 -1
  80. package/dist/runner-manager.js +17 -3
  81. package/dist/runner-manager.js.map +1 -1
  82. package/dist/runner.js +7 -3
  83. package/dist/runner.js.map +1 -1
  84. package/dist/shared/artifact-storage.d.ts +34 -1
  85. package/dist/shared/artifact-storage.js +53 -10
  86. package/dist/shared/artifact-storage.js.map +1 -1
  87. package/dist/shared/attachment-download-urls.d.ts +62 -0
  88. package/dist/shared/attachment-download-urls.js +79 -0
  89. package/dist/shared/attachment-download-urls.js.map +1 -0
  90. package/dist/shared/channel-attachment.d.ts +1 -1
  91. package/dist/shared/channel-attachment.js +1 -1
  92. package/dist/shared/checkpointer/http-saver.d.ts +4 -20
  93. package/dist/shared/checkpointer/http-saver.js +15 -57
  94. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  95. package/dist/shared/conversation-attachment.d.ts +2 -3
  96. package/dist/shared/conversation-attachment.js +2 -3
  97. package/dist/shared/conversation-attachment.js.map +1 -1
  98. package/dist/shared/conversation-catchup.d.ts +9 -2
  99. package/dist/shared/conversation-catchup.js +39 -6
  100. package/dist/shared/conversation-catchup.js.map +1 -1
  101. package/dist/shared/grpc-retry.d.ts +5 -1
  102. package/dist/shared/grpc-retry.js +5 -1
  103. package/dist/shared/grpc-retry.js.map +1 -1
  104. package/dist/shared/http-retry.d.ts +51 -9
  105. package/dist/shared/http-retry.js +60 -9
  106. package/dist/shared/http-retry.js.map +1 -1
  107. package/dist/shared/mcp-manager.d.ts +8 -4
  108. package/dist/shared/mcp-manager.js +30 -4
  109. package/dist/shared/mcp-manager.js.map +1 -1
  110. package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
  111. package/dist/shared/mcp-schema-sanitizer.js +197 -0
  112. package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
  113. package/dist/shared/mcp-transport-guard.d.ts +2 -2
  114. package/dist/shared/mcp-transport-guard.js +2 -2
  115. package/dist/shared/model-client.d.ts +6 -0
  116. package/dist/shared/model-client.js +14 -3
  117. package/dist/shared/model-client.js.map +1 -1
  118. package/dist/shared/model-pricing-data.d.ts +2 -0
  119. package/dist/shared/model-pricing-data.js +13 -3
  120. package/dist/shared/model-pricing-data.js.map +1 -1
  121. package/dist/shared/model-registry.js +3 -2
  122. package/dist/shared/model-registry.js.map +1 -1
  123. package/dist/shared/plan-mode-permissions.d.ts +7 -0
  124. package/dist/shared/plan-mode-permissions.js +7 -0
  125. package/dist/shared/plan-mode-permissions.js.map +1 -1
  126. package/dist/shared/registry-endpoint.d.ts +20 -0
  127. package/dist/shared/registry-endpoint.js +25 -0
  128. package/dist/shared/registry-endpoint.js.map +1 -1
  129. package/dist/shared/runner-credential-keys.d.ts +24 -0
  130. package/dist/shared/runner-credential-keys.js +47 -0
  131. package/dist/shared/runner-credential-keys.js.map +1 -0
  132. package/dist/shared/synthesized-attachment.d.ts +4 -4
  133. package/dist/shared/synthesized-attachment.js +4 -4
  134. package/dist/workflow-engine/resolve.d.ts +19 -1
  135. package/dist/workflow-engine/resolve.js +37 -2
  136. package/dist/workflow-engine/resolve.js.map +1 -1
  137. package/dist/workflows/connect-mcp-server.js +2 -0
  138. package/dist/workflows/connect-mcp-server.js.map +1 -1
  139. package/dist/workflows/types.d.ts +8 -0
  140. package/package.json +3 -4
  141. package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
  142. package/src/__test-utils__/fake-artifact-storage.ts +10 -1
  143. package/src/__test-utils__/vitest-global-setup.ts +50 -0
  144. package/src/__tests__/bootstrap.test.ts +43 -0
  145. package/src/__tests__/encryption-codec.test.ts +53 -0
  146. package/src/__tests__/preflight.test.ts +15 -0
  147. package/src/activities/__tests__/discover-mcp-server.test.ts +39 -1
  148. package/src/activities/__tests__/error-classifier.test.ts +94 -2
  149. package/src/activities/__tests__/run-command.test.ts +221 -0
  150. package/src/activities/discover-mcp-server.ts +16 -2
  151. package/src/activities/emit-event.ts +65 -21
  152. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
  153. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +173 -31
  154. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
  155. package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
  156. package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
  157. package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
  158. package/src/activities/execute-cursor/error-classifier.ts +40 -3
  159. package/src/activities/execute-cursor/index.ts +175 -74
  160. package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
  161. package/src/activities/execute-cursor/prompt-builder.ts +89 -22
  162. package/src/activities/execute-cursor/turn-recovery.ts +208 -0
  163. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +105 -0
  164. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
  165. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
  166. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
  167. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
  168. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
  169. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +71 -0
  170. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
  171. package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
  172. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +22 -17
  173. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -1
  174. package/src/activities/execute-deep-agent/index.ts +0 -19
  175. package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
  176. package/src/activities/execute-deep-agent/prompt-builder.ts +27 -15
  177. package/src/activities/execute-deep-agent/setup.ts +22 -73
  178. package/src/activities/execute-deep-agent/shell-env.ts +8 -9
  179. package/src/activities/execute-deep-agent/subagent-transformer.ts +7 -1
  180. package/src/activities/execute-deep-agent/subagent-wiring.ts +26 -3
  181. package/src/activities/hydrate-workflow-execution.ts +8 -5
  182. package/src/activities/run-command.ts +25 -12
  183. package/src/activities/run-env.ts +79 -0
  184. package/src/bootstrap.ts +5 -0
  185. package/src/client/__tests__/stigmer-client.test.ts +104 -5
  186. package/src/client/stigmer-client.ts +128 -29
  187. package/src/config.ts +6 -5
  188. package/src/encryption/config.ts +87 -23
  189. package/src/middleware/__tests__/path-normalization.test.ts +140 -0
  190. package/src/middleware/index.ts +8 -0
  191. package/src/middleware/path-normalization.ts +125 -0
  192. package/src/middleware/types.ts +11 -0
  193. package/src/payload-codecs.ts +8 -2
  194. package/src/runner-manager.ts +21 -3
  195. package/src/runner.ts +7 -3
  196. package/src/shared/__tests__/artifact-storage.test.ts +154 -2
  197. package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
  198. package/src/shared/__tests__/channel-attachment.test.ts +2 -2
  199. package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
  200. package/src/shared/__tests__/http-retry.test.ts +5 -4
  201. package/src/shared/__tests__/mcp-manager.test.ts +53 -13
  202. package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
  203. package/src/shared/__tests__/model-client.test.ts +46 -0
  204. package/src/shared/__tests__/model-pricing.test.ts +62 -2
  205. package/src/shared/__tests__/model-registry.test.ts +22 -0
  206. package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
  207. package/src/shared/artifact-storage.ts +86 -10
  208. package/src/shared/attachment-download-urls.ts +98 -0
  209. package/src/shared/channel-attachment.ts +1 -1
  210. package/src/shared/checkpointer/http-saver.ts +15 -60
  211. package/src/shared/conversation-attachment.ts +2 -3
  212. package/src/shared/conversation-catchup.ts +39 -6
  213. package/src/shared/grpc-retry.ts +5 -1
  214. package/src/shared/http-retry.ts +98 -9
  215. package/src/shared/mcp-manager.ts +35 -4
  216. package/src/shared/mcp-schema-sanitizer.ts +224 -0
  217. package/src/shared/mcp-transport-guard.ts +2 -2
  218. package/src/shared/model-client.ts +21 -3
  219. package/src/shared/model-pricing-data.ts +22 -3
  220. package/src/shared/model-registry.ts +7 -2
  221. package/src/shared/plan-mode-permissions.ts +7 -0
  222. package/src/shared/registry-endpoint.ts +27 -0
  223. package/src/shared/runner-credential-keys.ts +46 -0
  224. package/src/shared/synthesized-attachment.ts +4 -4
  225. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
  226. package/src/workflow-engine/resolve.ts +48 -2
  227. package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
  228. package/src/workflows/connect-mcp-server.ts +2 -0
  229. package/src/workflows/types.ts +8 -0
  230. package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
  231. package/dist/activities/execute-cursor/mcp-config.js +0 -39
  232. package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
  233. package/dist/shared/datastore-attachment.d.ts +0 -99
  234. package/dist/shared/datastore-attachment.js +0 -184
  235. package/dist/shared/datastore-attachment.js.map +0 -1
  236. package/src/activities/execute-cursor/mcp-config.ts +0 -66
  237. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
  238. package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
  239. 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
- expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith("ctx-abc");
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 { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent } from "../execute-cursor/error-classifier.js";
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("no detail from SDK");
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
- execCtx = await client.getExecutionContextByExecutionId(executionContextId);
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) {