@stigmer/runner 3.5.3 → 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 (101) 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/discover-mcp-server.js +9 -1
  5. package/dist/activities/discover-mcp-server.js.map +1 -1
  6. package/dist/activities/execute-cursor/index.d.ts +5 -0
  7. package/dist/activities/execute-cursor/index.js +88 -14
  8. package/dist/activities/execute-cursor/index.js.map +1 -1
  9. package/dist/activities/execute-cursor/model-pricing.d.ts +9 -0
  10. package/dist/activities/execute-cursor/model-pricing.js +19 -0
  11. package/dist/activities/execute-cursor/model-pricing.js.map +1 -1
  12. package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -0
  13. package/dist/activities/execute-cursor/prompt-builder.js +9 -0
  14. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  15. package/dist/activities/execute-cursor/service-tier.d.ts +68 -0
  16. package/dist/activities/execute-cursor/service-tier.js +187 -0
  17. package/dist/activities/execute-cursor/service-tier.js.map +1 -0
  18. package/dist/activities/execute-cursor/session-lifecycle.d.ts +16 -1
  19. package/dist/activities/execute-cursor/session-lifecycle.js +12 -4
  20. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  21. package/dist/activities/execute-cursor/usage-accumulator.d.ts +21 -1
  22. package/dist/activities/execute-cursor/usage-accumulator.js +23 -3
  23. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +6 -0
  25. package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
  26. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/setup.js +45 -9
  28. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  29. package/dist/client/stigmer-client.d.ts +32 -1
  30. package/dist/client/stigmer-client.js +42 -2
  31. package/dist/client/stigmer-client.js.map +1 -1
  32. package/dist/main.js +18 -0
  33. package/dist/main.js.map +1 -1
  34. package/dist/runner.js +48 -0
  35. package/dist/runner.js.map +1 -1
  36. package/dist/sandbox-token-renewal.d.ts +65 -0
  37. package/dist/sandbox-token-renewal.js +169 -0
  38. package/dist/sandbox-token-renewal.js.map +1 -0
  39. package/dist/shared/artifact-storage.d.ts +17 -3
  40. package/dist/shared/artifact-storage.js +22 -4
  41. package/dist/shared/artifact-storage.js.map +1 -1
  42. package/dist/shared/caller-identity.d.ts +89 -0
  43. package/dist/shared/caller-identity.js +124 -0
  44. package/dist/shared/caller-identity.js.map +1 -0
  45. package/dist/shared/channel-attachment.d.ts +85 -0
  46. package/dist/shared/channel-attachment.js +203 -0
  47. package/dist/shared/channel-attachment.js.map +1 -0
  48. package/dist/shared/datastore-attachment.d.ts +2 -25
  49. package/dist/shared/datastore-attachment.js +1 -28
  50. package/dist/shared/datastore-attachment.js.map +1 -1
  51. package/dist/shared/synthesized-attachment.d.ts +51 -0
  52. package/dist/shared/synthesized-attachment.js +45 -0
  53. package/dist/shared/synthesized-attachment.js.map +1 -0
  54. package/dist/workflow-engine/loader.js +99 -2
  55. package/dist/workflow-engine/loader.js.map +1 -1
  56. package/dist/workflow-engine/tasks/call-agent.d.ts +0 -2
  57. package/dist/workflow-engine/tasks/call-agent.js +0 -2
  58. package/dist/workflow-engine/tasks/call-agent.js.map +1 -1
  59. package/dist/workflow-engine/types.d.ts +39 -7
  60. package/dist/workflow-engine/types.js.map +1 -1
  61. package/dist/workflows/call-agent-orchestrator.d.ts +3 -2
  62. package/dist/workflows/call-agent-orchestrator.js +8 -2
  63. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/__test-utils__/mock-client.ts +4 -0
  66. package/src/__tests__/sandbox-token-renewal.test.ts +174 -0
  67. package/src/activities/__tests__/call-agent-contracts.test.ts +4 -4
  68. package/src/activities/__tests__/call-agent.test.ts +219 -4
  69. package/src/activities/__tests__/discover-mcp-server.test.ts +49 -0
  70. package/src/activities/call-agent.ts +94 -10
  71. package/src/activities/discover-mcp-server.ts +13 -1
  72. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +20 -0
  73. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +170 -0
  74. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +87 -1
  75. package/src/activities/execute-cursor/index.ts +121 -20
  76. package/src/activities/execute-cursor/model-pricing.ts +23 -0
  77. package/src/activities/execute-cursor/prompt-builder.ts +19 -0
  78. package/src/activities/execute-cursor/service-tier.ts +244 -0
  79. package/src/activities/execute-cursor/session-lifecycle.ts +33 -5
  80. package/src/activities/execute-cursor/usage-accumulator.ts +35 -3
  81. package/src/activities/execute-deep-agent/prompt-builder.ts +10 -0
  82. package/src/activities/execute-deep-agent/setup.ts +66 -10
  83. package/src/client/stigmer-client.ts +57 -4
  84. package/src/main.ts +20 -0
  85. package/src/runner.ts +62 -0
  86. package/src/sandbox-token-renewal.ts +212 -0
  87. package/src/shared/__tests__/caller-identity.test.ts +159 -0
  88. package/src/shared/__tests__/channel-attachment.test.ts +276 -0
  89. package/src/shared/__tests__/datastore-attachment.test.ts +4 -4
  90. package/src/shared/artifact-storage.ts +32 -7
  91. package/src/shared/caller-identity.ts +161 -0
  92. package/src/shared/channel-attachment.ts +237 -0
  93. package/src/shared/datastore-attachment.ts +2 -54
  94. package/src/shared/synthesized-attachment.ts +77 -0
  95. package/src/workflow-engine/__tests__/golden-execution.test.ts +8 -8
  96. package/src/workflow-engine/__tests__/loader.test.ts +192 -7
  97. package/src/workflow-engine/__tests__/tasks/call-agent.test.ts +9 -9
  98. package/src/workflow-engine/loader.ts +113 -2
  99. package/src/workflow-engine/tasks/call-agent.ts +0 -2
  100. package/src/workflow-engine/types.ts +40 -7
  101. package/src/workflows/call-agent-orchestrator.ts +8 -2
