@stigmer/runner 3.40.0 → 3.41.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 (278) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  34. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  35. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  37. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  38. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  40. package/dist/activities/execute-deep-agent/translator.js +5 -4
  41. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  43. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  45. package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
  46. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  48. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  49. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  51. package/dist/activities/execute-deep-agent/turn.js +15 -6
  52. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  53. package/dist/activities/workflow-event-activities.js +2 -0
  54. package/dist/activities/workflow-event-activities.js.map +1 -1
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +2 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/exit-backstop.d.ts +64 -0
  59. package/dist/exit-backstop.js +79 -0
  60. package/dist/exit-backstop.js.map +1 -0
  61. package/dist/harness/terminal-table.d.ts +12 -5
  62. package/dist/harness/terminal-table.js +13 -13
  63. package/dist/harness/terminal-table.js.map +1 -1
  64. package/dist/harness/transcript/builder.d.ts +21 -0
  65. package/dist/harness/transcript/builder.js +53 -31
  66. package/dist/harness/transcript/builder.js.map +1 -1
  67. package/dist/harness/transcript/events.d.ts +22 -26
  68. package/dist/harness/transcript/events.js +6 -5
  69. package/dist/harness/transcript/events.js.map +1 -1
  70. package/dist/harness/turn-context.d.ts +3 -1
  71. package/dist/harness/turn-context.js +5 -3
  72. package/dist/harness/turn-context.js.map +1 -1
  73. package/dist/main.js +10 -4
  74. package/dist/main.js.map +1 -1
  75. package/dist/middleware/cost-advisory.d.ts +6 -3
  76. package/dist/middleware/cost-advisory.js +6 -3
  77. package/dist/middleware/cost-advisory.js.map +1 -1
  78. package/dist/middleware/execution-budget.d.ts +22 -7
  79. package/dist/middleware/execution-budget.js +57 -35
  80. package/dist/middleware/execution-budget.js.map +1 -1
  81. package/dist/middleware/index.d.ts +10 -7
  82. package/dist/middleware/index.js +10 -7
  83. package/dist/middleware/index.js.map +1 -1
  84. package/dist/middleware/types.d.ts +6 -1
  85. package/dist/runner-manager.js +3 -2
  86. package/dist/runner-manager.js.map +1 -1
  87. package/dist/runner.d.ts +25 -0
  88. package/dist/runner.js +41 -14
  89. package/dist/runner.js.map +1 -1
  90. package/dist/shared/approval-policy.d.ts +23 -13
  91. package/dist/shared/approval-policy.js +47 -14
  92. package/dist/shared/approval-policy.js.map +1 -1
  93. package/dist/shared/caller-identity.d.ts +4 -5
  94. package/dist/shared/caller-identity.js +4 -5
  95. package/dist/shared/caller-identity.js.map +1 -1
  96. package/dist/shared/channel-attachment.js +3 -2
  97. package/dist/shared/channel-attachment.js.map +1 -1
  98. package/dist/shared/connect-backfill.d.ts +2 -1
  99. package/dist/shared/connect-backfill.js +2 -2
  100. package/dist/shared/connect-backfill.js.map +1 -1
  101. package/dist/shared/mcp-resolver.d.ts +10 -1
  102. package/dist/shared/mcp-resolver.js +11 -2
  103. package/dist/shared/mcp-resolver.js.map +1 -1
  104. package/dist/shared/memory-attachment.js +3 -2
  105. package/dist/shared/memory-attachment.js.map +1 -1
  106. package/dist/shared/model-error.d.ts +25 -3
  107. package/dist/shared/model-error.js +78 -10
  108. package/dist/shared/model-error.js.map +1 -1
  109. package/dist/shared/platform-server-address.d.ts +71 -0
  110. package/dist/shared/platform-server-address.js +103 -0
  111. package/dist/shared/platform-server-address.js.map +1 -0
  112. package/dist/shared/prompt-sections.d.ts +6 -28
  113. package/dist/shared/prompt-sections.js +6 -35
  114. package/dist/shared/prompt-sections.js.map +1 -1
  115. package/dist/shared/skill-resolver.d.ts +2 -3
  116. package/dist/shared/skill-resolver.js +2 -3
  117. package/dist/shared/skill-resolver.js.map +1 -1
  118. package/dist/shared/synthesized-attachment.d.ts +0 -5
  119. package/dist/shared/synthesized-attachment.js +0 -12
  120. package/dist/shared/synthesized-attachment.js.map +1 -1
  121. package/dist/shared/tool-rounds.d.ts +51 -23
  122. package/dist/shared/tool-rounds.js +67 -24
  123. package/dist/shared/tool-rounds.js.map +1 -1
  124. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  125. package/dist/shared/workspace/platform-dir.js +18 -1
  126. package/dist/shared/workspace/platform-dir.js.map +1 -1
  127. package/dist/shared/workspace/sources/git.js +13 -6
  128. package/dist/shared/workspace/sources/git.js.map +1 -1
  129. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  130. package/dist/shared/workspace/stigmer-link.js +91 -29
  131. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  132. package/dist/tools/url-guard.d.ts +5 -1
  133. package/dist/tools/url-guard.js +9 -2
  134. package/dist/tools/url-guard.js.map +1 -1
  135. package/dist/worker.d.ts +11 -2
  136. package/dist/worker.js +1 -1
  137. package/dist/worker.js.map +1 -1
  138. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  139. package/dist/workflow-engine/tasks/human-input.js +79 -37
  140. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +14 -4
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  144. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  145. package/dist/workflows/engine-core.d.ts +25 -0
  146. package/dist/workflows/engine-core.js.map +1 -1
  147. package/dist/workflows/execute-from-execution.d.ts +3 -0
  148. package/dist/workflows/execute-from-execution.js.map +1 -1
  149. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  150. package/dist/workflows/human-input-orchestrator.js +12 -44
  151. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  152. package/package.json +5 -5
  153. package/src/__test-utils__/config-fixture.ts +1 -0
  154. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  155. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  156. package/src/__test-utils__/test-gate.ts +8 -0
  157. package/src/__tests__/exit-backstop.test.ts +91 -0
  158. package/src/__tests__/golden-e2e.test.ts +80 -62
  159. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  160. package/src/__tests__/runner.test.ts +60 -2
  161. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  162. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  163. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  164. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  165. package/src/activities/call-agent.ts +1 -1
  166. package/src/activities/discover-mcp-server.ts +27 -37
  167. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  168. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  169. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  170. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  171. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  172. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  173. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  174. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  175. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  176. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  177. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  178. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  179. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  180. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  181. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  182. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  183. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  184. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  185. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  186. package/src/activities/execute-cursor/translator.ts +15 -32
  187. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  188. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  189. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  190. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  191. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  192. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  193. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  194. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  195. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  196. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  197. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  198. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  199. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  200. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  201. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  202. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  203. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  204. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  205. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  206. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  207. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  208. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  209. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  210. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  211. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  212. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  213. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  214. package/src/activities/execute-deep-agent/translator.ts +5 -4
  215. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  216. package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
  217. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  218. package/src/activities/execute-deep-agent/turn.ts +16 -7
  219. package/src/activities/workflow-event-activities.ts +2 -0
  220. package/src/config.ts +11 -0
  221. package/src/exit-backstop.ts +97 -0
  222. package/src/harness/README.md +5 -5
  223. package/src/harness/terminal-table.ts +13 -13
  224. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  225. package/src/harness/transcript/builder.ts +53 -30
  226. package/src/harness/transcript/events.ts +22 -27
  227. package/src/harness/turn-context.ts +5 -3
  228. package/src/main.ts +9 -5
  229. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  230. package/src/middleware/cost-advisory.ts +6 -3
  231. package/src/middleware/execution-budget.ts +60 -34
  232. package/src/middleware/index.ts +10 -7
  233. package/src/middleware/types.ts +6 -1
  234. package/src/runner-manager.ts +3 -2
  235. package/src/runner.ts +57 -15
  236. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  237. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  238. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  239. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  240. package/src/shared/__tests__/model-error.test.ts +71 -3
  241. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  242. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  243. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  244. package/src/shared/approval-policy.ts +57 -14
  245. package/src/shared/caller-identity.ts +4 -5
  246. package/src/shared/channel-attachment.ts +2 -2
  247. package/src/shared/connect-backfill.ts +5 -1
  248. package/src/shared/mcp-resolver.ts +11 -1
  249. package/src/shared/memory-attachment.ts +2 -2
  250. package/src/shared/model-error.ts +87 -9
  251. package/src/shared/platform-server-address.ts +123 -0
  252. package/src/shared/prompt-sections.ts +6 -55
  253. package/src/shared/skill-resolver.ts +2 -3
  254. package/src/shared/synthesized-attachment.ts +0 -12
  255. package/src/shared/tool-rounds.ts +70 -24
  256. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  257. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  258. package/src/shared/workspace/platform-dir.ts +19 -1
  259. package/src/shared/workspace/sources/git.ts +14 -8
  260. package/src/shared/workspace/stigmer-link.ts +99 -29
  261. package/src/tools/__tests__/url-guard.test.ts +19 -1
  262. package/src/tools/url-guard.ts +9 -1
  263. package/src/worker.ts +12 -2
  264. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  265. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  266. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  267. package/src/workflow-engine/tasks/human-input.ts +92 -42
  268. package/src/workflow-engine/types.ts +16 -4
  269. package/src/workflows/call-agent-orchestrator.ts +7 -0
  270. package/src/workflows/engine-core.ts +18 -0
  271. package/src/workflows/execute-from-execution.ts +2 -1
  272. package/src/workflows/human-input-orchestrator.ts +15 -56
  273. package/dist/shared/skill-relevance.d.ts +0 -65
  274. package/dist/shared/skill-relevance.js +0 -175
  275. package/dist/shared/skill-relevance.js.map +0 -1
  276. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  277. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  278. package/src/shared/skill-relevance.ts +0 -222
