@stigmer/runner 3.22.0 → 3.22.1

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 (135) hide show
  1. package/README.md +1 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/attach-session.js +0 -1
  4. package/dist/activities/attach-session.js.map +1 -1
  5. package/dist/activities/call-agent-status.d.ts +18 -11
  6. package/dist/activities/call-agent-status.js +21 -24
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-agent.d.ts +2 -2
  9. package/dist/activities/call-agent.js +4 -6
  10. package/dist/activities/call-agent.js.map +1 -1
  11. package/dist/activities/call-function.d.ts +4 -2
  12. package/dist/activities/call-function.js +13 -4
  13. package/dist/activities/call-function.js.map +1 -1
  14. package/dist/activities/classify-tool-approvals.js +1 -1
  15. package/dist/activities/classify-tool-approvals.js.map +1 -1
  16. package/dist/activities/discover-mcp-server.js +0 -1
  17. package/dist/activities/discover-mcp-server.js.map +1 -1
  18. package/dist/activities/emit-event.d.ts +6 -1
  19. package/dist/activities/emit-event.js +5 -14
  20. package/dist/activities/emit-event.js.map +1 -1
  21. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -1
  22. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  23. package/dist/activities/execute-cursor/adapter.js +0 -1
  24. package/dist/activities/execute-cursor/adapter.js.map +1 -1
  25. package/dist/activities/execute-cursor/turn-setup.d.ts +1 -1
  26. package/dist/activities/execute-cursor/turn-setup.js +5 -6
  27. package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +1 -1
  29. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/adapter.js +1 -1
  31. package/dist/activities/execute-deep-agent/adapter.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/turn-setup.d.ts +1 -1
  33. package/dist/activities/execute-deep-agent/turn-setup.js +5 -3
  34. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  35. package/dist/activities/generate-session-subject.js +1 -2
  36. package/dist/activities/generate-session-subject.js.map +1 -1
  37. package/dist/activities/hydrate-workflow-execution.js +0 -1
  38. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  39. package/dist/activities/promote-task-output.d.ts +2 -1
  40. package/dist/activities/promote-task-output.js +10 -14
  41. package/dist/activities/promote-task-output.js.map +1 -1
  42. package/dist/activities/workflow-event-activities.d.ts +9 -7
  43. package/dist/activities/workflow-event-activities.js +14 -18
  44. package/dist/activities/workflow-event-activities.js.map +1 -1
  45. package/dist/client/stigmer-client.d.ts +13 -13
  46. package/dist/client/stigmer-client.js +4 -7
  47. package/dist/client/stigmer-client.js.map +1 -1
  48. package/dist/config.d.ts +40 -20
  49. package/dist/config.js +25 -9
  50. package/dist/config.js.map +1 -1
  51. package/dist/harness/registry.js +0 -1
  52. package/dist/harness/registry.js.map +1 -1
  53. package/dist/harness/turn-context.js +2 -4
  54. package/dist/harness/turn-context.js.map +1 -1
  55. package/dist/main.js +13 -6
  56. package/dist/main.js.map +1 -1
  57. package/dist/runner-manager.d.ts +8 -6
  58. package/dist/runner-manager.js +20 -10
  59. package/dist/runner-manager.js.map +1 -1
  60. package/dist/runner-token-coordinator.d.ts +8 -3
  61. package/dist/runner-token-coordinator.js +8 -3
  62. package/dist/runner-token-coordinator.js.map +1 -1
  63. package/dist/runner.d.ts +10 -2
  64. package/dist/runner.js +31 -25
  65. package/dist/runner.js.map +1 -1
  66. package/dist/shared/artifact-storage.d.ts +6 -6
  67. package/dist/shared/artifact-storage.js +5 -12
  68. package/dist/shared/artifact-storage.js.map +1 -1
  69. package/dist/shared/checkpointer/factory.js +5 -1
  70. package/dist/shared/checkpointer/factory.js.map +1 -1
  71. package/dist/shared/checkpointer/http-saver.d.ts +13 -2
  72. package/dist/shared/checkpointer/http-saver.js +17 -5
  73. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  74. package/dist/shared/checkpointer/types.d.ts +10 -1
  75. package/dist/shared/checkpointer/types.js +16 -0
  76. package/dist/shared/checkpointer/types.js.map +1 -1
  77. package/dist/shared/extract-structured-output.js +1 -1
  78. package/dist/shared/extract-structured-output.js.map +1 -1
  79. package/package.json +5 -5
  80. package/src/__test-utils__/config-fixture.ts +2 -2
  81. package/src/__test-utils__/module-specifiers.ts +45 -3
  82. package/src/__tests__/config-boundary.test.ts +135 -0
  83. package/src/__tests__/config.test.ts +7 -16
  84. package/src/__tests__/runner-manager.test.ts +28 -6
  85. package/src/__tests__/runner.test.ts +28 -4
  86. package/src/activities/__tests__/attach-session.test.ts +1 -1
  87. package/src/activities/__tests__/call-agent-contracts.test.ts +4 -7
  88. package/src/activities/__tests__/call-agent-status.test.ts +17 -16
  89. package/src/activities/__tests__/call-agent.test.ts +28 -7
  90. package/src/activities/__tests__/call-function.test.ts +5 -4
  91. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -1
  92. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -1
  93. package/src/activities/__tests__/generate-session-subject-wire.test.ts +1 -1
  94. package/src/activities/__tests__/workflow-event-activities.test.ts +21 -23
  95. package/src/activities/attach-session.ts +0 -1
  96. package/src/activities/call-agent-status.ts +28 -20
  97. package/src/activities/call-agent.ts +5 -6
  98. package/src/activities/call-function.ts +15 -3
  99. package/src/activities/classify-tool-approvals.ts +1 -1
  100. package/src/activities/discover-mcp-server.ts +0 -1
  101. package/src/activities/emit-event.ts +7 -16
  102. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -1
  103. package/src/activities/execute-cursor/adapter.ts +0 -1
  104. package/src/activities/execute-cursor/turn-setup.ts +6 -7
  105. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +1 -1
  106. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +2 -2
  107. package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
  108. package/src/activities/execute-deep-agent/adapter.ts +1 -1
  109. package/src/activities/execute-deep-agent/turn-setup.ts +6 -4
  110. package/src/activities/generate-session-subject.ts +1 -2
  111. package/src/activities/hydrate-workflow-execution.ts +0 -1
  112. package/src/activities/promote-task-output.ts +11 -15
  113. package/src/activities/workflow-event-activities.ts +17 -18
  114. package/src/client/__tests__/stigmer-client.test.ts +8 -19
  115. package/src/client/stigmer-client.ts +18 -20
  116. package/src/config.ts +51 -23
  117. package/src/harness/registry.ts +0 -1
  118. package/src/harness/turn-context.ts +2 -4
  119. package/src/main.ts +13 -6
  120. package/src/runner-manager.ts +25 -15
  121. package/src/runner-token-coordinator.ts +8 -3
  122. package/src/runner.ts +39 -30
  123. package/src/shared/__tests__/artifact-storage-extended.test.ts +9 -9
  124. package/src/shared/__tests__/artifact-storage.test.ts +33 -30
  125. package/src/shared/__tests__/extract-structured-output.test.ts +2 -2
  126. package/src/shared/artifact-storage.ts +12 -17
  127. package/src/shared/checkpointer/__tests__/factory.test.ts +23 -3
  128. package/src/shared/checkpointer/__tests__/http-saver.test.ts +25 -2
  129. package/src/shared/checkpointer/factory.ts +5 -1
  130. package/src/shared/checkpointer/http-saver.ts +20 -6
  131. package/src/shared/checkpointer/types.ts +12 -1
  132. package/src/shared/extract-structured-output.ts +1 -1
  133. package/src/shared/filereview/__tests__/cas-substrate.test.ts +1 -1
  134. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +1 -1
  135. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +32 -54
