@stigmer/runner 3.6.0 → 3.7.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 (65) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-agent.js +85 -10
  3. package/dist/activities/call-agent.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.js +39 -5
  5. package/dist/activities/execute-cursor/index.js.map +1 -1
  6. package/dist/activities/execute-cursor/model-pricing.d.ts +9 -0
  7. package/dist/activities/execute-cursor/model-pricing.js +19 -0
  8. package/dist/activities/execute-cursor/model-pricing.js.map +1 -1
  9. package/dist/activities/execute-cursor/service-tier.d.ts +68 -0
  10. package/dist/activities/execute-cursor/service-tier.js +187 -0
  11. package/dist/activities/execute-cursor/service-tier.js.map +1 -0
  12. package/dist/activities/execute-cursor/session-lifecycle.d.ts +16 -1
  13. package/dist/activities/execute-cursor/session-lifecycle.js +12 -4
  14. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  15. package/dist/activities/execute-cursor/usage-accumulator.d.ts +21 -1
  16. package/dist/activities/execute-cursor/usage-accumulator.js +23 -3
  17. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  18. package/dist/client/stigmer-client.d.ts +6 -1
  19. package/dist/client/stigmer-client.js +5 -2
  20. package/dist/client/stigmer-client.js.map +1 -1
  21. package/dist/main.js +18 -0
  22. package/dist/main.js.map +1 -1
  23. package/dist/runner.js +48 -0
  24. package/dist/runner.js.map +1 -1
  25. package/dist/sandbox-token-renewal.d.ts +65 -0
  26. package/dist/sandbox-token-renewal.js +169 -0
  27. package/dist/sandbox-token-renewal.js.map +1 -0
  28. package/dist/shared/artifact-storage.d.ts +17 -3
  29. package/dist/shared/artifact-storage.js +22 -4
  30. package/dist/shared/artifact-storage.js.map +1 -1
  31. package/dist/workflow-engine/loader.js +99 -2
  32. package/dist/workflow-engine/loader.js.map +1 -1
  33. package/dist/workflow-engine/tasks/call-agent.d.ts +0 -2
  34. package/dist/workflow-engine/tasks/call-agent.js +0 -2
  35. package/dist/workflow-engine/tasks/call-agent.js.map +1 -1
  36. package/dist/workflow-engine/types.d.ts +39 -7
  37. package/dist/workflow-engine/types.js.map +1 -1
  38. package/dist/workflows/call-agent-orchestrator.d.ts +3 -2
  39. package/dist/workflows/call-agent-orchestrator.js +8 -2
  40. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/sandbox-token-renewal.test.ts +174 -0
  43. package/src/activities/__tests__/call-agent-contracts.test.ts +4 -4
  44. package/src/activities/__tests__/call-agent.test.ts +219 -4
  45. package/src/activities/call-agent.ts +94 -10
  46. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +20 -0
  47. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +170 -0
  48. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +87 -1
  49. package/src/activities/execute-cursor/index.ts +49 -7
  50. package/src/activities/execute-cursor/model-pricing.ts +23 -0
  51. package/src/activities/execute-cursor/service-tier.ts +244 -0
  52. package/src/activities/execute-cursor/session-lifecycle.ts +33 -5
  53. package/src/activities/execute-cursor/usage-accumulator.ts +35 -3
  54. package/src/client/stigmer-client.ts +11 -4
  55. package/src/main.ts +20 -0
  56. package/src/runner.ts +62 -0
  57. package/src/sandbox-token-renewal.ts +212 -0
  58. package/src/shared/artifact-storage.ts +32 -7
  59. package/src/workflow-engine/__tests__/golden-execution.test.ts +8 -8
  60. package/src/workflow-engine/__tests__/loader.test.ts +192 -7
  61. package/src/workflow-engine/__tests__/tasks/call-agent.test.ts +9 -9
  62. package/src/workflow-engine/loader.ts +113 -2
  63. package/src/workflow-engine/tasks/call-agent.ts +0 -2
  64. package/src/workflow-engine/types.ts +40 -7
  65. package/src/workflows/call-agent-orchestrator.ts +8 -2