package/src/runner.ts CHANGED
@@ -18,6 +18,7 @@ import { join } from "node:path";
18
18
  import { homedir, tmpdir } from "node:os";
19
19
  import type { Config, TokenRef } from "./config.js";
20
20
  import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
21
+ import type { NativeConnection } from "@temporalio/worker";
21
22
  import type { WorkerActivities } from "./worker.js";
22
23
  import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
23
24
  import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
@@ -348,7 +349,7 @@ export async function createStigmerRunner(
348
349
  const shutdownController = registerWorkerShutdownSignal(config.taskQueue);
349
350
 
350
351
  const { startWorker } = await import("./worker.js");
351
- const worker = await startWorker({ config, activities, payloadCodecs });
352
+ const { worker, connection } = await startWorker({ config, activities, payloadCodecs });
352
353
  markBoot("worker_created");
353
354
 
354
355
  return {
@@ -363,18 +364,10 @@ export async function createStigmerRunner(
363
364
  emitRunnerBootTiming({ task_queue: config.taskQueue, mode: config.mode });
364
365
  await worker.run();
365
366
  console.log("Worker stopped");
366
- // The worker has drained — every harness releases what it still holds
367
- // (the Cursor harness: its parked session agents, whose executor leases
368
- // dispose with the process, #215). Logged, never thrown: the drain has
369
- // already happened and the process is exiting, so a teardown failure
370
- // must not mask a clean stop.
371
- try {
372
- await shutdownHarnesses(adaptersOf(HARNESS_ADAPTERS));
373
- } catch (err) {
374
- console.error(
375
- `[runner] Harness shutdown failed after the worker drained (continuing): ${err instanceof Error ? err.message : String(err)}`,
376
- );
377
- }
367
+ await releaseAfterDrain({
368
+ shutdownHarnesses: () => shutdownHarnesses(adaptersOf(HARNESS_ADAPTERS)),
369
+ connection,
370
+ });
378
371
  },
379
372
  shutdown() {
380
373
  tokenRenewal?.stop();
@@ -404,6 +397,54 @@ function validateOptions(options: StigmerRunnerOptions): void {
404
397
  // localhost when no token is present.
405
398
  }
406
399
 
400
+ /** What a drained static worker still holds, released by {@link releaseAfterDrain}. */
401
+ export interface DrainedWorkerResources {
402
+ /** Releases what every harness still holds (the registry's `shutdownHarnesses` over the table). */
403
+ readonly shutdownHarnesses: () => Promise<void>;
404
+ /** The Temporal connection the drained worker polled on; this runner opened it, so it closes it. */
405
+ readonly connection: Pick<NativeConnection, "close">;
406
+ }
407
+
408
+ /**
409
+ * The static runner's teardown once `worker.run()` has resolved, in the
410
+ * manager's order (runner-manager.ts `shutdown`): harnesses, then the
411
+ * Temporal connection.
412
+ *
413
+ * - Harnesses first: they release what they still hold (the Cursor harness:
414
+ * its parked session agents, whose executor leases dispose with the
415
+ * process, #215) over their own channels, never the Temporal one.
416
+ * - The connection last, and only after the drain: the SDK refuses to close
417
+ * a connection a worker still holds, and `run()` releases it on its way
418
+ * out.
419
+ *
420
+ * Each step is logged, never thrown: the drain has already happened and the
421
+ * process is exiting, so a teardown failure must not mask a clean stop, and
422
+ * one step failing does not skip the next.
423
+ */
424
+ export async function releaseAfterDrain(
425
+ resources: DrainedWorkerResources,
426
+ logError: (line: string) => void = (line) => console.error(line),
427
+ ): Promise<void> {
428
+ try {
429
+ await resources.shutdownHarnesses();
430
+ } catch (err) {
431
+ logError(
432
+ `[runner] Harness shutdown failed after the worker drained (continuing): ${errorText(err)}`,
433
+ );
434
+ }
435
+ try {
436
+ await resources.connection.close();
437
+ } catch (err) {
438
+ logError(
439
+ `[runner] Temporal connection close failed after the worker drained (continuing): ${errorText(err)}`,
440
+ );
441
+ }
442
+ }
443
+
444
+ function errorText(err: unknown): string {
445
+ return err instanceof Error ? err.message : String(err);
446
+ }
447
+
407
448
  /**
408
449
  * The runtime `Config` for the static root. The refs are the root's: it
409
450
  * creates them before mapping and writes them on every rotation (the
@@ -437,9 +478,10 @@ export function mapOptionsToConfig(
437
478
  stigmerTokenRef: tokenRef,
438
479
  stigmerRunnerTokenRef: runnerTokenRef,
439
480
  proxyTokenRef: tokenRef,
440
- // Env-only like the env-loaded path (loadConfig): the bridge endpoint
441
- // is deployment topology, not per-session state.
481
+ // Env-only like the env-loaded path (loadConfig): the bridge and public
482
+ // endpoints are deployment topology, not per-session state.
442
483
  mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
484
+ mcpPublicEndpoint: process.env.STIGMER_MCP_PUBLIC_ENDPOINT || null,
443
485
  cursorApiKey: proxyActive
444
486
  ? (options.cursorApiKey ?? "proxy-managed")
445
487
  : (options.cursorApiKey ?? ""),
@@ -11,6 +11,7 @@ import {
11
11
  mergeApprovalPolicies,
12
12
  lookupMcpToolPolicy,
13
13
  resolveApprovalMessage,
14
+ resolveBuiltInApprovalMessage,
14
15
  deriveActiveLeases,
15
16
  resolveApprovalProvenance,
16
17
  resolveToolApproval,
@@ -551,6 +552,14 @@ describe("resolveToolApproval — THE gate decision, shared by the gate and the
551
552
  });
552
553
  });
553
554
 
555
+ it("a gated native file write waits with the file named on its card (#1112)", () => {
556
+ expect(resolveToolApproval("write_file", "", { file_path: "/workspace/src/app.ts", content: "x" }, new Map(), NO_CATEGORIES)).toEqual({
557
+ requiresApproval: true,
558
+ message: "Write file: /workspace/src/app.ts",
559
+ source: "builtin_category",
560
+ });
561
+ });
562
+
554
563
  it("a mutating built-in whose category is leased runs, source approval_lease", () => {
555
564
  expect(resolveToolApproval("execute", "", { command: "ls" }, new Map(), new Set<ToolApprovalCategory>(["shell"]))).toEqual({
556
565
  requiresApproval: false,
@@ -565,6 +574,38 @@ describe("resolveToolApproval — THE gate decision, shared by the gate and the
565
574
  });
566
575
  });
567
576
 
577
+ describe("resolveBuiltInApprovalMessage — the one card wording for a gated built-in, either harness", () => {
578
+ it("names the file a native write or edit touches: deepagents' file tools send file_path (#1112)", () => {
579
+ expect(resolveBuiltInApprovalMessage("write_file", { file_path: "/workspace/src/app.ts", content: "x" })).toBe("Write file: /workspace/src/app.ts");
580
+ expect(resolveBuiltInApprovalMessage("edit_file", { file_path: "/workspace/README.md", old_string: "a", new_string: "b" })).toBe("Write file: /workspace/README.md");
581
+ });
582
+
583
+ it("names the file from path in the Cursor stream's taxonomy, and from file_path in its hook's", () => {
584
+ expect(resolveBuiltInApprovalMessage("edit", { path: "src/app.ts" })).toBe("Write file: src/app.ts");
585
+ expect(resolveBuiltInApprovalMessage("Write", { file_path: "src/app.ts" })).toBe("Write file: src/app.ts");
586
+ expect(resolveBuiltInApprovalMessage("Delete", { file_path: "old.txt" })).toBe("Delete: old.txt");
587
+ expect(resolveBuiltInApprovalMessage("delete", { path: "old.txt" })).toBe("Delete: old.txt");
588
+ });
589
+
590
+ it("prefers path when a call carries both names", () => {
591
+ expect(resolveBuiltInApprovalMessage("write_file", { path: "a.txt", file_path: "b.txt" })).toBe("Write file: a.txt");
592
+ });
593
+
594
+ it("still reads <unknown> when a file call names no file at all", () => {
595
+ expect(resolveBuiltInApprovalMessage("write_file", { content: "x" })).toBe("Write file: <unknown>");
596
+ });
597
+
598
+ it("words a shell card from the command, in either taxonomy", () => {
599
+ expect(resolveBuiltInApprovalMessage("execute", { command: "rm -rf build" })).toBe("Run command: rm -rf build");
600
+ expect(resolveBuiltInApprovalMessage("Shell", { command: "ls" })).toBe("Run command: ls");
601
+ });
602
+
603
+ it("answers undefined for a built-in that is not gated", () => {
604
+ expect(resolveBuiltInApprovalMessage("read_file", { file_path: "/x" })).toBeUndefined();
605
+ expect(resolveBuiltInApprovalMessage("Read", { file_path: "/x" })).toBeUndefined();
606
+ });
607
+ });
608
+
568
609
  describe("toProtoPolicySource", () => {
569
610
  it("maps undefined (no governing layer) to UNSPECIFIED", () => {
570
611
  expect(toProtoPolicySource(undefined)).toBe(ApprovalPolicySource.UNSPECIFIED);
@@ -647,6 +647,7 @@ describe("loadArtifactStorageConfig", () => {
647
647
  temporalConnection: {},
648
648
  stigmerBackendEndpoint: "http://localhost:7234",
649
649
  mcpBridgeEndpoint: null,
650
+ mcpPublicEndpoint: null,
650
651
  cursorApiKey: "",
651
652
  workspaceRootDir: "/tmp",
652
653
  maxConcurrentActivities: 5,
@@ -5,6 +5,10 @@ import {
5
5
  extractRuntimeEnvForServer,
6
6
  } from "../connect-backfill.js";
7
7
  import type { ResolvedMcpServer } from "../mcp-resolver.js";
8
+ import { testConfig } from "../../__test-utils__/config-fixture.js";
9
+
10
+ /** The runner endpoints resolution fills STIGMER_SERVER_ADDRESS from. */
11
+ const PLATFORM_ENDPOINTS = testConfig();
8
12
 
9
13
  function makeServer(overrides: Partial<ResolvedMcpServer> = {}): ResolvedMcpServer {
10
14
  return {
@@ -125,7 +129,7 @@ describe("backfillMcpServersIfNeeded", () => {
125
129
  const client = makeMockClient();
126
130
 
127
131
  const result = await backfillMcpServersIfNeeded(
128
- client, servers, [], {}, "org", "stdio-allowed",
132
+ client, servers, [], {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
129
133
  );
130
134
 
131
135
  expect(result).toBe(servers);
@@ -138,7 +142,7 @@ describe("backfillMcpServersIfNeeded", () => {
138
142
  const client = makeMockClient();
139
143
 
140
144
  const result = await backfillMcpServersIfNeeded(
141
- client, servers, [], {}, "org", "stdio-allowed",
145
+ client, servers, [], {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
142
146
  );
143
147
 
144
148
  expect(result).toBe(servers);
@@ -156,7 +160,7 @@ describe("backfillMcpServersIfNeeded", () => {
156
160
  });
157
161
 
158
162
  const result = await backfillMcpServersIfNeeded(
159
- client, servers, usages, {}, "org", "stdio-allowed",
163
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
160
164
  );
161
165
 
162
166
  expect(client.getMcpServerByReference).toHaveBeenCalledOnce();
@@ -182,7 +186,7 @@ describe("backfillMcpServersIfNeeded", () => {
182
186
  });
183
187
 
184
188
  const result = await backfillMcpServersIfNeeded(
185
- client, servers, usages, {}, "org", "stdio-allowed",
189
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
186
190
  );
187
191
 
188
192
  expect(client.connectMcpServer).toHaveBeenCalledOnce();
@@ -207,7 +211,7 @@ describe("backfillMcpServersIfNeeded", () => {
207
211
  });
208
212
 
209
213
  await backfillMcpServersIfNeeded(
210
- client, servers, usages, mergedEnv, "org", "stdio-allowed",
214
+ client, servers, usages, mergedEnv, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
211
215
  );
212
216
 
213
217
  expect(client.connectMcpServer).toHaveBeenCalledWith(
@@ -225,7 +229,7 @@ describe("backfillMcpServersIfNeeded", () => {
225
229
  });
226
230
 
227
231
  const result = await backfillMcpServersIfNeeded(
228
- client, servers, usages, {}, "org", "stdio-allowed",
232
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
229
233
  );
230
234
 
231
235
  expect(result).toBe(servers);
@@ -243,7 +247,7 @@ describe("backfillMcpServersIfNeeded", () => {
243
247
  });
244
248
 
245
249
  const result = await backfillMcpServersIfNeeded(
246
- client, servers, usages, {}, "org", "stdio-allowed",
250
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
247
251
  );
248
252
 
249
253
  expect(result).toBe(servers);
@@ -262,7 +266,7 @@ describe("backfillMcpServersIfNeeded", () => {
262
266
  });
263
267
 
264
268
  await backfillMcpServersIfNeeded(
265
- client, servers, usages, {}, "org", "stdio-allowed", onHeartbeat,
269
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS, onHeartbeat,
266
270
  );
267
271
 
268
272
  expect(onHeartbeat).toHaveBeenCalledTimes(2);
@@ -275,7 +279,7 @@ describe("backfillMcpServersIfNeeded", () => {
275
279
  const client = makeMockClient();
276
280
 
277
281
  const result = await backfillMcpServersIfNeeded(
278
- client, servers, [], {}, "org", "stdio-allowed",
282
+ client, servers, [], {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
279
283
  );
280
284
 
281
285
  expect(client.getMcpServerByReference).not.toHaveBeenCalled();
@@ -295,7 +299,7 @@ describe("backfillMcpServersIfNeeded", () => {
295
299
  });
296
300
 
297
301
  const result = await backfillMcpServersIfNeeded(
298
- client, servers, usages, {}, "org", "stdio-allowed",
302
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
299
303
  );
300
304
 
301
305
  expect(client.connectMcpServer).not.toHaveBeenCalled();
@@ -338,7 +342,7 @@ describe("backfillMcpServersIfNeeded", () => {
338
342
  });
339
343
 
340
344
  const result = await backfillMcpServersIfNeeded(
341
- client, servers, usages, {}, "org", "stdio-allowed",
345
+ client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
342
346
  );
343
347
 
344
348
  expect(callCount).toBe(2);
@@ -1,6 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { mcpServerToResolved, mergeMcpServerUsages, resolveMcpServers } from "../mcp-resolver.js";
3
3
  import { McpTransportError } from "../mcp-transport-guard.js";
4
+ import { testConfig } from "../../__test-utils__/config-fixture.js";
5
+
6
+ /** The runner endpoints resolution fills STIGMER_SERVER_ADDRESS from. */
7
+ const PLATFORM_ENDPOINTS = testConfig();
4
8
 
5
9
  function makeUsage(
6
10
  slug: string,
@@ -60,7 +64,7 @@ describe("resolveMcpServers — transport guard integration", () => {
60
64
  const client = clientReturning({ filesystem: stdioMcpServer("filesystem") });
61
65
 
62
66
  await expect(
63
- resolveMcpServers(client, [makeUsage("filesystem")], {}, "stdio-forbidden"),
67
+ resolveMcpServers(client, [makeUsage("filesystem")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS),
64
68
  ).rejects.toThrow(McpTransportError);
65
69
  });
66
70
 
@@ -75,7 +79,7 @@ describe("resolveMcpServers — transport guard integration", () => {
75
79
  client,
76
80
  [makeUsage("github"), makeUsage("filesystem")],
77
81
  {},
78
- "stdio-forbidden",
82
+ "stdio-forbidden", PLATFORM_ENDPOINTS,
79
83
  ),
80
84
  ).rejects.toThrow(McpTransportError);
81
85
  });
@@ -84,7 +88,7 @@ describe("resolveMcpServers — transport guard integration", () => {
84
88
  const client = clientReturning({ github: httpMcpServer("github") });
85
89
 
86
90
  const result = await resolveMcpServers(
87
- client, [makeUsage("github")], {}, "stdio-forbidden",
91
+ client, [makeUsage("github")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS,
88
92
  );
89
93
 
90
94
  expect(result.resolvedServers).toHaveLength(1);
@@ -95,7 +99,7 @@ describe("resolveMcpServers — transport guard integration", () => {
95
99
  const client = clientReturning({ filesystem: stdioMcpServer("filesystem") });
96
100
 
97
101
  const result = await resolveMcpServers(
98
- client, [makeUsage("filesystem")], {}, "stdio-allowed",
102
+ client, [makeUsage("filesystem")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
99
103
  );
100
104
 
101
105
  expect(result.resolvedServers).toHaveLength(1);
@@ -106,7 +110,7 @@ describe("resolveMcpServers — transport guard integration", () => {
106
110
  const client = clientReturning({});
107
111
 
108
112
  const result = await resolveMcpServers(
109
- client, [makeUsage("ghost")], {}, "stdio-forbidden",
113
+ client, [makeUsage("ghost")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS,
110
114
  );
111
115
 
112
116
  expect(result.resolvedServers).toHaveLength(0);
@@ -123,7 +127,7 @@ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
123
127
  const client = clientReturning({ github: httpMcpServer("github") });
124
128
 
125
129
  const result = await resolveMcpServers(
126
- client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed",
130
+ client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
127
131
  );
128
132
 
129
133
  expect(result.resolvedServers[0].enabledTools).toEqual(["create_pr"]);
@@ -135,7 +139,7 @@ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
135
139
  });
136
140
 
137
141
  const result = await resolveMcpServers(
138
- client, [makeUsage("github")], {}, "stdio-allowed",
142
+ client, [makeUsage("github")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
139
143
  );
140
144
 
141
145
  expect(result.resolvedServers[0].enabledTools).toEqual(["search_code"]);
@@ -145,7 +149,7 @@ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
145
149
  const client = clientReturning({ github: httpMcpServer("github") });
146
150
 
147
151
  const result = await resolveMcpServers(
148
- client, [makeUsage("github")], {}, "stdio-allowed",
152
+ client, [makeUsage("github")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
149
153
  );
150
154
 
151
155
  expect(result.resolvedServers[0].enabledTools).toBeUndefined();
@@ -179,7 +183,7 @@ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)",
179
183
  makeUsage("slack"),
180
184
  ],
181
185
  {},
182
- "stdio-allowed",
186
+ "stdio-allowed", PLATFORM_ENDPOINTS,
183
187
  );
184
188
 
185
189
  const bySlug = new Map(result.resolvedServers.map((s) => [s.slug, s]));
@@ -208,7 +212,7 @@ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)",
208
212
  [makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: false }])],
209
213
  );
210
214
 
211
- const result = await resolveMcpServers(client, merged, {}, "stdio-allowed");
215
+ const result = await resolveMcpServers(client, merged, {}, "stdio-allowed", PLATFORM_ENDPOINTS);
212
216
 
213
217
  expect(result.resolvedServers[0].toolApprovalOverrides).toEqual([
214
218
  { toolName: "push", requiresApproval: false },
@@ -216,6 +220,64 @@ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)",
216
220
  });
217
221
  });
218
222
 
223
+ describe("resolveMcpServers — the platform STIGMER_SERVER_ADDRESS (stigmer/stigmer#1433)", () => {
224
+ function declaringAddress(server: any, headers: Record<string, string> = {}) {
225
+ server.spec.env = { STIGMER_SERVER_ADDRESS: {} };
226
+ if (server.spec.serverType.case === "http") server.spec.serverType.value.headers = headers;
227
+ return server;
228
+ }
229
+
230
+ it("fills a stdio server's missing address from the runner's backend endpoint", async () => {
231
+ const client = clientReturning({ stigmer: declaringAddress(stdioMcpServer("stigmer")) });
232
+
233
+ const result = await resolveMcpServers(
234
+ client, [makeUsage("stigmer")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
235
+ );
236
+
237
+ // testConfig's backend endpoint is http://127.0.0.1:1.
238
+ expect(result.resolvedServers[0].env).toEqual({ STIGMER_SERVER_ADDRESS: "127.0.0.1:1" });
239
+ });
240
+
241
+ it("keeps the address the execution environment carries", async () => {
242
+ const client = clientReturning({ stigmer: declaringAddress(stdioMcpServer("stigmer")) });
243
+
244
+ const result = await resolveMcpServers(
245
+ client, [makeUsage("stigmer")], { STIGMER_SERVER_ADDRESS: "api.example.com:443" },
246
+ "stdio-allowed", PLATFORM_ENDPOINTS,
247
+ );
248
+
249
+ expect(result.resolvedServers[0].env).toEqual({ STIGMER_SERVER_ADDRESS: "api.example.com:443" });
250
+ });
251
+
252
+ it("templates an http header from the operator's public endpoint", async () => {
253
+ const client = clientReturning({
254
+ remote: declaringAddress(httpMcpServer("remote"), { "X-Stigmer-Server": "${STIGMER_SERVER_ADDRESS}" }),
255
+ });
256
+
257
+ const result = await resolveMcpServers(
258
+ client, [makeUsage("remote")], {}, "stdio-forbidden",
259
+ testConfig({ mcpPublicEndpoint: "https://api.example.com" }),
260
+ );
261
+
262
+ expect(result.resolvedServers[0].headers).toEqual({ "X-Stigmer-Server": "api.example.com:443" });
263
+ });
264
+
265
+ it("never hands an http server the backend endpoint: an unknown address drops it with the named error", async () => {
266
+ const client = clientReturning({
267
+ remote: declaringAddress(httpMcpServer("remote"), { "X-Stigmer-Server": "${STIGMER_SERVER_ADDRESS}" }),
268
+ });
269
+ const errorLog = vi.spyOn(console, "error").mockImplementation(() => {});
270
+
271
+ const result = await resolveMcpServers(
272
+ client, [makeUsage("remote")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS,
273
+ );
274
+
275
+ expect(result.resolvedServers).toEqual([]);
276
+ expect(errorLog).toHaveBeenCalledWith(expect.stringContaining("STIGMER_SERVER_ADDRESS"));
277
+ errorLog.mockRestore();
278
+ });
279
+ });
280
+
219
281
  describe("mergeMcpServerUsages — session-wins-per-slug (shared by both harnesses)", () => {
220
282
  it("session usage replaces the agent's WHOLE usage for the same slug, including enabled_tools", () => {
221
283
  const merged = mergeMcpServerUsages(
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { describe, it, expect, vi, afterEach } from "vitest";
15
15
  import {
16
+ ORGANIZATION_PROVIDER_KEY_SENTINEL,
16
17
  PLATFORM_CAPACITY_SENTINEL,
17
18
  classifyModelCallError,
18
19
  describeExecutionError,
@@ -29,6 +30,20 @@ function middlewareWrap(inner: Error): Error {
29
30
  return wrapped;
30
31
  }
31
32
 
33
+ /**
34
+ * Wrap as a native turn does: LangChain adds one MiddlewareError per
35
+ * middleware implementing `wrapModelCall`, and the production stack has
36
+ * more than ten of them (stigmer/stigmer#1450).
37
+ */
38
+ function wrapLikeMiddlewareStack(inner: Error, layers: number): Error {
39
+ let wrapped = inner;
40
+ for (let layer = 0; layer < layers; layer++) wrapped = middlewareWrap(wrapped);
41
+ return wrapped;
42
+ }
43
+
44
+ /** A middleware stack deeper than any native turn's today. */
45
+ const DEEP_STACK = 30;
46
+
32
47
  /** Mimic a provider SDK APIError: message + numeric `.status`. */
33
48
  function sdkError(status: number, message: string): Error {
34
49
  return Object.assign(new Error(message), { status });
@@ -62,13 +77,17 @@ describe("unwrapModelError", () => {
62
77
  expect(unwrapModelError(wrapped)).toBe(root);
63
78
  });
64
79
 
65
- it("survives a pathological cause cycle via the depth cap", () => {
80
+ it("walks to the root however deep the middleware stack wraps it", () => {
81
+ const root = sdkError(401, "401 authentication_error");
82
+ expect(unwrapModelError(wrapLikeMiddlewareStack(root, DEEP_STACK))).toBe(root);
83
+ });
84
+
85
+ it("ends a cause cycle at the first error seen twice", () => {
66
86
  const a = new Error("a");
67
87
  const b = new Error("b");
68
88
  a.cause = b;
69
89
  b.cause = a;
70
- // Any chain member is acceptable; the point is it terminates.
71
- expect(unwrapModelError(a)).toBeInstanceOf(Error);
90
+ expect(unwrapModelError(a)).toBe(b);
72
91
  });
73
92
 
74
93
  it("passes non-Error values through", () => {
@@ -538,3 +557,52 @@ describe("describeExecutionError", () => {
538
557
  expect(errorMessage).toContain("platform-side issue");
539
558
  });
540
559
  });
560
+
561
+ describe("classifyModelCallError on the organization's own provider key", () => {
562
+ const proxied = { proxyMode: true, provider: "anthropic" as const, modelId: "claude-sonnet-4-6" };
563
+ const marked = (message: string) => `${message} [code: ${ORGANIZATION_PROVIDER_KEY_SENTINEL}]`;
564
+
565
+ it("words the key's billing exhaustion as the organization's account, never platform capacity", () => {
566
+ const err = middlewareWrap(sdkError(400, marked(ANTHROPIC_BILLING_MESSAGE)));
567
+ const classified = classifyModelCallError(err, proxied);
568
+ expect(classified?.code).toBe("LLM_PROVIDER_BILLING");
569
+ expect(classified?.retryable).toBe(false);
570
+ expect(classified?.message).toContain("organization's own Anthropic key");
571
+ expect(classified?.message).not.toContain(PLATFORM_CAPACITY_SENTINEL);
572
+ });
573
+
574
+ it("words a rejected key as the organization's key, not an expired Stigmer session", () => {
575
+ const err = sdkError(401, marked('401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}'));
576
+ const classified = classifyModelCallError(err, proxied);
577
+ expect(classified?.code).toBe("LLM_AUTHENTICATION_ERROR");
578
+ expect(classified?.message).toContain("rejected your organization's own key");
579
+ expect(classified?.message).not.toContain("Stigmer platform");
580
+ });
581
+
582
+ it("words a rejected key as the organization's through a native turn's middleware stack", () => {
583
+ const rejected = sdkError(401, marked('401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}'));
584
+ const { errorType, errorMessage } = describeExecutionError(wrapLikeMiddlewareStack(rejected, DEEP_STACK), proxied);
585
+ expect(errorType).toBe("LLM_AUTHENTICATION_ERROR");
586
+ expect(errorMessage).toContain("rejected your organization's own key");
587
+ expect(errorMessage).not.toContain("MiddlewareError");
588
+ });
589
+
590
+ it("words a forbidden model as the key's account permission", () => {
591
+ const classified = classifyModelCallError(sdkError(403, marked("403 permission_error")), proxied);
592
+ expect(classified?.code).toBe("LLM_PERMISSION_DENIED");
593
+ expect(classified?.message).toContain("denied your organization's own key");
594
+ });
595
+
596
+ it("classifies the key's other failures as direct mode would", () => {
597
+ const rateLimited = classifyModelCallError(sdkError(429, marked("429 rate_limit_error")), proxied);
598
+ expect(rateLimited?.code).toBe("LLM_RATE_LIMIT");
599
+ const serverError = classifyModelCallError(sdkError(529, marked("529 overloaded_error")), proxied);
600
+ expect(serverError?.code).toBe("LLM_PROVIDER_ERROR");
601
+ expect(serverError?.retryable).toBe(true);
602
+ });
603
+
604
+ it("leaves an unmarked proxied 401 on the platform wording", () => {
605
+ const classified = classifyModelCallError(sdkError(401, "401 unauthorized"), proxied);
606
+ expect(classified?.message).toContain("Stigmer platform rejected");
607
+ });
608
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Unit tests for the platform STIGMER_SERVER_ADDRESS: the one dial-target
3
+ * rule, which endpoint answers for which transport, the fill that never
4
+ * overrides, and that the address is the only key the platform fills: a
5
+ * declared STIGMER_API_KEY is the user's to save (stigmer/stigmer#1446).
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
10
+ import {
11
+ SERVER_ADDRESS_ENV_KEY,
12
+ fillPlatformServerAddress,
13
+ grpcTarget,
14
+ platformServerAddress,
15
+ type PlatformEndpoints,
16
+ } from "../platform-server-address.js";
17
+
18
+ const LOCAL: PlatformEndpoints = {
19
+ stigmerBackendEndpoint: "http://localhost:7234",
20
+ mcpPublicEndpoint: null,
21
+ };
22
+
23
+ const WITH_PUBLIC: PlatformEndpoints = {
24
+ stigmerBackendEndpoint: "http://stigmer-server.internal:80",
25
+ mcpPublicEndpoint: "https://api.example.com",
26
+ };
27
+
28
+ function server(
29
+ transport: "stdio" | "http",
30
+ declared: readonly string[] = [SERVER_ADDRESS_ENV_KEY],
31
+ ): McpServer {
32
+ const env = Object.fromEntries(declared.map((key) => [key, {}]));
33
+ const serverType = transport === "stdio"
34
+ ? { case: "stdio", value: { command: "stigmer", args: ["mcp-server"] } }
35
+ : { case: "http", value: { url: "https://mcp.example.com/mcp", headers: {} } };
36
+ return { metadata: { slug: "stigmer" }, spec: { serverType, env } } as unknown as McpServer;
37
+ }
38
+
39
+ describe("grpcTarget", () => {
40
+ it.each([
41
+ ["http://localhost:7234", "localhost:7234"],
42
+ ["https://api.stigmer.ai", "api.stigmer.ai:443"],
43
+ ["https://api.stigmer.ai:8443", "api.stigmer.ai:8443"],
44
+ ["http://api.local", "api.local:80"],
45
+ ["http://stigmer.lan:80", "stigmer.lan:80"],
46
+ ["http://[::1]:7234", "[::1]:7234"],
47
+ ["http://localhost:7234/", "localhost:7234"],
48
+ ["https://api.stigmer.ai/v1/rpc", "api.stigmer.ai:443"],
49
+ ])("takes an http(s) URL apart with an explicit port: %s -> %s", (input, expected) => {
50
+ expect(grpcTarget(input)).toBe(expected);
51
+ });
52
+
53
+ it.each([
54
+ ["localhost:7234"],
55
+ ["stigmer-server.stigmer-prod.svc.cluster.local:80"],
56
+ ["api.stigmer.ai"],
57
+ ["not a url"],
58
+ ])("passes a value that is not an http(s) URL through: %s", (input) => {
59
+ expect(grpcTarget(input)).toBe(input);
60
+ });
61
+ });
62
+
63
+ describe("platformServerAddress", () => {
64
+ it("answers the operator's public endpoint for every transport", () => {
65
+ expect(platformServerAddress("http", WITH_PUBLIC)).toBe("api.example.com:443");
66
+ expect(platformServerAddress("stdio", WITH_PUBLIC)).toBe("api.example.com:443");
67
+ });
68
+
69
+ it("answers the runner's own backend endpoint for a stdio child", () => {
70
+ expect(platformServerAddress("stdio", LOCAL)).toBe("localhost:7234");
71
+ });
72
+
73
+ it("knows no address for a remote server without a public endpoint", () => {
74
+ expect(platformServerAddress("http", LOCAL)).toBeNull();
75
+ expect(platformServerAddress(undefined, LOCAL)).toBeNull();
76
+ });
77
+ });
78
+
79
+ describe("fillPlatformServerAddress", () => {
80
+ it("fills a declared, missing address for a stdio server", () => {
81
+ const env = { OTHER: "x" };
82
+ const result = fillPlatformServerAddress(server("stdio"), env, LOCAL);
83
+ expect(result).toEqual({ OTHER: "x", [SERVER_ADDRESS_ENV_KEY]: "localhost:7234" });
84
+ expect(env).toEqual({ OTHER: "x" });
85
+ });
86
+
87
+ it("fills a declared, empty address", () => {
88
+ const result = fillPlatformServerAddress(
89
+ server("stdio"),
90
+ { [SERVER_ADDRESS_ENV_KEY]: "" },
91
+ LOCAL,
92
+ );
93
+ expect(result[SERVER_ADDRESS_ENV_KEY]).toBe("localhost:7234");
94
+ });
95
+
96
+ it("never overrides a value already present", () => {
97
+ const env = { [SERVER_ADDRESS_ENV_KEY]: "other.example:7234" };
98
+ expect(fillPlatformServerAddress(server("stdio"), env, WITH_PUBLIC)).toBe(env);
99
+ });
100
+
101
+ it("gives nothing to a server that does not declare the key", () => {
102
+ const env = {};
103
+ expect(fillPlatformServerAddress(server("stdio", ["OTHER"]), env, LOCAL)).toBe(env);
104
+ });
105
+
106
+ it("fills a remote server only from the public endpoint", () => {
107
+ expect(
108
+ fillPlatformServerAddress(server("http"), {}, WITH_PUBLIC)[SERVER_ADDRESS_ENV_KEY],
109
+ ).toBe("api.example.com:443");
110
+ const env = {};
111
+ expect(fillPlatformServerAddress(server("http"), env, LOCAL)).toBe(env);
112
+ });
113
+
114
+ it("fills the address alone, never a declared STIGMER_API_KEY", () => {
115
+ const result = fillPlatformServerAddress(
116
+ server("http", [SERVER_ADDRESS_ENV_KEY, "STIGMER_API_KEY"]),
117
+ {},
118
+ WITH_PUBLIC,
119
+ );
120
+ expect(result).toEqual({ [SERVER_ADDRESS_ENV_KEY]: "api.example.com:443" });
121
+ });
122
+ });