package/src/runner.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  import { mkdirSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { homedir, tmpdir } from "node:os";
19
- import type { Config } from "./config.js";
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
21
  import type { WorkerActivities } from "./worker.js";
22
22
  import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
@@ -127,17 +127,19 @@ export interface StigmerRunner {
127
127
  * Wire up self-renewal for a static cloud sandbox's control-plane credential
128
128
  * (see sandbox-token-renewal.ts for the model). The applied token reaches
129
129
  * every consumer through the ONE ref: activity gRPC clients read
130
- * {@code tokenRef} per request, per-call sites (call-llm, registry-endpoint
131
- * headers) resolve it through the runner credential store (which replaced
132
- * the process.env.STIGMER_TOKEN channel, #508), artifact storage resolves
133
- * the ref per call, and the two Cursor SDK interceptors read the same ref per
134
- * request as {@code Config.proxyTokenRef} — a static runner has no
130
+ * {@code tokenRef} per request, the artifact store and the checkpoint saver
131
+ * read it per request, the per-turn model clients read it at build, per-call
132
+ * sites (call-llm, registry-endpoint headers) resolve it through the runner
133
+ * credential store (which replaced the process.env.STIGMER_TOKEN channel,
134
+ * #508), and the two Cursor SDK interceptors read the same ref per request
135
+ * as {@code Config.proxyTokenRef} — a static runner has no
135
136
  * {@code RunnerTokenCoordinator} minting a separate proxy credential, so its
136
137
  * x-stigmer-auth IS this token and one write here rotates it everywhere.
138
+ * Nothing holds the value: `Config` carries the ref alone (config.ts header).
137
139
  */
138
140
  async function startStaticSandboxTokenRenewal(
139
141
  config: Config,
140
- tokenRef: { current: string | null },
142
+ tokenRef: TokenRef,
141
143
  ): Promise<{ stop(): void } | null> {
142
144
  const { isRenewableSandboxToken, startSandboxTokenRenewal } = await import(
143
145
  "./sandbox-token-renewal.js"
@@ -201,20 +203,19 @@ export async function createStigmerRunner(
201
203
  // for in-process embedders.
202
204
  captureRunnerSecrets();
203
205
 
204
- const baseConfig = mapOptionsToConfig(options);
205
-
206
- assertLlmBackendsPreflight(baseConfig.proxyEndpoint);
207
-
208
206
  // The control-plane credential lives in a shared mutable ref (as in manager
209
207
  // mode) so the sandbox-token renewal below can rotate it in-process:
210
208
  // activity clients read the ref per request instead of pinning the boot
211
- // token for the pod's whole life. It is ALSO this root's proxy credential
212
- // (Config.proxyTokenRef): a static host provides the proxy token itself and
213
- // mints no separate one, so the interceptors read this same ref per request.
214
- const tokenRef = { current: baseConfig.stigmerToken };
209
+ // token for the pod's whole life. This root owns the writable object; the
210
+ // config carries it read-only (mapOptionsToConfig binds it as both the
211
+ // control-plane and the proxy credential — see there).
212
+ const tokenRef: TokenRef = { current: options.stigmerToken ?? null };
215
213
  // The runner-class credential the bootstrap below may mint (see there); the
216
214
  // ref exists from here so the boot config can carry it.
217
- const runnerTokenRef: { current: string | null } = { current: null };
215
+ const runnerTokenRef: TokenRef = { current: null };
216
+ const bootConfig = mapOptionsToConfig(options, tokenRef, runnerTokenRef);
217
+
218
+ assertLlmBackendsPreflight(bootConfig.proxyEndpoint);
218
219
 
219
220
  // Boot the harnesses BEFORE resolving Temporal coordinates. Coordinate
220
221
  // discovery dials the control plane through connect-node, which snapshots
@@ -225,12 +226,6 @@ export async function createStigmerRunner(
225
226
  // coordinates, so `bootConfig` carries none yet. The two modules imported
226
227
  // here are connect-free by construction (`harness-adapters.ts`,
227
228
  // `harness/registry.ts`; `__tests__/harness-boot-order.test.ts`).
228
- const bootConfig: Config = {
229
- ...baseConfig,
230
- stigmerTokenRef: tokenRef,
231
- stigmerRunnerTokenRef: runnerTokenRef,
232
- proxyTokenRef: tokenRef,
233
- };
234
229
  const [{ HARNESS_ADAPTERS }, { adaptersOf, bootHarnesses, shutdownHarnesses }] = await Promise.all([
235
230
  import("./harness-adapters.js"),
236
231
  import("./harness/registry.js"),
@@ -248,7 +243,7 @@ export async function createStigmerRunner(
248
243
  explicitAddress: options.temporalAddress,
249
244
  explicitNamespace: options.temporalNamespace,
250
245
  token: options.stigmerToken,
251
- stigmerEndpoint: baseConfig.stigmerBackendEndpoint,
246
+ stigmerEndpoint: bootConfig.stigmerBackendEndpoint,
252
247
  });
253
248
 
254
249
  // Adopt the bootstrap-minted embedded_runner credential for gRPC runner-class
@@ -271,7 +266,7 @@ export async function createStigmerRunner(
271
266
  reMint: () =>
272
267
  refreshRunnerAccessToken({
273
268
  token: tokenRef.current,
274
- stigmerEndpoint: baseConfig.stigmerBackendEndpoint,
269
+ stigmerEndpoint: bootConfig.stigmerBackendEndpoint,
275
270
  }),
276
271
  });
277
272
  if (coordinates.runnerAccessToken) {
@@ -397,7 +392,19 @@ function validateOptions(options: StigmerRunnerOptions): void {
397
392
  // localhost when no token is present.
398
393
  }
399
394
 
400
- export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
395
+ /**
396
+ * The runtime `Config` for the static root. The refs are the root's: it
397
+ * creates them before mapping and writes them on every rotation (the
398
+ * sandbox-token renewal), so a `Config` never carries a credential by value
399
+ * (config.ts header). `tokenRef` is bound as BOTH the control-plane and the
400
+ * proxy credential — a static host provides the proxy token itself and mints
401
+ * no separate one, so the interceptors read this same ref per request.
402
+ */
403
+ export function mapOptionsToConfig(
404
+ options: StigmerRunnerOptions,
405
+ tokenRef: TokenRef,
406
+ runnerTokenRef: TokenRef,
407
+ ): Config {
401
408
  const proxyActive = !!options.proxyEndpoint;
402
409
 
403
410
  // Execution location is independent of proxy transport. An explicit
@@ -413,7 +420,9 @@ export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
413
420
  temporalAddress: options.temporalAddress ?? "",
414
421
  temporalNamespace: options.temporalNamespace ?? "default",
415
422
  stigmerBackendEndpoint: normalizeEndpoint(options.stigmerEndpoint),
416
- stigmerToken: options.stigmerToken ?? null,
423
+ stigmerTokenRef: tokenRef,
424
+ stigmerRunnerTokenRef: runnerTokenRef,
425
+ proxyTokenRef: tokenRef,
417
426
  // Env-only like the env-loaded path (loadConfig): the bridge endpoint
418
427
  // is deployment topology, not per-session state.
419
428
  mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
@@ -503,14 +512,14 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
503
512
  ...createEvaluateExpressionsActivities(),
504
513
  ...createCallHttpActivities(),
505
514
  ...createCallGrpcActivities(),
506
- ...createCallFunctionActivities(),
515
+ ...createCallFunctionActivities(config),
507
516
  ...createCallLlmActivities(),
508
517
  ...createCallAgentActivities(config),
509
- ...createCallAgentStatusActivities(),
518
+ ...createCallAgentStatusActivities(config),
510
519
  ...createRunCommandActivities(),
511
520
  ...createHydrateWorkflowActivities(config),
512
- ...createWorkflowEventActivities(),
513
- ...createPromoteTaskOutputActivities(),
521
+ ...createWorkflowEventActivities(config),
522
+ ...createPromoteTaskOutputActivities(config),
514
523
  ...createAttachSessionActivities(config),
515
524
  };
516
525
  }
@@ -109,7 +109,7 @@ describe("ProxyArtifactStorage", () => {
109
109
  });
110
110
 
111
111
  it("calls presigned-upload-url endpoint on upload", async () => {
112
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
112
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
113
113
 
114
114
  globalThis.fetch = vi.fn()
115
115
  .mockResolvedValueOnce({
@@ -129,7 +129,7 @@ describe("ProxyArtifactStorage", () => {
129
129
  });
130
130
 
131
131
  it("throws on presign failure", async () => {
132
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
132
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
133
133
 
134
134
  globalThis.fetch = vi.fn().mockResolvedValueOnce({
135
135
  ok: false,
@@ -143,7 +143,7 @@ describe("ProxyArtifactStorage", () => {
143
143
  });
144
144
 
145
145
  it("calls presigned-download-url for getDownloadUrl", async () => {
146
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
146
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
147
147
 
148
148
  globalThis.fetch = vi.fn().mockResolvedValueOnce({
149
149
  ok: true,
@@ -155,7 +155,7 @@ describe("ProxyArtifactStorage", () => {
155
155
  });
156
156
 
157
157
  it("download resolves a presigned URL then fetches the bytes", async () => {
158
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
158
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
159
159
 
160
160
  globalThis.fetch = vi.fn()
161
161
  .mockResolvedValueOnce({
@@ -172,7 +172,7 @@ describe("ProxyArtifactStorage", () => {
172
172
  });
173
173
 
174
174
  it("download throws with HTTP status and key on a failed fetch", async () => {
175
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
175
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
176
176
 
177
177
  globalThis.fetch = vi.fn()
178
178
  .mockResolvedValueOnce({
@@ -191,7 +191,7 @@ describe("ProxyArtifactStorage", () => {
191
191
  });
192
192
 
193
193
  it("exists returns true when the object is present (presign + ranged GET)", async () => {
194
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
194
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
195
195
 
196
196
  // Two-step probe: presign mints the URL, then a ranged GET hits the object.
197
197
  // Presign success alone is NOT existence — the object fetch is authoritative.
@@ -207,7 +207,7 @@ describe("ProxyArtifactStorage", () => {
207
207
  });
208
208
 
209
209
  it("exists returns false when the presign endpoint fails", async () => {
210
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
210
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
211
211
 
212
212
  globalThis.fetch = vi.fn(async () => new Response("nope", { status: 404 })) as typeof fetch;
213
213
 
@@ -233,7 +233,7 @@ describe("createArtifactStorage", () => {
233
233
  localPath: "/var/artifacts",
234
234
  localServeUrl: "http://localhost:7235",
235
235
  proxyEndpoint: "https://proxy.example.com",
236
- proxyAuthToken: "token",
236
+ proxyAuthToken: { current: "token" },
237
237
  });
238
238
  expect(storage).toBeInstanceOf(ProxyArtifactStorage);
239
239
  });
@@ -244,7 +244,7 @@ describe("createArtifactStorage", () => {
244
244
  localPath: "/var/artifacts",
245
245
  localServeUrl: "http://localhost:7235",
246
246
  proxyEndpoint: null,
247
- proxyAuthToken: "token",
247
+ proxyAuthToken: { current: "token" },
248
248
  })).toThrow("STIGMER_PROXY_ENDPOINT");
249
249
  });
250
250
 
@@ -172,7 +172,7 @@ describe("ProxyArtifactStorage", () => {
172
172
  });
173
173
 
174
174
  it("self-describes its URLs as presigned (time-limited, remotely fetchable — issue #532)", () => {
175
- const s = new ProxyArtifactStorage("https://proxy.example.com", "tok");
175
+ const s = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
176
176
  expect(s.downloadUrlKind).toBe("presigned");
177
177
  });
178
178
 
@@ -195,7 +195,7 @@ describe("ProxyArtifactStorage", () => {
195
195
  return new Response("not found", { status: 404 });
196
196
  }) as typeof fetch;
197
197
 
198
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "token-123");
198
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "token-123" });
199
199
  const key = await storage.upload("artifacts/exec-1/f.txt", Buffer.from("data"), "text/plain");
200
200
 
201
201
  expect(key).toBe("artifacts/exec-1/f.txt");
@@ -237,7 +237,7 @@ describe("ProxyArtifactStorage", () => {
237
237
  "content-type": "text/markdown",
238
238
  "host": "test-bucket.localhost:9000",
239
239
  });
240
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
240
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
241
241
  await storage.upload("artifacts/exec-1/plan.md", Buffer.from("# Plan"), "text/markdown");
242
242
 
243
243
  const sent = await headersPromise;
@@ -249,7 +249,7 @@ describe("ProxyArtifactStorage", () => {
249
249
  "content-type": "text/markdown",
250
250
  "host": "test-bucket.localhost:9000",
251
251
  });
252
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
252
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
253
253
  await storage.upload("artifacts/exec-1/plan.md", Buffer.from("# Plan"), "text/markdown");
254
254
 
255
255
  const sent = await headersPromise;
@@ -262,7 +262,7 @@ describe("ProxyArtifactStorage", () => {
262
262
  "content-type": ["text/markdown"],
263
263
  "host": ["test-bucket.localhost:9000"],
264
264
  });
265
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
265
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
266
266
  await storage.upload("artifacts/exec-1/plan.md", Buffer.from("# Plan"), "text/markdown");
267
267
 
268
268
  const sent = await headersPromise;
@@ -273,7 +273,7 @@ describe("ProxyArtifactStorage", () => {
273
273
  const headersPromise = captureUploadHeaders({
274
274
  "host": "test-bucket.localhost:9000",
275
275
  });
276
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
276
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
277
277
  await storage.upload("artifacts/exec-1/f.bin", Buffer.from("x"), "application/octet-stream");
278
278
 
279
279
  const sent = await headersPromise;
@@ -285,7 +285,7 @@ describe("ProxyArtifactStorage", () => {
285
285
  new Response("forbidden", { status: 403 }),
286
286
  ) as typeof fetch;
287
287
 
288
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "bad-token");
288
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "bad-token" });
289
289
  await expect(
290
290
  storage.upload("key", Buffer.from("x")),
291
291
  ).rejects.toThrow("Failed to get presigned upload URL (HTTP 403)");
@@ -307,7 +307,7 @@ describe("ProxyArtifactStorage", () => {
307
307
  // delayFn injected to skip the real ~7.75 s of backoff (#468). The
308
308
  // degrade contract under test (budget exhaustion still throws the
309
309
  // caller-visible error) is unchanged.
310
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok", {
310
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" }, {
311
311
  delayFn: async () => {},
312
312
  });
313
313
  await expect(
@@ -320,7 +320,7 @@ describe("ProxyArtifactStorage", () => {
320
320
  new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 }),
321
321
  ) as typeof fetch;
322
322
 
323
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
323
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
324
324
  const url = await storage.getDownloadUrl("key");
325
325
  expect(url).toBe("https://r2.example.com/dl");
326
326
  });
@@ -337,7 +337,7 @@ describe("ProxyArtifactStorage", () => {
337
337
  return new Response("not found", { status: 404 });
338
338
  }) as typeof fetch;
339
339
 
340
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
340
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
341
341
  const got = await storage.download("artifacts/exec-1/f.txt");
342
342
  expect(got.toString()).toBe("payload-bytes");
343
343
  });
@@ -351,7 +351,7 @@ describe("ProxyArtifactStorage", () => {
351
351
  return new Response("gone", { status: 404 });
352
352
  }) as typeof fetch;
353
353
 
354
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
354
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
355
355
  await expect(storage.download("artifacts/exec-1/missing.txt")).rejects.toThrow(
356
356
  /Artifact download failed \(HTTP 404\) for key 'artifacts\/exec-1\/missing\.txt'/,
357
357
  );
@@ -372,19 +372,19 @@ describe("ProxyArtifactStorage", () => {
372
372
 
373
373
  it("exists returns true when the object GET is 200", async () => {
374
374
  mockProxyFetch(() => new Response(Buffer.from("x"), { status: 200 }));
375
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
375
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
376
376
  expect(await storage.exists("key")).toBe(true);
377
377
  });
378
378
 
379
379
  it("exists returns true when the object GET is 206 (ranged)", async () => {
380
380
  mockProxyFetch(() => new Response(Buffer.from("x"), { status: 206 }));
381
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
381
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
382
382
  expect(await storage.exists("key")).toBe(true);
383
383
  });
384
384
 
385
385
  it("exists returns true on 416 (0-byte object, range unsatisfiable but present)", async () => {
386
386
  mockProxyFetch(() => new Response("", { status: 416 }));
387
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
387
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
388
388
  expect(await storage.exists("key")).toBe(true);
389
389
  });
390
390
 
@@ -393,7 +393,7 @@ describe("ProxyArtifactStorage", () => {
393
393
  // only the object fetch reveals it is absent. The old exists() returned true
394
394
  // here (presign.ok), which crashed the file-review reconcile on the R2 404.
395
395
  mockProxyFetch(() => new Response("<Error><Code>NoSuchKey</Code></Error>", { status: 404 }));
396
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
396
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
397
397
  expect(await storage.exists("key")).toBe(false);
398
398
  });
399
399
 
@@ -411,7 +411,7 @@ describe("ProxyArtifactStorage", () => {
411
411
  });
412
412
  return new Response(Buffer.from("x"), { status: 206 });
413
413
  }) as typeof fetch;
414
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
414
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
415
415
  await storage.exists("key");
416
416
  expect(seen).toEqual([{ url: "https://r2.example.com/obj", range: "bytes=0-0", method: "GET" }]);
417
417
  });
@@ -420,7 +420,7 @@ describe("ProxyArtifactStorage", () => {
420
420
  globalThis.fetch = vi.fn(async () => {
421
421
  throw new Error("network unreachable");
422
422
  }) as typeof fetch;
423
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
423
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
424
424
  expect(await storage.exists("key")).toBe(false);
425
425
  });
426
426
 
@@ -428,7 +428,7 @@ describe("ProxyArtifactStorage", () => {
428
428
  mockProxyFetch(() => new Response("boom", { status: 500 }));
429
429
  // Persistent 500 exhausts the retry budget; delayFn skips the real
430
430
  // backoff (#468). The fault-not-an-answer contract is unchanged.
431
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok", {
431
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" }, {
432
432
  delayFn: async () => {},
433
433
  });
434
434
  await expect(storage.exists("key")).rejects.toThrow(
@@ -450,7 +450,7 @@ describe("ProxyArtifactStorage", () => {
450
450
  });
451
451
 
452
452
  function makeStorage() {
453
- return new ProxyArtifactStorage("https://proxy.example.com", "tok", {
453
+ return new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" }, {
454
454
  delayFn: async (ms) => {
455
455
  recordedDelays.push(ms);
456
456
  },
@@ -554,7 +554,7 @@ describe("ProxyArtifactStorage", () => {
554
554
  );
555
555
  }) as typeof fetch;
556
556
 
557
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok", {
557
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" }, {
558
558
  requestTimeoutMs: 20,
559
559
  delayFn: async (ms) => {
560
560
  recordedDelays.push(ms);
@@ -591,7 +591,7 @@ describe("createArtifactStorage", () => {
591
591
  localPath: "/tmp/artifacts",
592
592
  localServeUrl: "http://localhost:7235",
593
593
  proxyEndpoint: "https://proxy.example.com",
594
- proxyAuthToken: "token-123",
594
+ proxyAuthToken: { current: "token-123" },
595
595
  };
596
596
  const storage = createArtifactStorage(cfg);
597
597
  expect(storage).toBeInstanceOf(ProxyArtifactStorage);
@@ -603,7 +603,7 @@ describe("createArtifactStorage", () => {
603
603
  localPath: "/tmp/artifacts",
604
604
  localServeUrl: "http://localhost:7235",
605
605
  proxyEndpoint: null,
606
- proxyAuthToken: "token",
606
+ proxyAuthToken: { current: "token" },
607
607
  };
608
608
  expect(() => createArtifactStorage(cfg)).toThrow("STIGMER_PROXY_ENDPOINT");
609
609
  });
@@ -640,7 +640,7 @@ describe("loadArtifactStorageConfig", () => {
640
640
  const baseConfig = {
641
641
  mode: "local" as const,
642
642
  proxyEndpoint: null,
643
- stigmerToken: null,
643
+ stigmerTokenRef: { current: null },
644
644
  taskQueue: "q",
645
645
  temporalAddress: "localhost:7233",
646
646
  temporalNamespace: "default",
@@ -681,7 +681,7 @@ describe("loadArtifactStorageConfig", () => {
681
681
  mode: "cloud",
682
682
  proxyEndpoint: "https://proxy.example.com",
683
683
  artifactProxyEndpoint: "https://proxy.example.com",
684
- stigmerToken: "tok",
684
+ stigmerTokenRef: { current: "tok" },
685
685
  });
686
686
  expect(cfg.type).toBe("proxy");
687
687
  expect(cfg.proxyEndpoint).toBe("https://proxy.example.com");
@@ -690,16 +690,19 @@ describe("loadArtifactStorageConfig", () => {
690
690
  it("uses proxy in local mode when a proxy endpoint is set (desktop case)", () => {
691
691
  // The desktop runner executes locally yet proxies its artifacts: storage
692
692
  // follows transport (proxyEndpoint), not execution location (mode).
693
+ const ref = { current: "tok" };
693
694
  const cfg = loadArtifactStorageConfig({
694
695
  ...baseConfig,
695
696
  mode: "local",
696
697
  proxyEndpoint: "https://localhost:9090",
697
698
  artifactProxyEndpoint: "https://localhost:9090",
698
- stigmerToken: "tok",
699
+ stigmerTokenRef: ref,
699
700
  });
700
701
  expect(cfg.type).toBe("proxy");
701
702
  expect(cfg.proxyEndpoint).toBe("https://localhost:9090");
702
- expect(cfg.proxyAuthToken).toBe("tok");
703
+ // The runner's ref itself, never a copy of its value: a rotation reaches
704
+ // the store through this same object.
705
+ expect(cfg.proxyAuthToken).toBe(ref);
703
706
  });
704
707
 
705
708
  it("presigns against the artifact override when split from the LLM proxy endpoint (stigmer#803)", () => {
@@ -711,7 +714,7 @@ describe("loadArtifactStorageConfig", () => {
711
714
  ...baseConfig,
712
715
  proxyEndpoint: "https://mock-llm.example.com",
713
716
  artifactProxyEndpoint: "https://service.example.com",
714
- stigmerToken: "tok",
717
+ stigmerTokenRef: { current: "tok" },
715
718
  });
716
719
  expect(cfg.type).toBe("proxy");
717
720
  expect(cfg.proxyEndpoint).toBe("https://service.example.com");
@@ -724,7 +727,7 @@ describe("loadArtifactStorageConfig", () => {
724
727
  const cfg = loadArtifactStorageConfig({
725
728
  ...baseConfig,
726
729
  proxyEndpoint: "https://proxy.example.com",
727
- stigmerToken: "tok",
730
+ stigmerTokenRef: { current: "tok" },
728
731
  });
729
732
  expect(cfg.type).toBe("none");
730
733
  });
@@ -734,7 +737,7 @@ describe("loadArtifactStorageConfig", () => {
734
737
  const cfg = loadArtifactStorageConfig({
735
738
  ...baseConfig,
736
739
  proxyEndpoint: "https://proxy.example.com",
737
- stigmerToken: "tok",
740
+ stigmerTokenRef: { current: "tok" },
738
741
  });
739
742
  expect(cfg.type).toBe("proxy");
740
743
  });
@@ -838,7 +841,7 @@ describe("resolveUsableArtifactStorage", () => {
838
841
  localPath: "/tmp/artifacts",
839
842
  localServeUrl: "http://localhost:7235",
840
843
  proxyEndpoint: "https://proxy.example.com",
841
- proxyAuthToken: "token-123",
844
+ proxyAuthToken: { current: "token-123" },
842
845
  };
843
846
 
844
847
  const storage = await resolveUsableArtifactStorage(cfg, ctx);
@@ -24,7 +24,7 @@ const SCHEMA = { type: "object", properties: { answer: { type: "string" } } };
24
24
  function makeConfig(overrides: Partial<Config> = {}): Config {
25
25
  return {
26
26
  proxyEndpoint: null,
27
- stigmerToken: null,
27
+ stigmerTokenRef: { current: null },
28
28
  ...overrides,
29
29
  } as Config;
30
30
  }
@@ -78,7 +78,7 @@ describe("extractStructuredOutput", () => {
78
78
 
79
79
  const result = await extractStructuredOutput(
80
80
  "text", SCHEMA,
81
- makeConfig({ proxyEndpoint: "https://api.stigmer.ai", stigmerToken: "tok" }),
81
+ makeConfig({ proxyEndpoint: "https://api.stigmer.ai", stigmerTokenRef: { current: "tok" } }),
82
82
  "gpt-4.1",
83
83
  );
84
84
 
@@ -24,7 +24,7 @@
24
24
  import { mkdir, writeFile, readFile, access, rm } from "node:fs/promises";
25
25
  import { dirname, join, resolve, sep } from "node:path";
26
26
  import { homedir } from "node:os";
27
- import type { Config } from "../config.js";
27
+ import type { Config, TokenRef } from "../config.js";
28
28
  import type { RetryOptions } from "./grpc-retry.js";
29
29
  import { fetchWithRetry, type FetchRetryPolicy } from "./http-retry.js";
30
30
 
@@ -212,9 +212,7 @@ export class ProxyArtifactStorage implements ArtifactStorage {
212
212
  * life — a captured string would silently 401 after the first rotation.
213
213
  */
214
214
  private get authToken(): string {
215
- return typeof this.authTokenSource === "string"
216
- ? this.authTokenSource
217
- : (this.authTokenSource.current ?? "");
215
+ return this.authTokenSource.current ?? "";
218
216
  }
219
217
 
220
218
  async upload(key: string, content: Buffer, contentType?: string): Promise<string> {
@@ -343,10 +341,12 @@ export class ProxyArtifactStorage implements ArtifactStorage {
343
341
  // ── Factory ──────────────────────────────────────────────────────────
344
342
 
345
343
  /**
346
- * The proxy credential, either fixed (a caller-supplied string) or live (a
347
- * shared mutable ref, read per call — the sandbox token-renewal posture).
344
+ * The proxy credential: the runner's shared ref, read per call (the
345
+ * sandbox token-renewal posture; config.ts header). Never a string — a
346
+ * captured value would present the boot credential after the first
347
+ * rotation.
348
348
  */
349
- export type ProxyAuthTokenSource = string | { readonly current: string | null };
349
+ export type ProxyAuthTokenSource = Readonly<TokenRef>;
350
350
 
351
351
  export interface ArtifactStorageConfig {
352
352
  readonly type: ArtifactStorageType;
@@ -392,11 +392,9 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
392
392
  localPath: process.env.LOCAL_ARTIFACT_PATH ?? defaultLocalArtifactPath(),
393
393
  localServeUrl: process.env.LOCAL_ARTIFACT_SERVE_URL ?? "http://localhost:7235",
394
394
  proxyEndpoint: type === "proxy" ? (config.artifactProxyEndpoint ?? null) : null,
395
- // Prefer the live ref: renewal rotates the token in place and uploads
396
- // must present the current credential, not the boot one.
397
- proxyAuthToken: type === "proxy"
398
- ? (config.stigmerTokenRef ?? config.stigmerToken ?? null)
399
- : null,
395
+ // The live ref: renewal rotates the token in place and uploads must
396
+ // present the current credential, not the boot one.
397
+ proxyAuthToken: type === "proxy" ? config.stigmerTokenRef : null,
400
398
  };
401
399
  }
402
400
 
@@ -415,13 +413,10 @@ export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStora
415
413
  "Proxy artifact storage requires STIGMER_ARTIFACT_PROXY_ENDPOINT or STIGMER_PROXY_ENDPOINT",
416
414
  );
417
415
  }
418
- const tokenAtBoot = typeof cfg.proxyAuthToken === "string"
419
- ? cfg.proxyAuthToken
420
- : cfg.proxyAuthToken?.current;
421
- if (!tokenAtBoot) {
416
+ if (!cfg.proxyAuthToken?.current) {
422
417
  throw new Error("Proxy artifact storage requires STIGMER_TOKEN");
423
418
  }
424
- return new ProxyArtifactStorage(cfg.proxyEndpoint, cfg.proxyAuthToken!);
419
+ return new ProxyArtifactStorage(cfg.proxyEndpoint, cfg.proxyAuthToken);
425
420
  }
426
421
 
427
422
  return new LocalArtifactStorage(cfg.localPath, cfg.localServeUrl);
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, afterEach } from "vitest";
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -39,14 +39,14 @@ describe("createCheckpointer", () => {
39
39
  const saver = await createCheckpointer({
40
40
  type: "http",
41
41
  proxyEndpoint: "https://proxy.test",
42
- authToken: "tok-123",
42
+ authToken: { current: "tok-123" },
43
43
  });
44
44
  expect(saver).toBeInstanceOf(HttpCheckpointSaver);
45
45
  });
46
46
 
47
47
  it("throws CheckpointerCreationError when http is missing proxyEndpoint", async () => {
48
48
  await expect(
49
- createCheckpointer({ type: "http", authToken: "tok" }),
49
+ createCheckpointer({ type: "http", authToken: { current: "tok" } }),
50
50
  ).rejects.toThrow(CheckpointerCreationError);
51
51
  });
52
52
 
@@ -56,6 +56,26 @@ describe("createCheckpointer", () => {
56
56
  ).rejects.toThrow(CheckpointerCreationError);
57
57
  });
58
58
 
59
+ it("throws CheckpointerCreationError when the http credential ref holds nothing at open", async () => {
60
+ await expect(
61
+ createCheckpointer({ type: "http", proxyEndpoint: "https://proxy.test", authToken: { current: null } }),
62
+ ).rejects.toThrow(CheckpointerCreationError);
63
+ });
64
+
65
+ it("hands the http saver the caller's ref itself, so a rotation after open reaches the saver's next request", async () => {
66
+ const ref = { current: "boot-credential" };
67
+ const saver = await createCheckpointer({ type: "http", proxyEndpoint: "https://proxy.test", authToken: ref });
68
+ const fetchSpy = vi.fn().mockResolvedValue({ status: 404, ok: false });
69
+ vi.stubGlobal("fetch", fetchSpy);
70
+ try {
71
+ ref.current = "session-credential";
72
+ await saver.getTuple({ configurable: { thread_id: "t", checkpoint_ns: "" } });
73
+ expect(fetchSpy.mock.calls[0][1].headers.Authorization).toBe("Bearer session-credential");
74
+ } finally {
75
+ vi.unstubAllGlobals();
76
+ }
77
+ });
78
+
59
79
  it("error includes the checkpointer type", async () => {
60
80
  try {
61
81
  await createCheckpointer({ type: "http" });