@stigmer/runner 3.21.0 → 3.22.1-dev.20260922095755
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/attach-session.js +0 -1
- package/dist/activities/attach-session.js.map +1 -1
- package/dist/activities/call-agent-status.d.ts +18 -11
- package/dist/activities/call-agent-status.js +21 -24
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-agent.d.ts +2 -2
- package/dist/activities/call-agent.js +4 -6
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/call-function.d.ts +4 -2
- package/dist/activities/call-function.js +13 -4
- package/dist/activities/call-function.js.map +1 -1
- package/dist/activities/classify-tool-approvals.js +1 -1
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.js +0 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +6 -1
- package/dist/activities/emit-event.js +5 -14
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/adapter.js +0 -1
- package/dist/activities/execute-cursor/adapter.js.map +1 -1
- package/dist/activities/execute-cursor/turn-setup.d.ts +1 -1
- package/dist/activities/execute-cursor/turn-setup.js +5 -6
- package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/adapter.js +1 -1
- package/dist/activities/execute-deep-agent/adapter.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +5 -3
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/generate-session-subject.js +1 -2
- package/dist/activities/generate-session-subject.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +0 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/promote-task-output.d.ts +2 -1
- package/dist/activities/promote-task-output.js +10 -14
- package/dist/activities/promote-task-output.js.map +1 -1
- package/dist/activities/workflow-event-activities.d.ts +9 -7
- package/dist/activities/workflow-event-activities.js +14 -18
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +13 -13
- package/dist/client/stigmer-client.js +4 -7
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/config.d.ts +40 -20
- package/dist/config.js +25 -9
- package/dist/config.js.map +1 -1
- package/dist/harness/registry.js +0 -1
- package/dist/harness/registry.js.map +1 -1
- package/dist/harness/turn-context.js +2 -4
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +13 -6
- package/dist/main.js.map +1 -1
- package/dist/runner-manager.d.ts +8 -6
- package/dist/runner-manager.js +20 -10
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner-token-coordinator.d.ts +8 -3
- package/dist/runner-token-coordinator.js +8 -3
- package/dist/runner-token-coordinator.js.map +1 -1
- package/dist/runner.d.ts +10 -2
- package/dist/runner.js +31 -25
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +6 -6
- package/dist/shared/artifact-storage.js +5 -12
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/checkpointer/factory.js +5 -1
- package/dist/shared/checkpointer/factory.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +13 -2
- package/dist/shared/checkpointer/http-saver.js +17 -5
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/checkpointer/types.d.ts +10 -1
- package/dist/shared/checkpointer/types.js +16 -0
- package/dist/shared/checkpointer/types.js.map +1 -1
- package/dist/shared/extract-structured-output.js +1 -1
- package/dist/shared/extract-structured-output.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +2 -2
- package/src/__test-utils__/module-specifiers.ts +45 -3
- package/src/__tests__/config-boundary.test.ts +135 -0
- package/src/__tests__/config.test.ts +7 -16
- package/src/__tests__/runner-manager.test.ts +28 -6
- package/src/__tests__/runner.test.ts +28 -4
- package/src/activities/__tests__/attach-session.test.ts +1 -1
- package/src/activities/__tests__/call-agent-contracts.test.ts +4 -7
- package/src/activities/__tests__/call-agent-status.test.ts +17 -16
- package/src/activities/__tests__/call-agent.test.ts +28 -7
- package/src/activities/__tests__/call-function.test.ts +5 -4
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -1
- package/src/activities/__tests__/generate-session-subject-wire.test.ts +1 -1
- package/src/activities/__tests__/workflow-event-activities.test.ts +21 -23
- package/src/activities/attach-session.ts +0 -1
- package/src/activities/call-agent-status.ts +28 -20
- package/src/activities/call-agent.ts +5 -6
- package/src/activities/call-function.ts +15 -3
- package/src/activities/classify-tool-approvals.ts +1 -1
- package/src/activities/discover-mcp-server.ts +0 -1
- package/src/activities/emit-event.ts +7 -16
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -1
- package/src/activities/execute-cursor/adapter.ts +0 -1
- package/src/activities/execute-cursor/turn-setup.ts +6 -7
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +1 -1
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +2 -2
- package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
- package/src/activities/execute-deep-agent/adapter.ts +1 -1
- package/src/activities/execute-deep-agent/turn-setup.ts +6 -4
- package/src/activities/generate-session-subject.ts +1 -2
- package/src/activities/hydrate-workflow-execution.ts +0 -1
- package/src/activities/promote-task-output.ts +11 -15
- package/src/activities/workflow-event-activities.ts +17 -18
- package/src/client/__tests__/stigmer-client.test.ts +8 -19
- package/src/client/stigmer-client.ts +18 -20
- package/src/config.ts +51 -23
- package/src/harness/registry.ts +0 -1
- package/src/harness/turn-context.ts +2 -4
- package/src/main.ts +13 -6
- package/src/runner-manager.ts +25 -15
- package/src/runner-token-coordinator.ts +8 -3
- package/src/runner.ts +39 -30
- package/src/shared/__tests__/artifact-storage-extended.test.ts +9 -9
- package/src/shared/__tests__/artifact-storage.test.ts +33 -30
- package/src/shared/__tests__/extract-structured-output.test.ts +2 -2
- package/src/shared/artifact-storage.ts +12 -17
- package/src/shared/checkpointer/__tests__/factory.test.ts +23 -3
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +25 -2
- package/src/shared/checkpointer/factory.ts +5 -1
- package/src/shared/checkpointer/http-saver.ts +20 -6
- package/src/shared/checkpointer/types.ts +12 -1
- package/src/shared/extract-structured-output.ts +1 -1
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +1 -1
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +1 -1
- 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,
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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:
|
|
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.
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
const tokenRef = { current:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
699
|
+
stigmerTokenRef: ref,
|
|
699
700
|
});
|
|
700
701
|
expect(cfg.type).toBe("proxy");
|
|
701
702
|
expect(cfg.proxyEndpoint).toBe("https://localhost:9090");
|
|
702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
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
|
|
347
|
-
*
|
|
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 =
|
|
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
|
-
//
|
|
396
|
-
//
|
|
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
|
-
|
|
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" });
|