@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
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { StigmerClient } from "../client/stigmer-client.js";
17
- import { loadConfig } from "../config.js";
17
+ import type { Config } from "../config.js";
18
18
  import { create } from "@bufbuild/protobuf";
19
19
  import {
20
20
  CreateArtifactInputSchema,
@@ -44,20 +44,16 @@ export interface ArtifactCreatedEventDescriptor {
44
44
  occurredAt: string;
45
45
  }
46
46
 
47
- let cachedClient: StigmerClient | null = null;
47
+ export function createPromoteTaskOutputActivities(config: Config) {
48
+ // One client for the factory's life, from the runner's injected Config: it
49
+ // reads the live credential ref per request (config.ts header), so holding
50
+ // it is safe across every rotation.
51
+ const client = new StigmerClient({
52
+ endpoint: config.stigmerBackendEndpoint,
53
+ tokenRef: config.stigmerTokenRef,
54
+ runnerTokenRef: config.stigmerRunnerTokenRef,
55
+ });
48
56
 
49
- function getClient(): StigmerClient {
50
- if (!cachedClient) {
51
- const config = loadConfig();
52
- cachedClient = new StigmerClient({
53
- endpoint: config.stigmerBackendEndpoint,
54
- token: config.stigmerToken,
55
- });
56
- }
57
- return cachedClient;
58
- }
59
-
60
- export function createPromoteTaskOutputActivities() {
61
57
  return {
62
58
  /**
63
59
  * Check if a task output should be promoted to an artifact. If the
@@ -107,7 +103,7 @@ export function createPromoteTaskOutputActivities() {
107
103
  content: new Uint8Array(contentBytes),
108
104
  });
109
105
 
110
- const artifact = await getClient().createArtifact(input);
106
+ const artifact = await client.createArtifact(input);
111
107
  const artifactId = artifact.metadata?.id ?? "";
112
108
 
113
109
  return {
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { StigmerClient } from "../client/stigmer-client.js";
18
- import { loadConfig } from "../config.js";
18
+ import type { Config } from "../config.js";
19
19
  import { create, fromJson } from "@bufbuild/protobuf";
20
20
  import { ValueSchema } from "@bufbuild/protobuf/wkt";
21
21
  import {
@@ -136,14 +136,6 @@ function toJsonObject(value: unknown): JsonObject | undefined {
136
136
  }
137
137
  }
138
138
 
139
- function buildClient(): StigmerClient {
140
- const config = loadConfig();
141
- return new StigmerClient({
142
- endpoint: config.stigmerBackendEndpoint,
143
- token: config.stigmerToken,
144
- });
145
- }
146
-
147
139
  /**
148
140
  * LEGACY sequence assignment — only for descriptors that arrive without a
149
141
  * workflow-assigned `sequenceNumber`, i.e. replays of histories recorded
@@ -181,12 +173,11 @@ function nextSequence(): bigint {
181
173
  * Returns a plain number — the value crosses back into the workflow
182
174
  * through Temporal's JSON payload converter, which cannot carry BigInt.
183
175
  */
184
- export async function initSequenceFromEventLog(executionId: string): Promise<number> {
176
+ export async function initSequenceFromEventLog(client: StigmerClient, executionId: string): Promise<number> {
185
177
  if (!executionId) {
186
178
  sequenceCounter = 0;
187
179
  return 0;
188
180
  }
189
- const client = buildClient();
190
181
  const highWaterMark = await client.getEventLogHighWaterMark(executionId);
191
182
  sequenceCounter = Number(highWaterMark);
192
183
  return sequenceCounter;
@@ -417,13 +408,13 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
417
408
  * exhausted, so a broken timeline write never fails the run.
418
409
  */
419
410
  export async function emitWorkflowEvents(
411
+ client: StigmerClient,
420
412
  executionId: string,
421
413
  events: WorkflowEventDescriptor[],
422
414
  taskStatuses?: TaskStatusEntry[],
423
415
  ): Promise<void> {
424
416
  if (!executionId || events.length === 0) return;
425
417
 
426
- const client = buildClient();
427
418
  const protoEvents = events.map(toProtoEvent);
428
419
 
429
420
  const protoTasks = (taskStatuses ?? []).map(ts =>
@@ -507,8 +498,7 @@ function structToPlain(value: JsonObject | undefined): unknown {
507
498
  * Unlike emitWorkflowEvents (best-effort), this activity propagates
508
499
  * errors — recovery context loading is required for correctness.
509
500
  */
510
- export async function loadRecoveryContext(executionId: string): Promise<RecoveryTaskData[]> {
511
- const client = buildClient();
501
+ export async function loadRecoveryContext(client: StigmerClient, executionId: string): Promise<RecoveryTaskData[]> {
512
502
  const execution = await client.getWorkflowExecution(executionId);
513
503
  const tasks = execution.status?.tasks ?? [];
514
504
 
@@ -519,10 +509,19 @@ export async function loadRecoveryContext(executionId: string): Promise<Recovery
519
509
  }));
520
510
  }
521
511
 
522
- export function createWorkflowEventActivities() {
512
+ export function createWorkflowEventActivities(config: Config) {
513
+ // One client from the runner's injected Config: it reads the live credential
514
+ // ref per request (config.ts header), so a rotation reaches these writes.
515
+ const client = new StigmerClient({
516
+ endpoint: config.stigmerBackendEndpoint,
517
+ tokenRef: config.stigmerTokenRef,
518
+ runnerTokenRef: config.stigmerRunnerTokenRef,
519
+ });
520
+
523
521
  return {
524
- EmitWorkflowEvents: emitWorkflowEvents,
525
- ResetEventSequence: initSequenceFromEventLog,
526
- LoadRecoveryContext: loadRecoveryContext,
522
+ EmitWorkflowEvents: (executionId: string, events: WorkflowEventDescriptor[], taskStatuses?: TaskStatusEntry[]) =>
523
+ emitWorkflowEvents(client, executionId, events, taskStatuses),
524
+ ResetEventSequence: (executionId: string) => initSequenceFromEventLog(client, executionId),
525
+ LoadRecoveryContext: (executionId: string) => loadRecoveryContext(client, executionId),
527
526
  };
528
527
  }
@@ -93,20 +93,22 @@ describe("StigmerClient", () => {
93
93
  expect(req.header.has("authorization")).toBe(false);
94
94
  });
95
95
 
96
- it("updateToken() changes the token used on subsequent requests", async () => {
97
- const client = new StigmerClient({ endpoint: "http://localhost", token: "old" });
96
+ it("a rotation is a write to the shared ref, never a method on the client: the next request carries the new value", async () => {
97
+ const ref: TokenRef = { current: "old" };
98
+ new StigmerClient({ endpoint: "http://localhost", tokenRef: ref });
98
99
 
99
- client.updateToken("new-tok");
100
+ ref.current = "new-tok";
100
101
 
101
102
  const req = makeRequest();
102
103
  await runInterceptor(req);
103
104
  expect(req.header.get("authorization")).toBe("Bearer new-tok");
104
105
  });
105
106
 
106
- it("updateToken(null) removes the Authorization header", async () => {
107
- const client = new StigmerClient({ endpoint: "http://localhost", token: "tok" });
107
+ it("a ref rotated to null removes the Authorization header", async () => {
108
+ const ref: TokenRef = { current: "tok" };
109
+ new StigmerClient({ endpoint: "http://localhost", tokenRef: ref });
108
110
 
109
- client.updateToken(null);
111
+ ref.current = null;
110
112
 
111
113
  const req = makeRequest();
112
114
  await runInterceptor(req);
@@ -623,19 +625,6 @@ describe("StigmerClient", () => {
623
625
  });
624
626
  });
625
627
 
626
- describe("updateToken", () => {
627
- it("does not affect tokenRef-based resolution", async () => {
628
- const ref: TokenRef = { current: "from-ref" };
629
- const client = new StigmerClient({ endpoint: "http://localhost", token: null, tokenRef: ref });
630
-
631
- client.updateToken("from-update");
632
-
633
- const req = makeRequest();
634
- await runInterceptor(req);
635
- expect(req.header.get("authorization")).toBe("Bearer from-ref");
636
- });
637
- });
638
-
639
628
  describe("transport", () => {
640
629
  it("exposes transport as a readonly property", () => {
641
630
  const client = new StigmerClient({ endpoint: "http://localhost", token: null });
@@ -55,6 +55,7 @@ import { ChannelMessageQueryController } from "@stigmer/protos/ai/stigmer/agenti
55
55
  import type { ChannelTemplate, MessagingChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
56
56
  import { TOKEN_TYPE_EMBEDDED_RUNNER, tokenTypeOf } from "./token-claims.js";
57
57
  import { assertCreateRequirements, assertReferenceRequirements } from "./server-contracts.js";
58
+ import type { TokenRef } from "../config.js";
58
59
  import { currentRunCredential } from "../shared/run-credential-store.js";
59
60
 
60
61
  /**
@@ -151,31 +152,31 @@ function toRunnerScopedTokenOneof(scope: RunnerScopedTokenScope) {
151
152
  return { case: "renewal", value: create(TokenRenewalSchema) } as const;
152
153
  }
153
154
 
154
- /**
155
- * A shared mutable token reference. When provided, the interceptor
156
- * reads from this on every request, enabling token updates to
157
- * propagate to all clients sharing the same ref.
158
- */
159
- export interface TokenRef {
160
- current: string | null;
161
- }
155
+ /** The runner's shared credential reference (config.ts); re-exported for the client's callers. */
156
+ export type { TokenRef };
162
157
 
163
158
  export interface StigmerClientOptions {
164
159
  endpoint: string;
165
- token: string | null;
166
- tokenRef?: TokenRef;
160
+ /**
161
+ * A fixed credential, for a client whose credential never rotates (a
162
+ * renewal client that authenticates each call explicitly passes `null`).
163
+ * Runner activities pass {@link tokenRef} instead: the interceptor reads
164
+ * it on every request, so one rotation reaches every client sharing it.
165
+ */
166
+ token?: string | null;
167
+ tokenRef?: Readonly<TokenRef>;
167
168
  /**
168
169
  * Optional runner credential (a server-minted token with a runner-class
169
170
  * `token_type` claim). When populated, ExecutionContext reads authenticate
170
171
  * with this instead of the control-plane token — see the interceptor in the
171
172
  * constructor for why the credential differs per service.
172
173
  */
173
- runnerTokenRef?: TokenRef;
174
+ runnerTokenRef?: Readonly<TokenRef>;
174
175
  }
175
176
 
176
177
  export class StigmerClient {
177
178
  readonly transport: Transport;
178
- private currentToken: string | null;
179
+ private readonly fixedToken: string | null;
179
180
  private readonly executionQuery: Client<typeof AgentExecutionQueryController>;
180
181
  private readonly executionCommand: Client<typeof AgentExecutionCommandController>;
181
182
  private readonly executionContextQuery: Client<typeof ExecutionContextQueryController>;
@@ -195,11 +196,11 @@ export class StigmerClient {
195
196
  private readonly platformQuery: Client<typeof PlatformQueryController>;
196
197
  private readonly channelMessageQuery: Client<typeof ChannelMessageQueryController>;
197
198
 
198
- private readonly tokenRef: TokenRef | null;
199
- private readonly runnerTokenRef: TokenRef | null;
199
+ private readonly tokenRef: Readonly<TokenRef> | null;
200
+ private readonly runnerTokenRef: Readonly<TokenRef> | null;
200
201
 
201
202
  constructor(options: StigmerClientOptions) {
202
- this.currentToken = options.token;
203
+ this.fixedToken = options.token ?? null;
203
204
  this.tokenRef = options.tokenRef ?? null;
204
205
  this.runnerTokenRef = options.runnerTokenRef ?? null;
205
206
  this.transport = createGrpcTransport({
@@ -274,7 +275,7 @@ export class StigmerClient {
274
275
  (isProcessBound ? undefined : currentRunCredential())
275
276
  ?? (usesRunnerCredential ? this.runnerTokenRef?.current : null)
276
277
  ?? this.tokenRef?.current
277
- ?? this.currentToken;
278
+ ?? this.fixedToken;
278
279
  if (token) {
279
280
  req.header.set("authorization", `Bearer ${token}`);
280
281
  }
@@ -467,7 +468,7 @@ export class StigmerClient {
467
468
  }
468
469
  // Inspect the same credential chain the EC read's interceptor would use.
469
470
  const ambientTokenType = tokenTypeOf(
470
- this.runnerTokenRef?.current ?? this.tokenRef?.current ?? this.currentToken,
471
+ this.runnerTokenRef?.current ?? this.tokenRef?.current ?? this.fixedToken,
471
472
  );
472
473
  const isEmbeddedRunner = ambientTokenType === TOKEN_TYPE_EMBEDDED_RUNNER;
473
474
  if (ambientTokenType !== undefined && !isEmbeddedRunner) {
@@ -762,7 +763,4 @@ export class StigmerClient {
762
763
  return this.workflowInstanceQuery.get({ value: instanceId });
763
764
  }
764
765
 
765
- updateToken(token: string | null): void {
766
- this.currentToken = token;
767
- }
768
766
  }
package/src/config.ts CHANGED
@@ -20,6 +20,21 @@
20
20
  * - CURSOR_API_KEY is NOT required — the proxy injects it.
21
21
  * - The fetch interceptor rewrites outbound Cursor SDK requests.
22
22
  *
23
+ * The credential is never held by value:
24
+ *
25
+ * The control-plane credential rotates while the process runs (the manager's
26
+ * `updateToken` on a pool claim or a host refresh, the static root's
27
+ * sandbox-token renewal), so `Config` carries it as a shared {@link TokenRef}
28
+ * and nothing else. Every reader resolves `ref.current` at the moment of use;
29
+ * a client that makes many requests over its life holds the ref and reads it
30
+ * per request (the artifact store, the checkpoint saver). The two roots own
31
+ * the writable ref; readers see it through `Readonly`, so a rotation has one
32
+ * writer and no copy to go stale. {@link loadConfig} is the environment
33
+ * boundary and is read once, by `main.ts`: the ref it returns holds the boot
34
+ * capture that the roots turn into the live ref (a test pins that no other
35
+ * module imports it). The only by-value reads are the pool boot's, where the
36
+ * credential the pod BOOTED with is the fact (`main.ts`).
37
+ *
23
38
  * Workspace isolation:
24
39
  *
25
40
  * WORKSPACE_ROOT_DIR must always resolve to a directory that is NOT the
@@ -58,12 +73,28 @@ export { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-
58
73
  import { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
59
74
  import { getRunnerSecret } from "./shared/runner-credential-store.js";
60
75
 
76
+ /**
77
+ * A shared mutable credential reference. The root that owns a credential
78
+ * writes `current` on every rotation; every reader resolves it at the moment
79
+ * of use, so one write reaches all of them (the per-request reads in the
80
+ * Cursor interceptors, StigmerClient, the artifact store and the checkpoint
81
+ * saver). `Config` hands readers a `Readonly` view: rotation has one writer.
82
+ */
83
+ export interface TokenRef {
84
+ current: string | null;
85
+ }
86
+
61
87
  export interface Config {
62
88
  readonly taskQueue: string;
63
89
  readonly temporalAddress: string;
64
90
  readonly temporalNamespace: string;
65
91
  readonly stigmerBackendEndpoint: string;
66
- readonly stigmerToken: string | null;
92
+ /**
93
+ * The control-plane credential, read per use (the module header). The
94
+ * static root and the manager both rotate it in place; `main.ts`'s
95
+ * `loadConfig` result holds the boot capture the roots start from.
96
+ */
97
+ readonly stigmerTokenRef: Readonly<TokenRef>;
67
98
  /**
68
99
  * The MCP bridge endpoint for the runner-synthesized attachments —
69
100
  * channel messaging and conversation participation
@@ -74,14 +105,12 @@ export interface Config {
74
105
  */
75
106
  readonly mcpBridgeEndpoint: string | null;
76
107
  /**
77
- * Bearer credential for the Cursor SDK's Connect RPC transport.
78
- *
79
- * - Direct mode: the real Cursor API key (authenticates with Cursor directly).
80
- * - Proxy mode: the STIGMER_TOKEN JWT (authenticates with our BiDi proxy,
81
- * which validates it and injects the real Cursor key upstream).
82
- *
83
- * Named after the CURSOR_API_KEY env var the SDK reads, though in proxy mode
84
- * the credential authenticates with the proxy endpoint, not with Cursor.
108
+ * The Cursor API key for direct mode (authenticates with Cursor directly).
109
+ * In proxy mode this is a placeholder (`"proxy-managed"` unless the operator
110
+ * set CURSOR_API_KEY anyway): the credential the SDK transport presents to
111
+ * the proxy is the control-plane credential, read from
112
+ * {@link stigmerTokenRef} per turn by the Cursor adapter, never copied here.
113
+ * Named after the CURSOR_API_KEY env var the SDK reads.
85
114
  */
86
115
  readonly cursorApiKey: string;
87
116
  readonly workspaceRootDir: string;
@@ -136,16 +165,14 @@ export interface Config {
136
165
  * See shared/workspace/workspace-lock.ts for the serialization model.
137
166
  */
138
167
  readonly workspaceLockTimeoutMs: number;
139
- /** Shared mutable token reference for dynamic token updates (manager mode). */
140
- readonly stigmerTokenRef?: { current: string | null };
141
168
  /**
142
- * Shared mutable reference to the runner credential (manager mode): the
169
+ * Shared reference to the runner credential (the roots bind it): the
143
170
  * server-minted runner token once adopted, tracking the control-plane token
144
171
  * in lockstep before that. StigmerClient authenticates ExecutionContext
145
172
  * reads with it so the server's runner-class decrypt gate recognizes a
146
173
  * desktop runner (see stigmer-client.ts).
147
174
  */
148
- readonly stigmerRunnerTokenRef?: { current: string | null };
175
+ readonly stigmerRunnerTokenRef?: Readonly<TokenRef>;
149
176
  /**
150
177
  * Shared mutable reference to the credential the Cursor proxy transport
151
178
  * sends as `x-stigmer-auth` (the fetch and HTTP/2 interceptors read it per
@@ -158,7 +185,7 @@ export interface Config {
158
185
  * Required whenever `proxyEndpoint` is set; the Cursor adapter's
159
186
  * boot refuses a proxy without it.
160
187
  */
161
- readonly proxyTokenRef?: { current: string | null };
188
+ readonly proxyTokenRef?: Readonly<TokenRef>;
162
189
  }
163
190
 
164
191
  export function loadConfig(): Config {
@@ -186,22 +213,23 @@ export function loadConfig(): Config {
186
213
  );
187
214
 
188
215
  // Secrets resolve through the credential store, not process.env — the
189
- // boot capture has already moved them out of the environment (#508).
216
+ // boot capture has already moved them out of the environment (#508). The
217
+ // ref holds the boot capture: this function is read once, by main.ts, and
218
+ // the root it feeds builds the live ref from the value (the module header).
190
219
  const stigmerTokenValue = getRunnerSecret("STIGMER_TOKEN");
191
220
  if ((mode === "cloud" || proxyActive) && !stigmerTokenValue) {
192
221
  throw new Error("Required environment variable STIGMER_TOKEN is not set");
193
222
  }
194
- const stigmerToken = stigmerTokenValue ?? null;
223
+ const stigmerTokenRef: TokenRef = { current: stigmerTokenValue ?? null };
195
224
 
196
225
  const mcpBridgeEndpoint = process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null;
197
226
 
198
- // In proxy mode, pass STIGMER_TOKEN as the SDK's API key. The SDK exchanges
199
- // it (via REST proxy → Tomcat → Cursor) for an access token. The HTTP/2
200
- // interceptor injects x-stigmer-auth for BiDi proxy authentication while
201
- // the SDK's authorization header (Cursor access token) passes through to
202
- // api2.cursor.sh unchanged.
227
+ // In proxy mode the SDK's API key is the control-plane credential, which the
228
+ // Cursor adapter reads from the ref per turn (execute-cursor/turn-setup.ts);
229
+ // the placeholder here is the two option mappers' exact value, so the three
230
+ // construction sites agree and no copy of the credential rides in Config.
203
231
  const cursorApiKey = proxyActive
204
- ? (getRunnerSecret("CURSOR_API_KEY") ?? stigmerToken ?? "proxy-managed")
232
+ ? (getRunnerSecret("CURSOR_API_KEY") ?? "proxy-managed")
205
233
  : (getRunnerSecret("CURSOR_API_KEY") ?? "");
206
234
 
207
235
  const workspaceRootDir = resolveWorkspaceRootDir();
@@ -245,7 +273,7 @@ export function loadConfig(): Config {
245
273
  temporalAddress,
246
274
  temporalNamespace,
247
275
  stigmerBackendEndpoint,
248
- stigmerToken,
276
+ stigmerTokenRef,
249
277
  mcpBridgeEndpoint,
250
278
  cursorApiKey,
251
279
  workspaceRootDir,
@@ -104,7 +104,6 @@ export async function createHarnessActivities(
104
104
  const { StigmerClient } = await import("../client/stigmer-client.js");
105
105
  const client: StigmerClient = new StigmerClient({
106
106
  endpoint: config.stigmerBackendEndpoint,
107
- token: config.stigmerToken,
108
107
  tokenRef: config.stigmerTokenRef,
109
108
  runnerTokenRef: config.stigmerRunnerTokenRef,
110
109
  });
@@ -724,9 +724,7 @@ export async function resolveMcpServersAndPolicies(
724
724
  `reads; degrading to the ambient credential: ${err instanceof Error ? err.message : err}`,
725
725
  );
726
726
  }
727
- const attachmentCredential = exchangedRunnerToken
728
- ?? config.stigmerTokenRef?.current
729
- ?? config.stigmerToken;
727
+ const attachmentCredential = exchangedRunnerToken ?? config.stigmerTokenRef.current;
730
728
  const attachmentEndpoints = {
731
729
  bridgeEndpoint: config.mcpBridgeEndpoint,
732
730
  credential: attachmentCredential,
@@ -1004,7 +1002,7 @@ export function resolveStandingContext(
1004
1002
  const selectRecalledMemories = (): Promise<RecalledMemoriesContent | undefined> => {
1005
1003
  memorySelection ??= selectRecalledFacts(spec.recalledMemories, spec.message, {
1006
1004
  proxyEndpoint: deps.config.proxyEndpoint,
1007
- stigmerToken: deps.config.stigmerToken,
1005
+ stigmerToken: deps.config.stigmerTokenRef.current,
1008
1006
  executionId: deps.input.executionId,
1009
1007
  priorReport: execution.status?.recalledMemoriesReport,
1010
1008
  }).then((selection) => {
package/src/main.ts CHANGED
@@ -81,7 +81,7 @@ async function runManagerMode(config: import("./config.js").Config): Promise<voi
81
81
  temporalAddress: config.temporalAddress,
82
82
  temporalNamespace: config.temporalNamespace,
83
83
  stigmerEndpoint: config.stigmerBackendEndpoint,
84
- stigmerToken: config.stigmerToken ?? undefined,
84
+ stigmerToken: config.stigmerTokenRef.current ?? undefined,
85
85
  cursorApiKey: config.cursorApiKey || undefined,
86
86
  workspaceRootDir: config.workspaceRootDir,
87
87
  maxConcurrentActivitiesPerSession: config.maxConcurrentActivities,
@@ -197,7 +197,14 @@ async function runPoolMode(
197
197
  // process serves (the control plane rewrites the Secret at claim time, and
198
198
  // secretKeyRef env is read only at pod start — so a restart lands here with
199
199
  // the post-claim identity). Decide before any expensive boot work.
200
- const intent = decidePoolBoot(config.stigmerToken);
200
+ //
201
+ // Read by value on purpose — the one place the runner may: the credential
202
+ // the pod BOOTED with is the fact here. Its class decides the mode, and the
203
+ // attach exchange must authenticate with the pool credential after the
204
+ // claim has rotated the live ref to the session's (pool-member.ts,
205
+ // PoolMemberContext.poolToken). Everything after this line reads the ref.
206
+ const bootToken = config.stigmerTokenRef.current;
207
+ const intent = decidePoolBoot(bootToken);
201
208
  if (intent.kind === "invalid") {
202
209
  throw new Error(`Pool member ${memberId} cannot boot: ${intent.reason}`);
203
210
  }
@@ -206,7 +213,7 @@ async function runPoolMode(
206
213
  temporalAddress: config.temporalAddress,
207
214
  temporalNamespace: config.temporalNamespace,
208
215
  stigmerEndpoint: config.stigmerBackendEndpoint,
209
- stigmerToken: config.stigmerToken ?? undefined,
216
+ stigmerToken: config.stigmerTokenRef.current ?? undefined,
210
217
  cursorApiKey: config.cursorApiKey || undefined,
211
218
  workspaceRootDir: config.workspaceRootDir,
212
219
  maxConcurrentActivitiesPerSession: config.maxConcurrentActivities,
@@ -243,7 +250,7 @@ async function runPoolMode(
243
250
  if (intent.kind === "pool-control") {
244
251
  registerPoolMemberContext({
245
252
  memberId,
246
- poolToken: config.stigmerToken!,
253
+ poolToken: bootToken!,
247
254
  manager,
248
255
  });
249
256
  await manager.addPoolControl(memberId);
@@ -332,7 +339,7 @@ async function runStaticMode(config: import("./config.js").Config): Promise<void
332
339
  temporalAddress: config.temporalAddress,
333
340
  temporalNamespace: config.temporalNamespace,
334
341
  stigmerEndpoint: config.stigmerBackendEndpoint,
335
- stigmerToken: config.stigmerToken ?? undefined,
342
+ stigmerToken: config.stigmerTokenRef.current ?? undefined,
336
343
  cursorApiKey: config.cursorApiKey || undefined,
337
344
  workspaceRootDir: config.workspaceRootDir,
338
345
  maxConcurrentActivities: config.maxConcurrentActivities,
@@ -495,7 +502,7 @@ async function main(): Promise<void> {
495
502
  : "static";
496
503
  console.warn(
497
504
  `[runner] mode=${runnerMode}, proxy=${config.proxyEndpoint ?? "none"}, ` +
498
- `hasToken=${!!config.stigmerToken}, workspace=${config.workspaceRootDir}, ` +
505
+ `hasToken=${!!config.stigmerTokenRef.current}, workspace=${config.workspaceRootDir}, ` +
499
506
  `taskQueue=${config.taskQueue}`,
500
507
  );
501
508
 
@@ -22,7 +22,7 @@ import {
22
22
  type InjectedSinks,
23
23
  type WorkflowBundleOption,
24
24
  } from "@temporalio/worker";
25
- import type { Config } from "./config.js";
25
+ import type { Config, TokenRef } from "./config.js";
26
26
  import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
27
27
  // Boot marks mirror the static runner's segment names where the work is the
28
28
  // same. Inert unless a mode emits the boot timeline (pool mode does; the
@@ -237,7 +237,7 @@ export async function createStigmerRunnerManager(
237
237
  // separately by a RunnerTokenCoordinator (created after the harnesses boot)
238
238
  // — see that module for the two-writer rationale and the staleness history
239
239
  // it guards.
240
- const tokenRef = { current: options.stigmerToken ?? null };
240
+ const tokenRef: TokenRef = { current: options.stigmerToken ?? null };
241
241
  // `runnerTokenRef` mirrors the coordinator's proxy credential for gRPC use:
242
242
  // the minted runner token (token_type=embedded_runner) once adopted, tracking
243
243
  // the control-plane token in lockstep before that. StigmerClient authenticates
@@ -246,7 +246,7 @@ export async function createStigmerRunnerManager(
246
246
  // is the user's own Auth0 token, which the server treats as a browsing user
247
247
  // and answers with redacted secrets. Pre-mint the ref equals the control-plane
248
248
  // token, which is exactly what the client would fall back to anyway.
249
- const runnerTokenRef = { current: options.stigmerToken ?? null };
249
+ const runnerTokenRef: TokenRef = { current: options.stigmerToken ?? null };
250
250
  const baseConfig = mapManagerOptionsToConfig(options, tokenRef, runnerTokenRef);
251
251
 
252
252
  assertLlmBackendsPreflight(baseConfig.proxyEndpoint);
@@ -582,11 +582,16 @@ export async function createStigmerRunnerManager(
582
582
  },
583
583
 
584
584
  updateToken(token: string | null): void {
585
- // Writes the control-plane credential. The coordinator decides whether the
586
- // proxy credential follows it: only when no token has been minted (so the
587
- // pre-mint lockstep is preserved), never once the runner owns a minted
588
- // token. See runner-token-coordinator.ts and the staleness changelogs.
589
- // The credential store is the second leg of the pair (it replaced the
585
+ // Writes the control-plane credential. Every reader follows this one
586
+ // write: the activity clients, the artifact store and the checkpoint
587
+ // saver read `tokenRef` per request, the per-turn model clients read it
588
+ // at build, and nothing holds the value (Config carries the ref alone;
589
+ // config.ts header). The coordinator decides whether the proxy
590
+ // credential follows it: only when no token has been minted (so the
591
+ // pre-mint lockstep is preserved — the in-cluster sandbox case, where
592
+ // nothing is ever minted), never once the runner owns a minted token.
593
+ // See runner-token-coordinator.ts and the staleness changelogs. The
594
+ // credential store is the second leg of the pair (it replaced the
590
595
  // process.env.STIGMER_TOKEN write, #508): per-call readers like
591
596
  // call-llm and the registry headers resolve the current token there.
592
597
  tokenRef.current = token;
@@ -677,10 +682,16 @@ function validateManagerOptions(options: RunnerManagerOptions): void {
677
682
  // explicit address nor a token is present, discovery falls back to localhost.
678
683
  }
679
684
 
685
+ /**
686
+ * The runtime `Config` for the manager root. The refs are the root's: it
687
+ * creates them before mapping and writes them on every rotation
688
+ * (`updateToken`, the token coordinator), so a `Config` never carries a
689
+ * credential by value (config.ts header).
690
+ */
680
691
  export function mapManagerOptionsToConfig(
681
692
  options: RunnerManagerOptions,
682
- tokenRef?: { current: string | null },
683
- runnerTokenRef?: { current: string | null },
693
+ tokenRef: TokenRef,
694
+ runnerTokenRef: TokenRef,
684
695
  ): Config {
685
696
  const proxyActive = !!options.proxyEndpoint;
686
697
 
@@ -697,7 +708,6 @@ export function mapManagerOptionsToConfig(
697
708
  temporalAddress: options.temporalAddress ?? "",
698
709
  temporalNamespace: options.temporalNamespace ?? "default",
699
710
  stigmerBackendEndpoint: normalizeEndpoint(options.stigmerEndpoint),
700
- stigmerToken: options.stigmerToken ?? null,
701
711
  // Env-only like the env-loaded path (loadConfig): the bridge endpoint
702
712
  // is deployment topology, not per-session state.
703
713
  mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
@@ -791,14 +801,14 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
791
801
  ...createEvaluateExpressionsActivities(),
792
802
  ...createCallHttpActivities(),
793
803
  ...createCallGrpcActivities(),
794
- ...createCallFunctionActivities(),
804
+ ...createCallFunctionActivities(config),
795
805
  ...createCallLlmActivities(),
796
806
  ...createCallAgentActivities(config),
797
- ...createCallAgentStatusActivities(),
807
+ ...createCallAgentStatusActivities(config),
798
808
  ...createRunCommandActivities(),
799
809
  ...createHydrateWorkflowActivities(config),
800
- ...createWorkflowEventActivities(),
801
- ...createPromoteTaskOutputActivities(),
810
+ ...createWorkflowEventActivities(config),
811
+ ...createPromoteTaskOutputActivities(config),
802
812
  ...createAttachSessionActivities(config),
803
813
  };
804
814
  }
@@ -7,9 +7,14 @@
7
7
  * Auth0 token), used for Authorization on control-plane gRPC; and
8
8
  * - the proxy token (x-stigmer-auth), used for Cursor-proxy traffic.
9
9
  *
10
- * On most hosts these are the same token. On cloud the control plane mints a
11
- * dedicated iss=stigmer proxy token during bootstrap; from that point the proxy
12
- * token is a *separate* credential with its own TTL, owned and refreshed here.
10
+ * On most hosts these are the same token. When a manager boots WITHOUT a
11
+ * pinned Temporal address (the desktop runner), bootstrap discovery dials the
12
+ * control plane and it mints a dedicated iss=stigmer proxy token; from that
13
+ * point the proxy token is a *separate* credential with its own TTL, owned and
14
+ * refreshed here. An in-cluster sandbox pod pins its Temporal address, so its
15
+ * bootstrap returns before any mint and nothing is ever minted: its proxy
16
+ * credential stays the control-plane credential, in lockstep, for the pod's
17
+ * whole life — the claim of a pool member rotates both with one write.
13
18
  *
14
19
  * This coordinator exists because the proxy token's freshness was the subject of
15
20
  * two production fixes (stigmer-cloud _changelog 2026-05-26 and 2026-06-01):