@@ -40,7 +40,12 @@ import { mkdirSync } from "node:fs";
40
40
  import { join } from "node:path";
41
41
 
42
42
  import { Agent } from "@cursor/sdk";
43
- import type { SDKAgent, CursorAgentPlatformOptions, AgentDefinition } from "@cursor/sdk";
43
+ import type {
44
+ SDKAgent,
45
+ CursorAgentPlatformOptions,
46
+ AgentDefinition,
47
+ ModelParameterValue,
48
+ } from "@cursor/sdk";
44
49
  import { withTimeout, TimeoutError } from "../../shared/with-timeout.js";
45
50
  import type { CursorMcpServerConfig } from "./mcp-resolver.js";
46
51
 
@@ -83,6 +88,15 @@ const LOCAL_SETTING_SOURCES = ["project"] as const;
83
88
  export interface CreateAgentOptions {
84
89
  apiKey: string;
85
90
  model: string;
91
+ /**
92
+ * Explicit variant parameters sent with the model selection on every
93
+ * create AND resume (stigmer/stigmer#357). A bare `{ id }` lets the
94
+ * Cursor catalog's default variant pick the price, so the caller always
95
+ * supplies the pinned params from resolveServiceTierParams — possibly
96
+ * empty (Auto, or a model with no price-bearing parameters), but never
97
+ * absent by accident.
98
+ */
99
+ modelParams?: ModelParameterValue[];
86
100
  workspaceDirs: string[];
87
101
  sessionId: string;
88
102
  /** Durable workspace volume root; the SDK state store lives under it. */
@@ -112,6 +126,8 @@ export interface ResumeAgentOptions {
112
126
  /** Durable workspace volume root; the SDK state store lives under it. */
113
127
  workspaceRootDir: string;
114
128
  model?: string;
129
+ /** Explicit variant parameters — see {@link CreateAgentOptions.modelParams}. */
130
+ modelParams?: ModelParameterValue[];
115
131
  mcpServers?: Record<string, CursorMcpServerConfig>;
116
132
  /** Custom sub-agents — see {@link CreateAgentOptions.agents}. */
117
133
  agents?: Record<string, AgentDefinition>;
@@ -129,6 +145,8 @@ export interface CloudRepo {
129
145
  export interface CreateCloudAgentOptions {
130
146
  apiKey: string;
131
147
  model?: string;
148
+ /** Explicit variant parameters — see {@link CreateAgentOptions.modelParams}. */
149
+ modelParams?: ModelParameterValue[];
132
150
  repos: CloudRepo[];
133
151
  sessionId: string;
134
152
  mcpServers?: Record<string, CursorMcpServerConfig>;
@@ -140,6 +158,8 @@ export interface ResumeCloudAgentOptions {
140
158
  apiKey: string;
141
159
  agentId: string;
142
160
  model?: string;
161
+ /** Explicit variant parameters — see {@link CreateAgentOptions.modelParams}. */
162
+ modelParams?: ModelParameterValue[];
143
163
  mcpServers?: Record<string, CursorMcpServerConfig>;
144
164
  /** Custom sub-agents — see {@link CreateAgentOptions.agents}. */
145
165
  agents?: Record<string, AgentDefinition>;
@@ -245,7 +265,9 @@ export async function createAgent(options: CreateAgentOptions): Promise<SDKAgent
245
265
 
246
266
  return Agent.create({
247
267
  apiKey: options.apiKey,
248
- model: { id: options.model },
268
+ // Always a full selection — id AND params. A bare { id } would let the
269
+ // catalog's default variant (account-influenced) pick the price (#357).
270
+ model: { id: options.model, params: options.modelParams },
249
271
  local: { cwd, settingSources: [...LOCAL_SETTING_SOURCES] },
250
272
  mcpServers: options.mcpServers as Record<string, any>,
251
273
  agents: options.agents,
@@ -273,7 +295,11 @@ export async function resumeAgent(options: ResumeAgentOptions): Promise<SDKAgent
273
295
 
274
296
  return Agent.resume(options.agentId, {
275
297
  apiKey: options.apiKey,
276
- model: options.model ? { id: options.model } : undefined,
298
+ // Variant params must be re-supplied on resume exactly like mcpServers:
299
+ // explicit params hold across resume (verified against the billing
300
+ // ledger, #357), but an id-only resume would fall back to the catalog
301
+ // default variant for the new turns.
302
+ model: options.model ? { id: options.model, params: options.modelParams } : undefined,
277
303
  // Neither cwd nor settingSources survive Agent.resume(); both must be
278
304
  // re-supplied every turn. Omitting cwd makes the SDK fall back to
279
305
  // process.cwd(), which re-roots the agent in the runner's own working
@@ -307,7 +333,7 @@ export async function createCloudAgent(options: CreateCloudAgentOptions): Promis
307
333
 
308
334
  return Agent.create({
309
335
  apiKey: options.apiKey,
310
- model: options.model ? { id: options.model } : undefined,
336
+ model: options.model ? { id: options.model, params: options.modelParams } : undefined,
311
337
  cloud: { repos: options.repos },
312
338
  mcpServers: options.mcpServers as Record<string, any>,
313
339
  agents: options.agents,
@@ -328,7 +354,7 @@ export async function resumeCloudAgent(options: ResumeCloudAgentOptions): Promis
328
354
 
329
355
  return Agent.resume(options.agentId, {
330
356
  apiKey: options.apiKey,
331
- model: options.model ? { id: options.model } : undefined,
357
+ model: options.model ? { id: options.model, params: options.modelParams } : undefined,
332
358
  mcpServers: options.mcpServers as Record<string, any>,
333
359
  agents: options.agents,
334
360
  });
@@ -372,6 +398,7 @@ export async function resolveAgent(
372
398
  apiKey: options.apiKey,
373
399
  agentId: harnessStateId,
374
400
  model: options.model,
401
+ modelParams: options.modelParams,
375
402
  mcpServers: options.mcpServers,
376
403
  agents: options.agents,
377
404
  })
@@ -382,6 +409,7 @@ export async function resolveAgent(
382
409
  workspaceDirs: (options as CreateAgentOptions).workspaceDirs,
383
410
  workspaceRootDir: (options as CreateAgentOptions).workspaceRootDir,
384
411
  model: options.model,
412
+ modelParams: options.modelParams,
385
413
  mcpServers: options.mcpServers,
386
414
  agents: options.agents,
387
415
  });
@@ -13,7 +13,10 @@
13
13
  * wire via ProxyUsageReporter once traffic routing (Task 5B) is complete.
14
14
  */
15
15
 
16
- import { getCursorModelPricing, computeTurnCost } from "./model-pricing.js";
16
+ import type { ModelParameterValue } from "@cursor/sdk";
17
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
18
+
19
+ import { getCursorModelPricingForVariant, computeTurnCost } from "./model-pricing.js";
17
20
 
18
21
  export interface TurnUsage {
19
22
  readonly inputTokens?: number;
@@ -32,6 +35,10 @@ export interface UsageSnapshot {
32
35
  readonly estimatedCostUsd: number;
33
36
  readonly model: string;
34
37
  readonly observedAt: string;
38
+ /** Tier the runner requested — always explicit post-translation (#357). */
39
+ readonly requestedServiceTier: ServiceTier;
40
+ /** JSON-encoded ModelSelection.params the runner sent; "" when none. */
41
+ readonly requestedModelParams: string;
35
42
  }
36
43
 
37
44
  const EMPTY_SNAPSHOT: UsageSnapshot = {
@@ -44,6 +51,8 @@ const EMPTY_SNAPSHOT: UsageSnapshot = {
44
51
  estimatedCostUsd: 0,
45
52
  model: "",
46
53
  observedAt: "",
54
+ requestedServiceTier: ServiceTier.UNSPECIFIED,
55
+ requestedModelParams: "",
47
56
  };
48
57
 
49
58
  export interface TurnRecord {
@@ -64,7 +73,22 @@ export class UsageAccumulator {
64
73
  private observedAt = "";
65
74
  private readonly turnRecords: TurnRecord[] = [];
66
75
 
67
- constructor(private readonly model: string) {}
76
+ /** JSON-encoded params the runner sent with the model selection (#357). */
77
+ private readonly requestedModelParams: string;
78
+
79
+ constructor(
80
+ private readonly model: string,
81
+ /**
82
+ * The explicit tier the runner requested from the provider. Recorded
83
+ * verbatim into status as the audit trail that the account default was
84
+ * never left in control (#357).
85
+ */
86
+ private readonly requestedServiceTier: ServiceTier = ServiceTier.UNSPECIFIED,
87
+ requestedModelParams: readonly ModelParameterValue[] = [],
88
+ ) {
89
+ this.requestedModelParams =
90
+ requestedModelParams.length > 0 ? JSON.stringify(requestedModelParams) : "";
91
+ }
68
92
 
69
93
  addTurn(usage: TurnUsage): void {
70
94
  const input = usage.inputTokens ?? 0;
@@ -86,7 +110,13 @@ export class UsageAccumulator {
86
110
  cacheWriteTokens: cacheWrite,
87
111
  });
88
112
 
89
- const pricing = getCursorModelPricing(this.model);
113
+ // Estimate at the rates of the variant we explicitly requested — a
114
+ // FAST run priced at base rates would understate the display estimate
115
+ // ~4x relative to the authoritative bill (#357).
116
+ const pricing = getCursorModelPricingForVariant(
117
+ this.model,
118
+ this.requestedServiceTier === ServiceTier.FAST ? "fast" : null,
119
+ );
90
120
  this.estimatedCostUsd += computeTurnCost(
91
121
  pricing, input, output, cacheWrite, cacheRead,
92
122
  );
@@ -122,6 +152,8 @@ export class UsageAccumulator {
122
152
  estimatedCostUsd: this.estimatedCostUsd,
123
153
  model: this.model,
124
154
  observedAt: this.observedAt,
155
+ requestedServiceTier: this.requestedServiceTier,
156
+ requestedModelParams: this.requestedModelParams,
125
157
  };
126
158
  }
127
159
  }
@@ -98,6 +98,12 @@ export interface PromptBuilderInput {
98
98
  * formatDatastoresSection); empty when the agent uses no datastores.
99
99
  */
100
100
  datastoresPromptSection?: string;
101
+ /**
102
+ * The `<available_channel_templates>` section
103
+ * (shared/channel-attachment.ts formatChannelTemplatesSection); absent
104
+ * when the agent serves no proactive channel or nothing is sendable.
105
+ */
106
+ channelTemplatesPromptSection?: string;
101
107
  workspaceFileRefs: string[];
102
108
  workspaceRoot: string;
103
109
  injectedFiles: InjectedFile[];
@@ -174,6 +180,10 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
174
180
  prompt += "\n\n" + input.datastoresPromptSection;
175
181
  }
176
182
 
183
+ if (input.channelTemplatesPromptSection) {
184
+ prompt += "\n\n" + input.channelTemplatesPromptSection;
185
+ }
186
+
177
187
  if (input.workspaceFileRefs.length > 0) {
178
188
  const refSection = buildReferencedFilesSection(
179
189
  input.workspaceFileRefs,
@@ -26,6 +26,10 @@ import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js
26
26
  import { createCheckpointer } from "../../shared/checkpointer/factory.js";
27
27
  import { readContextBridge } from "../../shared/context-bridge.js";
28
28
  import { readSenderIdentity } from "../../shared/sender-identity.js";
29
+ import {
30
+ injectCallerIdentityEnv,
31
+ resolveCallerIdentity,
32
+ } from "../../shared/caller-identity.js";
29
33
  import { readSessionContext } from "../../shared/session-context.js";
30
34
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
31
35
  import { resolveMcpServers } from "../../shared/mcp-resolver.js";
@@ -33,9 +37,14 @@ import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js"
33
37
  import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
34
38
  import {
35
39
  formatDatastoresSection,
36
- injectDatastoreAttachment,
37
40
  synthesizeDatastoreAttachment,
38
41
  } from "../../shared/datastore-attachment.js";
42
+ import {
43
+ discoverChannelMessaging,
44
+ formatChannelTemplatesSection,
45
+ synthesizeChannelAttachment,
46
+ } from "../../shared/channel-attachment.js";
47
+ import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
39
48
  import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
40
49
  import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
41
50
  import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
@@ -334,12 +343,41 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
334
343
  ];
335
344
  const datastoreUsages = agent.spec!.datastoreUsages || [];
336
345
 
346
+ // The synthesized attachments' credential story (DD-006 D4): the
347
+ // exchanged token authenticates the discovery reads per-call (the
348
+ // messaging reach refuses a desktop runner's ambient embedded_runner
349
+ // credential; undefined lets the ambient credential apply). The
350
+ // attachment header falls back to the ambient credential.
351
+ const exchangedRunnerToken =
352
+ await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
353
+ const attachmentCredential = exchangedRunnerToken
354
+ ?? config.stigmerTokenRef?.current
355
+ ?? config.stigmerToken;
356
+
357
+ // The channel discovery read runs BEFORE the MCP gate below: an
358
+ // agent whose ONLY tool source is a proactive channel would
359
+ // otherwise never enter MCP resolution and never connect the
360
+ // attachment (DD-006 D7). Every failure mode degrades to an empty
361
+ // answer — no tool, no section, execution unharmed.
362
+ const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
363
+
337
364
  let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
338
- if (mcpServerUsages.length > 0 || datastoreUsages.length > 0) {
365
+ if (mcpServerUsages.length > 0 || datastoreUsages.length > 0 || channelMessaging.length > 0) {
339
366
  await reportSetupProgress(client, executionId, "Connecting tools…");
340
367
  const transportPosture = resolveMcpTransportPosture(config.mode);
368
+ // The MCP-bound env map (and ONLY it) carries the reserved
369
+ // caller-identity keys — mirror of the Cursor harness's Phase 4
370
+ // injection; filterEnvToDeclaredKeys keeps undeclared servers blind.
371
+ const mcpEnvVars = injectCallerIdentityEnv(
372
+ envResult.mergedEnvVars,
373
+ resolveCallerIdentity(
374
+ session.spec!.metadata,
375
+ session.status?.audit?.specAudit?.createdBy,
376
+ ),
377
+ sessionId,
378
+ );
341
379
  resolvedMcpServers = await resolveMcpServers(
342
- client, mcpServerUsages, envResult.mergedEnvVars, transportPosture,
380
+ client, mcpServerUsages, mcpEnvVars, transportPosture,
343
381
  );
344
382
  timing.mark("resolve_mcp_servers");
345
383
 
@@ -348,7 +386,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
348
386
  client,
349
387
  resolvedMcpServers.resolvedServers,
350
388
  mcpServerUsages,
351
- envResult.mergedEnvVars,
389
+ mcpEnvVars,
352
390
  sessionOrg,
353
391
  transportPosture,
354
392
  undefined,
@@ -360,17 +398,30 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
360
398
  // approval maps keep it approval-free by construction (see
361
399
  // shared/datastore-attachment.ts).
362
400
  if (datastoreUsages.length > 0) {
363
- const scopedCredential =
364
- (await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
365
- ?? config.stigmerTokenRef?.current
366
- ?? config.stigmerToken;
367
401
  const attachment = synthesizeDatastoreAttachment(datastoreUsages, {
368
402
  bridgeEndpoint: config.mcpBridgeEndpoint,
369
- credential: scopedCredential,
403
+ credential: attachmentCredential,
404
+ backendEndpoint: config.stigmerBackendEndpoint,
405
+ });
406
+ if (attachment) {
407
+ backfilledServers = injectSynthesizedAttachment(
408
+ backfilledServers, attachment, "datastore records",
409
+ );
410
+ }
411
+ }
412
+
413
+ // The channel messaging attachment (DD-006 D7/D8) — the records
414
+ // attachment's twin, injected under the same after-backfill rule.
415
+ if (channelMessaging.length > 0) {
416
+ const attachment = synthesizeChannelAttachment(channelMessaging, {
417
+ bridgeEndpoint: config.mcpBridgeEndpoint,
418
+ credential: attachmentCredential,
370
419
  backendEndpoint: config.stigmerBackendEndpoint,
371
420
  });
372
421
  if (attachment) {
373
- backfilledServers = injectDatastoreAttachment(backfilledServers, attachment);
422
+ backfilledServers = injectSynthesizedAttachment(
423
+ backfilledServers, attachment, "channel messaging",
424
+ );
374
425
  }
375
426
  }
376
427
  resolvedMcpServers = { resolvedServers: backfilledServers };
@@ -445,6 +496,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
445
496
  datastoresPromptSection: datastoreUsages.length > 0
446
497
  ? formatDatastoresSection(datastoreUsages)
447
498
  : undefined,
499
+ // "" (nothing sendable) threads as undefined: the tool alone still
500
+ // serves text sends inside a 24-hour window (DD-006 D6).
501
+ channelTemplatesPromptSection: channelMessaging.length > 0
502
+ ? formatChannelTemplatesSection(channelMessaging) || undefined
503
+ : undefined,
448
504
  workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
449
505
  workspaceRoot: workspaceBackend.rootDir,
450
506
  injectedFiles,
@@ -49,7 +49,9 @@ 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
+ import { ChannelMessageQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_query_pb";
54
+ import type { ChannelTemplate, MessagingChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
53
55
  import { isEmbeddedRunnerToken } from "./token-claims.js";
54
56
  import { assertCreateRequirements, assertReferenceRequirements } from "./server-contracts.js";
55
57
 
@@ -92,12 +94,16 @@ export interface RunnerScopedToken {
92
94
  * `poolClaimSessionId` is the warm-pool attach exchange: a pool sandbox
93
95
  * presenting its pool_sandbox credential for the session it was claimed for
94
96
  * (the server authorizes against the claim record, not the caller's FGA
95
- * 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.
96
101
  */
97
102
  export type RunnerScopedTokenScope =
98
103
  | { agentExecutionId: string }
99
104
  | { workflowExecutionId: string }
100
- | { poolClaimSessionId: string };
105
+ | { poolClaimSessionId: string }
106
+ | { renewal: true };
101
107
 
102
108
  /**
103
109
  * Map the scope union onto the proto oneof init shape. The narrowing chain is
@@ -111,7 +117,10 @@ function toRunnerScopedTokenOneof(scope: RunnerScopedTokenScope) {
111
117
  if ("workflowExecutionId" in scope) {
112
118
  return { case: "workflowExecutionId", value: scope.workflowExecutionId } as const;
113
119
  }
114
- 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;
115
124
  }
116
125
 
117
126
  /**
@@ -156,6 +165,7 @@ export class StigmerClient {
156
165
  private readonly workflowQuery: Client<typeof WorkflowQueryController>;
157
166
  private readonly workflowInstanceQuery: Client<typeof WorkflowInstanceQueryController>;
158
167
  private readonly platformQuery: Client<typeof PlatformQueryController>;
168
+ private readonly channelMessageQuery: Client<typeof ChannelMessageQueryController>;
159
169
 
160
170
  private readonly tokenRef: TokenRef | null;
161
171
  private readonly runnerTokenRef: TokenRef | null;
@@ -239,6 +249,7 @@ export class StigmerClient {
239
249
  this.workflowQuery = createClient(WorkflowQueryController, this.transport);
240
250
  this.workflowInstanceQuery = createClient(WorkflowInstanceQueryController, this.transport);
241
251
  this.platformQuery = createClient(PlatformQueryController, this.transport);
252
+ this.channelMessageQuery = createClient(ChannelMessageQueryController, this.transport);
242
253
  }
243
254
 
244
255
  /**
@@ -380,6 +391,48 @@ export class StigmerClient {
380
391
  }
381
392
  }
382
393
 
394
+ /**
395
+ * The agent's serving proactive-messaging channels, as data
396
+ * (proactive-messaging DD-006 D2) — the runner's tool-attachment
397
+ * decision. An empty list is the everyday answer (most agents have no
398
+ * proactive channel).
399
+ *
400
+ * When a scoped runner token is supplied, the read authenticates with
401
+ * it per-call (the {@link getExecutionContextByExecutionId} precedent):
402
+ * the messaging reach refuses the desktop runner's ambient
403
+ * embedded_runner credential outright, and one desktop runner process
404
+ * serves many sessions concurrently, so the credential cannot live in
405
+ * a shared ref. A cloud sandbox runner's ambient credential is already
406
+ * the session-scoped token; OSS sends nothing and answers empty.
407
+ */
408
+ async listMessagingChannels(scopedToken?: string): Promise<MessagingChannel[]> {
409
+ const res = await this.channelMessageQuery.listMessagingChannels(
410
+ {},
411
+ scopedToken
412
+ ? { headers: { authorization: `Bearer ${scopedToken}` } }
413
+ : undefined,
414
+ );
415
+ return res.entries;
416
+ }
417
+
418
+ /**
419
+ * The channel's provider template registry, approved entries only —
420
+ * the `<available_channel_templates>` prompt section's source
421
+ * (proactive-messaging DD-003 D5). Entries carry the DD-006 D1
422
+ * sendability verdict (`unsupportedReason`, empty means sendable);
423
+ * the section formatter filters on it. Credential rules as
424
+ * {@link listMessagingChannels}.
425
+ */
426
+ async listChannelTemplates(channel: string, scopedToken?: string): Promise<ChannelTemplate[]> {
427
+ const res = await this.channelMessageQuery.listTemplates(
428
+ { channel, approvedOnly: true },
429
+ scopedToken
430
+ ? { headers: { authorization: `Bearer ${scopedToken}` } }
431
+ : undefined,
432
+ );
433
+ return res.entries;
434
+ }
435
+
383
436
  async getSession(sessionId: string): Promise<Session> {
384
437
  return this.sessionQuery.get({ value: sessionId });
385
438
  }
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
  };