@@ -49,7 +49,7 @@ import { WorkflowQueryController } from "@stigmer/protos/ai/stigmer/agentic/work
49
49
  import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
50
50
  import { WorkflowInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/query_pb";
51
51
  import type { WorkflowInstance } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
52
- import { PlatformQueryController, GetRunnerScopedTokenInputSchema } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
52
+ import { PlatformQueryController, GetRunnerScopedTokenInputSchema, TokenRenewalSchema } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
53
53
  import { ChannelMessageQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_query_pb";
54
54
  import type { ChannelTemplate, MessagingChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
55
55
  import { isEmbeddedRunnerToken } from "./token-claims.js";
@@ -94,12 +94,16 @@ export interface RunnerScopedToken {
94
94
  * `poolClaimSessionId` is the warm-pool attach exchange: a pool sandbox
95
95
  * presenting its pool_sandbox credential for the session it was claimed for
96
96
  * (the server authorizes against the claim record, not the caller's FGA
97
- * relations). The execution arms remain embedded_runner-only.
97
+ * relations). `renewal` is a live sandbox extending its own credential
98
+ * before expiry: no id is named because every mint parameter comes from the
99
+ * presented credential's verified claims (see sandbox-token-renewal.ts).
100
+ * The execution arms remain embedded_runner-only.
98
101
  */
99
102
  export type RunnerScopedTokenScope =
100
103
  | { agentExecutionId: string }
101
104
  | { workflowExecutionId: string }
102
- | { poolClaimSessionId: string };
105
+ | { poolClaimSessionId: string }
106
+ | { renewal: true };
103
107
 
104
108
  /**
105
109
  * Map the scope union onto the proto oneof init shape. The narrowing chain is
@@ -113,7 +117,10 @@ function toRunnerScopedTokenOneof(scope: RunnerScopedTokenScope) {
113
117
  if ("workflowExecutionId" in scope) {
114
118
  return { case: "workflowExecutionId", value: scope.workflowExecutionId } as const;
115
119
  }
116
- return { case: "poolClaim", value: { sessionId: scope.poolClaimSessionId } } as const;
120
+ if ("poolClaimSessionId" in scope) {
121
+ return { case: "poolClaim", value: { sessionId: scope.poolClaimSessionId } } as const;
122
+ }
123
+ return { case: "renewal", value: create(TokenRenewalSchema) } as const;
117
124
  }
118
125
 
119
126
  /**
package/src/main.ts CHANGED
@@ -213,6 +213,25 @@ async function runPoolMode(
213
213
  executionMode: config.mode,
214
214
  });
215
215
 
216
+ // Sandbox credential self-renewal (see sandbox-token-renewal.ts). Watches
217
+ // the env var because manager.updateToken keeps it in lockstep with the
218
+ // manager's internal ref: a blank member's pool_sandbox token parks the
219
+ // loop, the claim's updateToken swaps in a renewable session token, and
220
+ // from then on renewal applies fresh tokens back through the same
221
+ // updateToken (which also cascades to the proxy-credential coordinator).
222
+ const { startSandboxTokenRenewal } = await import("./sandbox-token-renewal.js");
223
+ const { StigmerClient } = await import("./client/stigmer-client.js");
224
+ const renewalClient = new StigmerClient({
225
+ endpoint: config.stigmerBackendEndpoint,
226
+ token: null,
227
+ });
228
+ const tokenRenewal = startSandboxTokenRenewal({
229
+ getToken: () => process.env.STIGMER_TOKEN ?? null,
230
+ renew: (currentToken) =>
231
+ renewalClient.getRunnerScopedToken({ renewal: true }, currentToken),
232
+ applyToken: (token) => manager.updateToken(token),
233
+ });
234
+
216
235
  let taskQueue: string;
217
236
  if (intent.kind === "pool-control") {
218
237
  registerPoolMemberContext({
@@ -277,6 +296,7 @@ async function runPoolMode(
277
296
  }
278
297
  shutdownRequested = true;
279
298
  console.warn(`[pool-member] Received ${signal}, shutting down gracefully...`);
299
+ tokenRenewal.stop();
280
300
  void manager.shutdown().then(resolve, (err) => {
281
301
  console.error("[pool-member] Shutdown failed:", err);
282
302
  resolve();
package/src/runner.ts CHANGED
@@ -114,6 +114,56 @@ export interface StigmerRunner {
114
114
  shutdown(): void;
115
115
  }
116
116
 
117
+ /**
118
+ * Wire up self-renewal for a static cloud sandbox's control-plane credential
119
+ * (see sandbox-token-renewal.ts for the model). The applied token reaches
120
+ * every consumer: activity gRPC clients read {@code tokenRef} per request,
121
+ * env-reading call sites (call-llm, registry-endpoint) read
122
+ * {@code process.env.STIGMER_TOKEN} per call, artifact storage resolves the
123
+ * ref per call, and the two Cursor SDK interceptors are updated directly —
124
+ * a static runner has no {@code RunnerTokenCoordinator} minting a separate
125
+ * proxy credential, so its x-stigmer-auth IS this token.
126
+ */
127
+ async function startStaticSandboxTokenRenewal(
128
+ config: Config,
129
+ tokenRef: { current: string | null },
130
+ ): Promise<{ stop(): void } | null> {
131
+ const { isRenewableSandboxToken, startSandboxTokenRenewal } = await import(
132
+ "./sandbox-token-renewal.js"
133
+ );
134
+ // Static mode's credential class never changes (it is baked into the pod's
135
+ // env), so a non-renewable credential — desktop/OSS/local — never starts
136
+ // the loop at all.
137
+ if (!isRenewableSandboxToken(tokenRef.current)) {
138
+ return null;
139
+ }
140
+
141
+ const { StigmerClient } = await import("./client/stigmer-client.js");
142
+ const { updateInterceptorToken } = await import(
143
+ "./activities/execute-cursor/fetch-interceptor.js"
144
+ );
145
+ const { updateHttp2InterceptorToken } = await import(
146
+ "./activities/execute-cursor/http2-interceptor.js"
147
+ );
148
+ const client = new StigmerClient({
149
+ endpoint: config.stigmerBackendEndpoint,
150
+ token: null,
151
+ tokenRef,
152
+ });
153
+
154
+ return startSandboxTokenRenewal({
155
+ getToken: () => tokenRef.current,
156
+ renew: (currentToken) =>
157
+ client.getRunnerScopedToken({ renewal: true }, currentToken),
158
+ applyToken: (token) => {
159
+ tokenRef.current = token;
160
+ process.env.STIGMER_TOKEN = token;
161
+ updateInterceptorToken(token);
162
+ updateHttp2InterceptorToken(token);
163
+ },
164
+ });
165
+ }
166
+
117
167
  /**
118
168
  * Create a Stigmer runner ready to poll a Temporal task queue.
119
169
  *
@@ -198,13 +248,24 @@ export async function createStigmerRunner(
198
248
  token: options.stigmerToken,
199
249
  stigmerEndpoint: baseConfig.stigmerBackendEndpoint,
200
250
  });
251
+ // The control-plane credential lives in a shared mutable ref (as in manager
252
+ // mode) so the sandbox-token renewal below can rotate it in-process:
253
+ // activity clients read the ref per request instead of pinning the boot
254
+ // token for the pod's whole life.
255
+ const tokenRef = { current: baseConfig.stigmerToken };
201
256
  const config: Config = {
202
257
  ...baseConfig,
203
258
  temporalAddress: coordinates.temporalAddress,
204
259
  temporalNamespace: coordinates.temporalNamespace,
260
+ stigmerTokenRef: tokenRef,
205
261
  };
206
262
  markBoot("bootstrap_resolved");
207
263
 
264
+ // Cloud sandbox credential self-renewal (no-op for desktop/OSS/local
265
+ // credentials — see the helper). Started before the worker so the first
266
+ // renewal point is scheduled even if the pod boots with a part-used token.
267
+ const tokenRenewal = await startStaticSandboxTokenRenewal(config, tokenRef);
268
+
208
269
  const { setExecutionContextRef } = await import(
209
270
  "./activities/execute-cursor/rejection-capture.js"
210
271
  );
@@ -242,6 +303,7 @@ export async function createStigmerRunner(
242
303
  console.log("Worker stopped");
243
304
  },
244
305
  shutdown() {
306
+ tokenRenewal?.stop();
245
307
  worker.shutdown();
246
308
  },
247
309
  };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Self-renewal of a cloud sandbox's control-plane credential (STIGMER_TOKEN).
3
+ *
4
+ * A sandbox token is minted with a fixed TTL, but the sandbox it serves has
5
+ * no fixed lifetime: an active conversation extends a session sandbox
6
+ * indefinitely, and a long workflow run can outlast any TTL chosen at
7
+ * provisioning. Before this module, the only refresh path was the control
8
+ * plane's Secret-rewrite + pod restart — which the 2026-08-05 incident
9
+ * showed both sacrifices the turn that trips it (the old pod picks up the
10
+ * work holding the dead env token) and wipes an ephemeral sandbox's
11
+ * workspace. Renewal decouples credential lifetime from sandbox lifetime:
12
+ * the runner re-mints in-process before expiry via the getRunnerScopedToken
13
+ * `renewal` arm, and the server bounds renewability by the live sandbox
14
+ * record (a reaped sandbox's credential dies with it).
15
+ *
16
+ * Relationship to {@link createRunnerTokenCoordinator}
17
+ * (runner-token-coordinator.ts): the coordinator owns the PROXY credential
18
+ * (x-stigmer-auth) and re-mints it using the control-plane token; this
19
+ * module keeps that control-plane token itself fresh, so the two form one
20
+ * chain with no expiring root. They stay separate modules because their
21
+ * hosts differ — every proxy-mode runner has a coordinator, but only
22
+ * in-cluster sandbox runners (static cloud sandboxes and pool members) hold
23
+ * a renewable credential; the desktop's control-plane token is the user's
24
+ * own Auth0 token, refreshed by the host app.
25
+ *
26
+ * The loop re-reads the CURRENT token every cycle rather than binding to
27
+ * the boot credential, because the credential's class can change under it:
28
+ * a pool member boots with a non-renewable pool_sandbox token and swaps to
29
+ * a renewable session token at claim time (attach-session.ts). A
30
+ * non-renewable token parks the loop on a slow recheck instead of stopping
31
+ * it, so the swap is picked up without any coupling to the claim path.
32
+ */
33
+
34
+ /**
35
+ * Renew once the token has lived this fraction of its issued lifetime
36
+ * (anchored on iat/exp, the coordinator's 0.8 convention). Anchoring on the
37
+ * ISSUED lifetime matters: a fraction of the *remaining* lifetime re-arms at
38
+ * 80% of an ever-shrinking remainder and never reaches the renewal point.
39
+ */
40
+ const RENEW_AT_LIFETIME_FRACTION = 0.8;
41
+
42
+ /**
43
+ * Fallback safety margin before expiry for a token without an iat claim:
44
+ * renew this early, leaving room for two retry cycles.
45
+ */
46
+ const FALLBACK_RENEW_MARGIN_MS = 10 * 60_000;
47
+
48
+ /** Retry delay after a failed renewal — short enough to recover well before expiry. */
49
+ const RETRY_DELAY_MS = 60_000;
50
+
51
+ /** Recheck cadence while the current token is not a renewable class (pre-claim pool member). */
52
+ const RECHECK_DELAY_MS = 60_000;
53
+
54
+ /** Floor for any scheduled delay so a malformed expiry cannot spin a hot loop. */
55
+ const MIN_DELAY_MS = 5_000;
56
+
57
+ /** The token_type claims the renewal arm accepts (mirrors the server's gate). */
58
+ const RENEWABLE_TOKEN_TYPES = new Set(["sandbox", "workflow_sandbox"]);
59
+
60
+ export interface SandboxTokenRenewalOptions {
61
+ /** Reads the credential currently in effect (re-read every cycle; see module doc). */
62
+ readonly getToken: () => string | null | undefined;
63
+ /**
64
+ * Calls the getRunnerScopedToken `renewal` arm authenticated with
65
+ * {@code currentToken}. Returns undefined when the server minted nothing;
66
+ * the loop retries rather than crashing the runner.
67
+ */
68
+ readonly renew: (currentToken: string) => Promise<
69
+ { token: string; expiresInSeconds?: number } | undefined
70
+ >;
71
+ /** Applies a freshly minted credential to every sink the host wires (ref, env, interceptors). */
72
+ readonly applyToken: (token: string) => void;
73
+ /** Optional structured logger; defaults to console. */
74
+ readonly log?: Pick<typeof console, "log" | "warn">;
75
+ }
76
+
77
+ export interface SandboxTokenRenewal {
78
+ /** Stop the renewal timer. Idempotent. */
79
+ stop(): void;
80
+ }
81
+
82
+ /**
83
+ * Whether a token's decoded claims mark it as renewable through the
84
+ * `renewal` arm. Exported so a static-mode host can decide not to start
85
+ * the loop at all for a credential whose class can never change.
86
+ */
87
+ export function isRenewableSandboxToken(token: string | null | undefined): boolean {
88
+ const claims = decodeClaims(token);
89
+ return claims !== null
90
+ && RENEWABLE_TOKEN_TYPES.has(claims.tokenType ?? "")
91
+ && claims.expMs !== null;
92
+ }
93
+
94
+ /**
95
+ * Start the renewal loop. The first cycle runs immediately (it only reads
96
+ * the token and schedules; no network call unless the credential is already
97
+ * past its renewal point).
98
+ */
99
+ export function startSandboxTokenRenewal(
100
+ options: SandboxTokenRenewalOptions,
101
+ ): SandboxTokenRenewal {
102
+ const log = options.log ?? console;
103
+ let stopped = false;
104
+ let timer: ReturnType<typeof setTimeout> | null = null;
105
+
106
+ const schedule = (delayMs: number): void => {
107
+ if (stopped) {
108
+ return;
109
+ }
110
+ timer = setTimeout(() => {
111
+ void cycle();
112
+ }, Math.max(MIN_DELAY_MS, delayMs));
113
+ // Never keep the process alive solely for a token renewal.
114
+ timer.unref?.();
115
+ };
116
+
117
+ const cycle = async (): Promise<void> => {
118
+ if (stopped) {
119
+ return;
120
+ }
121
+ const token = options.getToken() ?? null;
122
+ const claims = decodeClaims(token);
123
+
124
+ if (token === null || claims === null
125
+ || !RENEWABLE_TOKEN_TYPES.has(claims.tokenType ?? "")) {
126
+ // Not (yet) a renewable credential — a pre-claim pool member, or a
127
+ // credential class this loop does not own. Park and look again.
128
+ schedule(RECHECK_DELAY_MS);
129
+ return;
130
+ }
131
+
132
+ if (claims.expMs === null) {
133
+ // A renewable-class token without an expiry is a mint-side anomaly;
134
+ // renewing "before expiry" is undefined, so park rather than spin.
135
+ log.warn("[sandbox-token-renewal] Credential carries no exp claim; parking");
136
+ schedule(RECHECK_DELAY_MS);
137
+ return;
138
+ }
139
+
140
+ const renewAtMs = claims.iatMs !== null
141
+ ? claims.iatMs + (claims.expMs - claims.iatMs) * RENEW_AT_LIFETIME_FRACTION
142
+ : claims.expMs - FALLBACK_RENEW_MARGIN_MS;
143
+ const delayMs = renewAtMs - Date.now();
144
+ if (delayMs > MIN_DELAY_MS) {
145
+ schedule(delayMs);
146
+ return;
147
+ }
148
+
149
+ // At or past the renewal point (possibly past expiry after a long pod
150
+ // pause — the attempt is still correct: the server decides).
151
+ try {
152
+ const renewed = await options.renew(token);
153
+ if (renewed) {
154
+ options.applyToken(renewed.token);
155
+ log.log("[sandbox-token-renewal] Sandbox credential renewed");
156
+ // Recompute from the fresh token (its own exp is authoritative).
157
+ schedule(MIN_DELAY_MS);
158
+ } else {
159
+ log.warn("[sandbox-token-renewal] Server minted no renewal; will retry");
160
+ schedule(RETRY_DELAY_MS);
161
+ }
162
+ } catch (err) {
163
+ log.warn(
164
+ "[sandbox-token-renewal] Renewal failed; will retry: " +
165
+ `${err instanceof Error ? err.message : err}`,
166
+ );
167
+ schedule(RETRY_DELAY_MS);
168
+ }
169
+ };
170
+
171
+ void cycle();
172
+
173
+ return {
174
+ stop(): void {
175
+ stopped = true;
176
+ if (timer) {
177
+ clearTimeout(timer);
178
+ timer = null;
179
+ }
180
+ },
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Decode the claims this module reads (token_type, iat, exp) from an
186
+ * unverified JWT. Decode-only on purpose: this is the runner's OWN
187
+ * credential, minted by the server it will present it back to —
188
+ * verification happens there.
189
+ */
190
+ function decodeClaims(
191
+ token: string | null | undefined,
192
+ ): { tokenType: string | undefined; iatMs: number | null; expMs: number | null } | null {
193
+ if (!token) {
194
+ return null;
195
+ }
196
+ const parts = token.split(".");
197
+ if (parts.length !== 3) {
198
+ return null;
199
+ }
200
+ try {
201
+ const payload = JSON.parse(
202
+ Buffer.from(parts[1], "base64url").toString("utf-8"),
203
+ ) as { token_type?: string; iat?: number; exp?: number };
204
+ return {
205
+ tokenType: payload.token_type,
206
+ iatMs: typeof payload.iat === "number" ? payload.iat * 1000 : null,
207
+ expMs: typeof payload.exp === "number" ? payload.exp * 1000 : null,
208
+ };
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
@@ -89,11 +89,23 @@ export class LocalArtifactStorage implements ArtifactStorage {
89
89
 
90
90
  export class ProxyArtifactStorage implements ArtifactStorage {
91
91
  private readonly baseUrl: string;
92
- private readonly authToken: string;
92
+ private readonly authTokenSource: ProxyAuthTokenSource;
93
93
 
94
- constructor(proxyEndpoint: string, authToken: string) {
94
+ constructor(proxyEndpoint: string, authToken: ProxyAuthTokenSource) {
95
95
  this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/artifacts`;
96
- this.authToken = authToken;
96
+ this.authTokenSource = authToken;
97
+ }
98
+
99
+ /**
100
+ * Resolve the credential per call rather than pinning the boot token: a
101
+ * cloud sandbox's control-plane token rotates in place (see
102
+ * sandbox-token-renewal.ts), and this storage lives for the pod's whole
103
+ * life — a captured string would silently 401 after the first rotation.
104
+ */
105
+ private get authToken(): string {
106
+ return typeof this.authTokenSource === "string"
107
+ ? this.authTokenSource
108
+ : (this.authTokenSource.current ?? "");
97
109
  }
98
110
 
99
111
  async upload(key: string, content: Buffer, contentType?: string): Promise<string> {
@@ -214,12 +226,18 @@ export class ProxyArtifactStorage implements ArtifactStorage {
214
226
 
215
227
  // ── Factory ──────────────────────────────────────────────────────────
216
228
 
229
+ /**
230
+ * The proxy credential, either fixed (a caller-supplied string) or live (a
231
+ * shared mutable ref, read per call — the sandbox token-renewal posture).
232
+ */
233
+ export type ProxyAuthTokenSource = string | { readonly current: string | null };
234
+
217
235
  export interface ArtifactStorageConfig {
218
236
  readonly type: ArtifactStorageType;
219
237
  readonly localPath: string;
220
238
  readonly localServeUrl: string;
221
239
  readonly proxyEndpoint: string | null;
222
- readonly proxyAuthToken: string | null;
240
+ readonly proxyAuthToken: ProxyAuthTokenSource | null;
223
241
  }
224
242
 
225
243
  export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig {
@@ -239,7 +257,11 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
239
257
  localPath: process.env.LOCAL_ARTIFACT_PATH ?? "/var/stigmer/artifacts",
240
258
  localServeUrl: process.env.LOCAL_ARTIFACT_SERVE_URL ?? "http://localhost:7235",
241
259
  proxyEndpoint: type === "proxy" ? (config.proxyEndpoint ?? null) : null,
242
- proxyAuthToken: type === "proxy" ? (config.stigmerToken ?? null) : null,
260
+ // Prefer the live ref: renewal rotates the token in place and uploads
261
+ // must present the current credential, not the boot one.
262
+ proxyAuthToken: type === "proxy"
263
+ ? (config.stigmerTokenRef ?? config.stigmerToken ?? null)
264
+ : null,
243
265
  };
244
266
  }
245
267
 
@@ -248,10 +270,13 @@ export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStora
248
270
  if (!cfg.proxyEndpoint) {
249
271
  throw new Error("Proxy artifact storage requires STIGMER_PROXY_ENDPOINT");
250
272
  }
251
- if (!cfg.proxyAuthToken) {
273
+ const tokenAtBoot = typeof cfg.proxyAuthToken === "string"
274
+ ? cfg.proxyAuthToken
275
+ : cfg.proxyAuthToken?.current;
276
+ if (!tokenAtBoot) {
252
277
  throw new Error("Proxy artifact storage requires STIGMER_TOKEN");
253
278
  }
254
- return new ProxyArtifactStorage(cfg.proxyEndpoint, cfg.proxyAuthToken);
279
+ return new ProxyArtifactStorage(cfg.proxyEndpoint, cfg.proxyAuthToken!);
255
280
  }
256
281
 
257
282
  return new LocalArtifactStorage(cfg.localPath, cfg.localServeUrl);
@@ -404,9 +404,9 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
404
404
  expect(config.message).not.toContain("${ $context");
405
405
  expect(config.message).not.toContain("${ $input");
406
406
  expect(config.env).toEqual({ GITHUB_TOKEN: "${.secrets.GITHUB_TOKEN}" });
407
- expect(config.config?.model).toBe("claude-3-5-sonnet");
408
- expect(config.config?.timeout).toBe(300);
409
- expect(config.config?.temperature).toBe(0.2);
407
+ expect(config.run_config?.model_name).toBe("claude-3-5-sonnet");
408
+ expect(config.run_config?.max_cost_usd).toBe(0.75);
409
+ expect(config.run_config?.service_tier).toBe("SERVICE_TIER_STANDARD");
410
410
  expect(config.harness).toBe("HARNESS_NATIVE");
411
411
  expect(config.output?.schema).toBeDefined();
412
412
  expect(config.output?.schema.required).toContain("severity");
@@ -428,7 +428,6 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
428
428
  " with:",
429
429
  ' agent: "stigmer/code-reviewer"',
430
430
  ' message: "Review this"',
431
- ' org: "stigmer"',
432
431
  ].join("\n");
433
432
 
434
433
  const model = loadWorkflowFromYaml(crossOrgYaml);
@@ -444,7 +443,6 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
444
443
  expect(mockCallAgent).toHaveBeenCalledOnce();
445
444
  const [config] = mockCallAgent.mock.calls[0];
446
445
  expect(config.agent).toBe("stigmer/code-reviewer");
447
- expect(config.org).toBe("stigmer");
448
446
  expect(config.message).toBe("Review this");
449
447
  });
450
448
 
@@ -1048,9 +1046,11 @@ describe("Golden Execution — Structured Output Pipeline", () => {
1048
1046
  expect(config.message).toContain("Date: 2026-05-26");
1049
1047
  expect(config.message).not.toContain("${ $env");
1050
1048
 
1051
- // Config and harness preserved
1052
- expect(config.config?.model).toBe("claude-sonnet-4");
1053
- expect(config.config?.timeout).toBe(300);
1049
+ // Run config and harness preserved. The tier arrives canonical from the
1050
+ // golden's "fast" shorthand — the loader mapping riding a full workflow
1051
+ // load (#357).
1052
+ expect(config.run_config?.model_name).toBe("claude-sonnet-4");
1053
+ expect(config.run_config?.service_tier).toBe("SERVICE_TIER_FAST");
1054
1054
  expect(config.harness).toBe("HARNESS_CURSOR");
1055
1055
  expect(metadata.taskName).toBe("analyze_player_data");
1056
1056