@stigmer/runner 3.12.3 → 3.12.5

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 (227) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-http.js +12 -0
  3. package/dist/activities/call-http.js.map +1 -1
  4. package/dist/activities/call-llm.d.ts +18 -0
  5. package/dist/activities/call-llm.js +56 -2
  6. package/dist/activities/call-llm.js.map +1 -1
  7. package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
  8. package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
  9. package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
  10. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  11. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  12. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  13. package/dist/activities/execute-cursor/index.d.ts +10 -1
  14. package/dist/activities/execute-cursor/index.js +108 -57
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  17. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  18. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  19. package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
  20. package/dist/activities/execute-cursor/service-tier.js +5 -21
  21. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  23. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  24. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  25. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  27. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  28. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  29. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  30. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  31. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/index.js +57 -5
  33. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  34. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  35. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  37. package/dist/activities/execute-deep-agent/setup.js +31 -16
  38. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
  40. package/dist/activities/execute-deep-agent/shell-env.js +10 -7
  41. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  43. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  45. package/dist/config.js +10 -5
  46. package/dist/config.js.map +1 -1
  47. package/dist/encryption/config.js +7 -2
  48. package/dist/encryption/config.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/main.js +12 -6
  52. package/dist/main.js.map +1 -1
  53. package/dist/middleware/path-normalization.d.ts +49 -49
  54. package/dist/middleware/path-normalization.js +73 -84
  55. package/dist/middleware/path-normalization.js.map +1 -1
  56. package/dist/middleware/types.d.ts +3 -3
  57. package/dist/payload-codecs.js +2 -1
  58. package/dist/payload-codecs.js.map +1 -1
  59. package/dist/runner-manager.d.ts +1 -1
  60. package/dist/runner-manager.js +42 -22
  61. package/dist/runner-manager.js.map +1 -1
  62. package/dist/runner.js +30 -6
  63. package/dist/runner.js.map +1 -1
  64. package/dist/shared/args-preview.d.ts +8 -0
  65. package/dist/shared/args-preview.js +14 -3
  66. package/dist/shared/args-preview.js.map +1 -1
  67. package/dist/shared/artifact-storage.d.ts +11 -1
  68. package/dist/shared/artifact-storage.js +10 -1
  69. package/dist/shared/artifact-storage.js.map +1 -1
  70. package/dist/shared/declared-preferences.d.ts +47 -0
  71. package/dist/shared/declared-preferences.js +64 -0
  72. package/dist/shared/declared-preferences.js.map +1 -0
  73. package/dist/shared/fingerprint-secret.d.ts +3 -2
  74. package/dist/shared/fingerprint-secret.js +5 -3
  75. package/dist/shared/fingerprint-secret.js.map +1 -1
  76. package/dist/shared/llm-backend.js +8 -1
  77. package/dist/shared/llm-backend.js.map +1 -1
  78. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  79. package/dist/shared/mcp-enabled-tools.js +2 -3
  80. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  81. package/dist/shared/mcp-resolver.d.ts +20 -15
  82. package/dist/shared/mcp-resolver.js +11 -12
  83. package/dist/shared/mcp-resolver.js.map +1 -1
  84. package/dist/shared/model-client.d.ts +15 -0
  85. package/dist/shared/model-client.js +57 -13
  86. package/dist/shared/model-client.js.map +1 -1
  87. package/dist/shared/placeholder-resolver.d.ts +9 -2
  88. package/dist/shared/placeholder-resolver.js +9 -2
  89. package/dist/shared/placeholder-resolver.js.map +1 -1
  90. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  91. package/dist/shared/plan-mode-permissions.js +27 -55
  92. package/dist/shared/plan-mode-permissions.js.map +1 -1
  93. package/dist/shared/registry-endpoint.d.ts +5 -0
  94. package/dist/shared/registry-endpoint.js +7 -1
  95. package/dist/shared/registry-endpoint.js.map +1 -1
  96. package/dist/shared/runner-credential-keys.d.ts +26 -1
  97. package/dist/shared/runner-credential-keys.js +34 -1
  98. package/dist/shared/runner-credential-keys.js.map +1 -1
  99. package/dist/shared/runner-credential-store.d.ts +77 -0
  100. package/dist/shared/runner-credential-store.js +111 -0
  101. package/dist/shared/runner-credential-store.js.map +1 -0
  102. package/dist/shared/service-tier.d.ts +55 -0
  103. package/dist/shared/service-tier.js +67 -0
  104. package/dist/shared/service-tier.js.map +1 -0
  105. package/dist/shared/skill-mount.d.ts +89 -0
  106. package/dist/shared/skill-mount.js +142 -0
  107. package/dist/shared/skill-mount.js.map +1 -0
  108. package/dist/shared/skill-writer.d.ts +28 -26
  109. package/dist/shared/skill-writer.js +79 -102
  110. package/dist/shared/skill-writer.js.map +1 -1
  111. package/dist/shared/worker-shutdown.d.ts +67 -0
  112. package/dist/shared/worker-shutdown.js +79 -0
  113. package/dist/shared/worker-shutdown.js.map +1 -0
  114. package/dist/shared/workspace/types.d.ts +3 -2
  115. package/dist/shared/zip-extract.d.ts +10 -3
  116. package/dist/shared/zip-extract.js +10 -3
  117. package/dist/shared/zip-extract.js.map +1 -1
  118. package/dist/workflow-engine/loader.js +38 -1
  119. package/dist/workflow-engine/loader.js.map +1 -1
  120. package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
  121. package/dist/workflow-engine/tasks/call-function.js +49 -5
  122. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  123. package/dist/workflow-engine/tasks/human-input.js +23 -2
  124. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  125. package/dist/workflow-engine/types.d.ts +6 -0
  126. package/dist/workflow-engine/types.js.map +1 -1
  127. package/dist/workflows/engine-core.js +36 -8
  128. package/dist/workflows/engine-core.js.map +1 -1
  129. package/package.json +15 -11
  130. package/src/activities/__tests__/call-http.test.ts +36 -0
  131. package/src/activities/__tests__/call-llm.test.ts +77 -0
  132. package/src/activities/call-http.ts +17 -0
  133. package/src/activities/call-llm.ts +78 -2
  134. package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
  135. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  136. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  137. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
  138. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  139. package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
  140. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  141. package/src/activities/execute-cursor/index.ts +129 -55
  142. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  143. package/src/activities/execute-cursor/service-tier.ts +5 -29
  144. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  145. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  146. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  147. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  148. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  149. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  150. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  151. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  152. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  153. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  154. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  155. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  156. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  157. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  158. package/src/activities/execute-deep-agent/index.ts +57 -5
  159. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  160. package/src/activities/execute-deep-agent/setup.ts +36 -19
  161. package/src/activities/execute-deep-agent/shell-env.ts +10 -7
  162. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  163. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  164. package/src/config.ts +10 -5
  165. package/src/encryption/config.ts +8 -2
  166. package/src/index.ts +1 -1
  167. package/src/main.ts +16 -6
  168. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  169. package/src/middleware/path-normalization.ts +78 -90
  170. package/src/middleware/types.ts +3 -3
  171. package/src/payload-codecs.ts +2 -1
  172. package/src/runner-manager.ts +55 -22
  173. package/src/runner.ts +39 -6
  174. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  175. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  176. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  177. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  178. package/src/shared/__tests__/model-client.test.ts +99 -0
  179. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  180. package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
  181. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  182. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  183. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  184. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  185. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  186. package/src/shared/__tests__/zip-extract.test.ts +46 -11
  187. package/src/shared/args-preview.ts +17 -3
  188. package/src/shared/artifact-storage.ts +20 -1
  189. package/src/shared/declared-preferences.ts +84 -0
  190. package/src/shared/fingerprint-secret.ts +5 -3
  191. package/src/shared/llm-backend.ts +7 -1
  192. package/src/shared/mcp-enabled-tools.ts +2 -3
  193. package/src/shared/mcp-resolver.ts +20 -20
  194. package/src/shared/model-client.ts +76 -13
  195. package/src/shared/placeholder-resolver.ts +9 -2
  196. package/src/shared/plan-mode-permissions.ts +27 -58
  197. package/src/shared/registry-endpoint.ts +9 -1
  198. package/src/shared/runner-credential-keys.ts +36 -1
  199. package/src/shared/runner-credential-store.ts +115 -0
  200. package/src/shared/service-tier.ts +78 -0
  201. package/src/shared/skill-mount.ts +179 -0
  202. package/src/shared/skill-writer.ts +96 -130
  203. package/src/shared/worker-shutdown.ts +99 -0
  204. package/src/shared/workspace/types.ts +3 -2
  205. package/src/shared/zip-extract.ts +14 -7
  206. package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
  207. package/src/workflow-engine/__tests__/loader.test.ts +51 -0
  208. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
  209. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
  210. package/src/workflow-engine/loader.ts +46 -1
  211. package/src/workflow-engine/tasks/call-function.ts +74 -13
  212. package/src/workflow-engine/tasks/human-input.ts +33 -5
  213. package/src/workflow-engine/types.ts +6 -0
  214. package/src/workflows/engine-core.ts +39 -8
  215. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  216. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  217. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  218. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  219. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  220. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  221. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  222. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  223. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  224. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  225. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  226. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  227. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -34,6 +34,12 @@ import {
34
34
  toFoundryDeploymentName,
35
35
  } from "./llm-backend.js";
36
36
  import { resolveToApiModelId } from "./model-registry.js";
37
+ import {
38
+ toAnthropicServiceTier,
39
+ toOpenAiServiceTier,
40
+ type EffectiveServiceTier,
41
+ } from "./service-tier.js";
42
+ import { getRunnerSecret } from "./runner-credential-store.js";
37
43
 
38
44
  export interface BuildChatModelOptions {
39
45
  /** Registry id ("claude-haiku-4.5"), "provider:model", or a provider API id. */
@@ -63,6 +69,20 @@ export interface BuildChatModelOptions {
63
69
  */
64
70
  readonly timeoutMs?: number;
65
71
  readonly maxRetries?: number;
72
+ /**
73
+ * The execution's EFFECTIVE service tier (stigmer/stigmer#361) — already
74
+ * resolved by the caller (resolveEffectiveServiceTier), never
75
+ * UNSPECIFIED. When set, every provider request pins its tier explicitly
76
+ * (OpenAI `service_tier`; Anthropic `service_tier` via invocationKwargs)
77
+ * so the provider ACCOUNT's default can never pick the price — the #357
78
+ * contract, held on the native harness. Deliberately optional:
79
+ * platform-internal utility calls (tool-approval classification, session
80
+ * subjects, structured extraction, workflow llm_call) are not the
81
+ * execution's own turns and send no tier — the provider treats an absent
82
+ * parameter as its standard behavior, and those calls' models are
83
+ * platform-chosen economy models.
84
+ */
85
+ readonly serviceTier?: EffectiveServiceTier;
66
86
  }
67
87
 
68
88
  /**
@@ -128,11 +148,11 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
128
148
  // tests. Prerequisites are re-checked here (not only in
129
149
  // the factories' preflight) so paths that construct models without a
130
150
  // runner factory still fail at dispatch with the catalog message instead
131
- // of mid-request. Credentials are read natively by each SDK from its
132
- // standard conventions (GCP: CLOUD_ML_REGION + ADC; AWS: AWS_REGION +
133
- // the credential chain / AWS_BEARER_TOKEN_BEDROCK; Foundry:
134
- // ANTHROPIC_FOUNDRY_API_KEY, or the Azure credential chain when no key
135
- // is set).
151
+ // of mid-request. Ambient credentials are read natively by each SDK from
152
+ // its standard conventions (GCP: CLOUD_ML_REGION + ADC; AWS: AWS_REGION +
153
+ // the credential chain); runner-held keys (AWS_BEARER_TOKEN_BEDROCK,
154
+ // ANTHROPIC_FOUNDRY_API_KEY) are passed explicitly from the credential
155
+ // store because the boot capture empties their env slots (#508).
136
156
  let backendCreateClient:
137
157
  | ((options: { maxRetries?: number; timeout?: number }) => unknown)
138
158
  | undefined;
@@ -149,8 +169,17 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
149
169
  const prereq = checkBedrockPrerequisites();
150
170
  if (prereq !== null) throw new Error(prereq);
151
171
  const { AnthropicBedrock } = await import("@anthropic-ai/bedrock-sdk");
172
+ // The SDK's own default for `apiKey` is process.env.AWS_BEARER_TOKEN_BEDROCK,
173
+ // which the boot capture has emptied (#508) — hand it the stored value
174
+ // explicitly. `undefined` when absent preserves the SDK's fallthrough to
175
+ // the ambient AWS credential chain (env keys, IRSA, config files).
176
+ const bedrockBearerToken = getRunnerSecret("AWS_BEARER_TOKEN_BEDROCK");
152
177
  backendCreateClient = (options) =>
153
- new AnthropicBedrock({ maxRetries: options.maxRetries, timeout: options.timeout });
178
+ new AnthropicBedrock({
179
+ apiKey: bedrockBearerToken,
180
+ maxRetries: options.maxRetries,
181
+ timeout: options.timeout,
182
+ });
154
183
  wireModelId = toBedrockModelId(apiModelId);
155
184
  if (maxTokens === undefined) {
156
185
  // LangChain's per-model maxTokens table prefix-matches the model
@@ -179,8 +208,13 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
179
208
  // so refreshed tokens flow without reconstruction (pinned by
180
209
  // foundry-seam.test.ts). Endpoint (resource or base URL) and the key
181
210
  // are read natively by the SDK from its own env vars.
211
+ // The SDK's own default for `apiKey` is process.env.ANTHROPIC_FOUNDRY_API_KEY,
212
+ // which the boot capture has emptied (#508) — resolve it from the store
213
+ // and hand it over explicitly. The either/or stays intact: exactly one of
214
+ // apiKey / azureADTokenProvider reaches the constructor.
215
+ const foundryApiKey = getRunnerSecret("ANTHROPIC_FOUNDRY_API_KEY")?.trim() || undefined;
182
216
  let azureADTokenProvider: (() => Promise<string>) | undefined;
183
- if (!process.env.ANTHROPIC_FOUNDRY_API_KEY?.trim()) {
217
+ if (!foundryApiKey) {
184
218
  const { DefaultAzureCredential, getBearerTokenProvider } = await import("@azure/identity");
185
219
  azureADTokenProvider = getBearerTokenProvider(
186
220
  new DefaultAzureCredential(),
@@ -191,7 +225,7 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
191
225
  new AnthropicFoundry({
192
226
  maxRetries: options.maxRetries,
193
227
  timeout: options.timeout,
194
- ...(azureADTokenProvider ? { azureADTokenProvider } : {}),
228
+ ...(foundryApiKey ? { apiKey: foundryApiKey } : { azureADTokenProvider }),
195
229
  });
196
230
  // Unlike the vertex/bedrock ids, the deployment name needs no maxTokens
197
231
  // handling: stripping the snapshot date preserves LangChain's per-model
@@ -207,19 +241,27 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
207
241
  ? buildProxyHeaders(opts.stigmerToken, opts.headerScope ?? {})
208
242
  : undefined;
209
243
 
210
- // Proxy mode authenticates with the Stigmer token; direct mode falls back to
211
- // the provider's own env-var key.
244
+ // Proxy mode authenticates with the Stigmer token; direct mode falls back
245
+ // to the provider's own key, resolved from the credential store (the boot
246
+ // capture moved it out of process.env, #508).
212
247
  const apiKey = opts.proxyEndpoint
213
248
  ? (opts.stigmerToken ?? "proxy-managed")
214
249
  : provider === "openai"
215
- ? (process.env.OPENAI_API_KEY ?? "")
216
- : (process.env.ANTHROPIC_API_KEY ?? "");
250
+ ? (getRunnerSecret("OPENAI_API_KEY") ?? "")
251
+ : (getRunnerSecret("ANTHROPIC_API_KEY") ?? "");
217
252
 
253
+ // maxRetries applies when a timeout is bound (a retry loop under a bound
254
+ // multiplies the wall-clock budget) or when the caller pinned it
255
+ // explicitly (call-llm hands retry ownership to Temporal, #686). Callers
256
+ // that set neither keep LangChain's default retry behavior unchanged.
257
+ const maxRetries = timeoutMs !== undefined || opts.maxRetries !== undefined
258
+ ? { maxRetries: opts.maxRetries ?? 0 }
259
+ : {};
218
260
  const common = {
219
261
  temperature: opts.temperature ?? 0,
220
262
  apiKey,
221
263
  ...(maxTokens ? { maxTokens } : {}),
222
- ...(timeoutMs ? { maxRetries: opts.maxRetries ?? 0 } : {}),
264
+ ...maxRetries,
223
265
  };
224
266
 
225
267
  // The request timeout lives in a different slot per wrapper: ChatOpenAI
@@ -242,6 +284,18 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
242
284
  ? { clientOptions: anthropicClientOptions }
243
285
  : {};
244
286
 
287
+ // The tier rides the request body per provider dialect (#361): OpenAI
288
+ // takes `service_tier` as a first-class constructor field; ChatAnthropic
289
+ // has no such field, so it rides `invocationKwargs`, which the wrapper
290
+ // spreads into every request body. Both spellings resolve through the
291
+ // shared mapping so no construction site can invent a third one.
292
+ const openAiServiceTierField = opts.serviceTier !== undefined
293
+ ? { service_tier: toOpenAiServiceTier(opts.serviceTier) }
294
+ : {};
295
+ const anthropicServiceTierField = opts.serviceTier !== undefined
296
+ ? { invocationKwargs: { service_tier: toAnthropicServiceTier(opts.serviceTier) } }
297
+ : {};
298
+
245
299
  // The two SDKs name the transport-override block differently (OpenAI:
246
300
  // `configuration`, Anthropic: `clientOptions`) — encapsulating that here is
247
301
  // the whole point, since the shape mismatch is where bugs used to hide.
@@ -249,6 +303,7 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
249
303
  ? new ChatOpenAI({
250
304
  model: apiModelId,
251
305
  ...common,
306
+ ...openAiServiceTierField,
252
307
  ...(timeoutMs ? { timeout: timeoutMs } : {}),
253
308
  ...(baseUrl || headers
254
309
  ? {
@@ -266,6 +321,13 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
266
321
  // provided) is what lets this construct with no ANTHROPIC_API_KEY.
267
322
  // (Backend mode never has a proxy — see the precedence rule above —
268
323
  // so the clientOptions here carry at most the timeout.)
324
+ //
325
+ // service_tier deliberately does NOT ride backend requests:
326
+ // standard/priority tiers are an Anthropic-FIRST-PARTY billing
327
+ // concept, and Vertex/Bedrock/Foundry bill through the cloud
328
+ // provider with no tier dimension — an unknown body param there is
329
+ // a request refusal waiting to happen. The account-default price
330
+ // hole this parameter closes does not exist on those backends.
269
331
  new ChatAnthropic({
270
332
  model: wireModelId,
271
333
  ...common,
@@ -275,6 +337,7 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
275
337
  : new ChatAnthropic({
276
338
  model: apiModelId,
277
339
  ...common,
340
+ ...anthropicServiceTierField,
278
341
  ...anthropicClientOptionsField,
279
342
  });
280
343
 
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Strict placeholder resolver for ${VAR_NAME} syntax in MCP server configs.
3
3
  *
4
+ * Port of the Python agent-runner's PlaceholderResolver (strict mode only).
4
5
  * Always raises on unresolved placeholders — sending literal ${VAR} as an
5
- * HTTP header value produces cryptic auth failures from the remote server.
6
+ * HTTP header value (e.g. Authorization: Bearer ${API_KEY}) produces
7
+ * cryptic auth failures from the remote server.
6
8
  */
7
9
 
8
10
  const PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
@@ -41,6 +43,9 @@ export function resolvePlaceholders(
41
43
 
42
44
  /**
43
45
  * Resolve placeholders in all values of a headers map.
46
+ *
47
+ * @throws PlaceholderResolutionError if any header value contains an
48
+ * unresolvable placeholder.
44
49
  */
45
50
  export function resolveHeaders(
46
51
  headers: Record<string, string>,
@@ -55,7 +60,9 @@ export function resolveHeaders(
55
60
 
56
61
  /**
57
62
  * Filter env vars to only keys declared in the MCP server's spec.env.
58
- * Prevents secret over-sharing by restricting the environment to
63
+ *
64
+ * Mirrors the agent-runner's _filter_env_to_declared_keys: prevents
65
+ * secret over-sharing by restricting the subprocess/HTTP environment to
59
66
  * explicitly declared variables.
60
67
  */
61
68
  export function filterEnvToDeclaredKeys(
@@ -2,26 +2,25 @@
2
2
  * The Plan-mode filesystem permission rules — the enforcement twin of
3
3
  * `plan-mode-prompt.ts` (which carries the instruction half of the contract).
4
4
  *
5
- * Plan mode is contained BY CONSTRUCTION on the native harness: these rules
6
- * deny every filesystem write everywhere AND scope reads to the session
7
- * workspace (issue #528 owner ruling: the workspace is plan mode's read
8
- * boundary on cloud and desktop runners alike). Without the read boundary,
9
- * model-provided absolute paths reached anywhere the process account could
10
- * read the pod filesystem (including /proc/self/environ) on cloud runners,
11
- * the user's whole home directory on desktop while plan mode still carries
12
- * exfiltration-capable tools (web_fetch, MCP). Rules are first-match-wins
13
- * with a permissive default (deepagents' decidePathAccess), so order is
14
- * load-bearing: the workspace read-allow must precede the read-deny.
5
+ * Plan mode's policy is: reads anywhere in the workspace, writes nowhere.
6
+ * Only the write half needs a RULE. The read boundary that issue #528 built
7
+ * out of rules (workspace read-allow + read-deny, glob-escaped real root) is
8
+ * now STRUCTURAL: every native-harness backend is virtual-rooted
9
+ * (`virtualMode: true` see cas-capture-backend.ts, issue #754), so every
10
+ * path a tool can express resolves inside the workspace root and traversal
11
+ * is rejected at resolution. A rule cannot widen that, and no workspace-root
12
+ * glob is needed to narrow it — which also retires the whole
13
+ * special-characters-in-the-root hazard (`escapeGlobLiteral` and its
14
+ * end-to-end matcher suite) that the real-path rules carried.
15
15
  *
16
- * The workspace-root pattern is matched as a STRING against the raw tool-call
17
- * path (micromatch, dot:true), before the backend touches disk. That is
18
- * exactly why the legitimate out-of-workspace reads keep working: skills,
19
- * attachments, and the approved plan live in the platform dir but are
20
- * addressed through the `{workspace}/.stigmer` symlink (see
21
- * shared/workspace/stigmer-link.ts), so their path strings are in-root even
22
- * though the bytes are not. The same holds for multi-workspace local entries
23
- * (`{workspace}/{name}` symlinks). A realpath-based boundary would break
24
- * both; do not "harden" this into one.
16
+ * Paths in these rules are VIRTUAL: "/" is the workspace root, matched by
17
+ * micromatch against the raw tool-call path (dot:true) after the
18
+ * path-normalization middleware has canonicalized it (relative "/"-rooted;
19
+ * see middleware/path-normalization.ts). The legitimate platform-dir reads
20
+ * (skills, attachments, the approved plan) keep working exactly as before:
21
+ * they are addressed through the `{workspace}/.stigmer` symlink
22
+ * (shared/workspace/stigmer-link.ts), an in-root path string in either
23
+ * dialect.
25
24
  *
26
25
  * Applied in execute-deep-agent/setup.ts to the parent graph AND threaded
27
26
  * into every compiled sub-agent graph: deepagents' parent-permission
@@ -30,13 +29,6 @@
30
29
  * (issue #255). Kept as its own side-effect-free module so tests can pin the
31
30
  * production rules without dragging in setup.ts's import graph.
32
31
  *
33
- * Rules travel with a companion: every graph that carries them also installs
34
- * the path-normalization middleware (middleware/path-normalization.ts,
35
- * issues #429/#528), because deepagents' rule validation refuses
36
- * workspace-relative paths outright and its `ls`/`glob`/`grep` schema default
37
- * of "/" would deny the bare first listing. Both are derived from the same
38
- * expression at each composition site so they cannot drift apart.
39
- *
40
32
  * Invariant: never combine these rules with a shell-capable (sandbox)
41
33
  * backend — deepagents rejects that pairing at graph construction (see the
42
34
  * cas-capture-backend.ts header). Plan mode guarantees it by construction:
@@ -44,41 +36,18 @@
44
36
  * for shell capability on both the parent and sub-agent backends.
45
37
  */
46
38
 
47
- import { resolve } from "node:path";
48
39
  import type { FilesystemPermission } from "deepagents";
49
40
 
50
41
  /**
51
- * Backslash-escape every character micromatch/picomatch treats as glob
52
- * syntax, so the result matches the input literally. micromatch exports no
53
- * escape API of its own, and this is correctness, not caution: an unescaped
54
- * `(` in a desktop project path would make the workspace read-allow rule
55
- * silently never match — bricking every plan-mode read for that workspace.
56
- * Semantics are pinned end-to-end through deepagents' own matcher by the
57
- * special-character workspace suite in plan-mode-path-normalization.test.ts.
58
- */
59
- export function escapeGlobLiteral(literal: string): string {
60
- return literal.replace(/[\\*?()[\]{}!+@]/g, "\\$&");
61
- }
62
-
63
- /**
64
- * Build the plan-mode rule set for a graph whose filesystem backend is
65
- * rooted at `workspaceRootDir`. The three rules read as the policy:
66
- * reads allowed in the workspace, reads denied everywhere else, writes
67
- * denied everywhere.
42
+ * Build the plan-mode rule set. One rule IS the policy: writes denied
43
+ * everywhere ("/**" is every virtual path; deepagents evaluates
44
+ * first-match-wins with a permissive default, so reads stay allowed
45
+ * workspace-confined structurally, not by rule).
68
46
  *
69
- * `{root}/**` matches the root itself as well as its subtree (verified
70
- * against the installed micromatch), so one allow pattern suffices. The
71
- * root is `path.resolve`d first because enforcement canonicalizes incoming
72
- * paths (collapsed slashes, no trailing separator) before matching — a
73
- * trailing slash in the pattern would silently match nothing.
47
+ * Deliberately parameterless: the pre-#754 signature took the workspace root
48
+ * to build a real-path read fence; resurrecting a root-derived rule would
49
+ * silently mismatch the virtual dialect the enforcement now sees.
74
50
  */
75
- export function buildPlanModePermissions(
76
- workspaceRootDir: string,
77
- ): FilesystemPermission[] {
78
- const canonicalRoot = resolve(workspaceRootDir);
79
- return [
80
- { operations: ["read"], paths: [`${escapeGlobLiteral(canonicalRoot)}/**`] },
81
- { operations: ["read"], paths: ["/**"], mode: "deny" },
82
- { operations: ["write"], paths: ["/**"], mode: "deny" },
83
- ];
51
+ export function buildPlanModePermissions(): FilesystemPermission[] {
52
+ return [{ operations: ["write"], paths: ["/**"], mode: "deny" }];
84
53
  }
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { normalizeEndpoint } from "../config.js";
23
23
  import type { FetchRetryPolicy } from "./http-retry.js";
24
+ import { runnerSecretsEnvView } from "./runner-credential-store.js";
24
25
 
25
26
  /** Default local stigmer-server origin — mirrors config.ts's local-mode default. */
26
27
  const DEFAULT_LOCAL_BACKEND = "http://localhost:7234";
@@ -68,8 +69,15 @@ export function resolveRegistryBaseUrl(env: NodeJS.ProcessEnv = process.env): st
68
69
  /**
69
70
  * Build request headers for registry fetches: bearer auth when a token is
70
71
  * present (cloud requires it; the local server ignores it).
72
+ *
73
+ * The default env is the credential-store view, not bare `process.env`:
74
+ * both token names are captured out of the environment at boot (#508), and
75
+ * the store also carries rotated tokens (manager updateToken / static
76
+ * renewal) that never touch env at all.
71
77
  */
72
- export function buildRegistryHeaders(env: NodeJS.ProcessEnv = process.env): Record<string, string> {
78
+ export function buildRegistryHeaders(
79
+ env: NodeJS.ProcessEnv = runnerSecretsEnvView(),
80
+ ): Record<string, string> {
73
81
  const token = env.STIGMER_TOKEN ?? env.STIGMER_AUTH_TOKEN;
74
82
  return token ? { Authorization: `Bearer ${token}` } : {};
75
83
  }
@@ -10,7 +10,13 @@
10
10
  * their sanctioned delivery channel, and denying them here would break it.
11
11
  *
12
12
  * Consumers:
13
- * - shell-env.ts: SHELL_ENV_DENYLIST for the native harness `execute` tool.
13
+ * - runner-credential-store.ts: captures every name listed in this module
14
+ * out of `process.env` at boot (issue #508 — the Cursor SDK's local agent
15
+ * runtime runs in-process and its shell tool spawns from the runner's own
16
+ * env, so credentials must not LIVE there; see that module for custody
17
+ * rules).
18
+ * - shell-env.ts: SHELL_ENV_DENYLIST for the native harness `execute` tool
19
+ * (defense-in-depth behind the boot scrub).
14
20
  * - mcp-manager.test.ts: leak-tripwire canaries for MCP stdio subprocesses
15
21
  * (that path passes NO runner env by construction; the test plants these
16
22
  * names to prove none leak through).
@@ -44,3 +50,32 @@ export const RUNNER_CREDENTIAL_ENV_KEYS: readonly string[] = [
44
50
  // (llm-backend.ts documents it as a supported auth path).
45
51
  "AWS_BEARER_TOKEN_BEDROCK",
46
52
  ];
53
+
54
+ /**
55
+ * Env var names of the runner's non-credential secrets — material that is
56
+ * not an outbound-call credential (so it does not belong in
57
+ * {@link RUNNER_CREDENTIAL_ENV_KEYS}, whose inclusion rule is pinned above)
58
+ * but is every bit as sensitive in an agent-readable environment
59
+ * (owner ruling on #508: same boot scrub, separate constant so the #385
60
+ * rule keeps its meaning).
61
+ *
62
+ * The `*_KEY_ID` companions are deliberately absent: key identifiers are
63
+ * rotation bookkeeping, not secrets.
64
+ */
65
+ export const RUNNER_ENCRYPTION_ENV_KEYS: readonly string[] = [
66
+ // Temporal payload-encryption keys (encryption/config.ts). An agent that
67
+ // reads these could decrypt the runner's Temporal history payloads.
68
+ "STIGMER_PAYLOAD_ENCRYPTION_KEY",
69
+ "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY",
70
+ ];
71
+
72
+ /**
73
+ * Every env name whose VALUE the credential store takes custody of at boot:
74
+ * the #385 credential set plus the #508 encryption-key set. This is the
75
+ * scrub list — after `captureRunnerSecrets()`, none of these names remain
76
+ * in `process.env`.
77
+ */
78
+ export const RUNNER_SECRET_ENV_KEYS: readonly string[] = [
79
+ ...RUNNER_CREDENTIAL_ENV_KEYS,
80
+ ...RUNNER_ENCRYPTION_ENV_KEYS,
81
+ ];
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Custody point for the runner's own secrets — the VALUES companion to
3
+ * runner-credential-keys.ts's names (issue #508).
4
+ *
5
+ * Why this exists: the Cursor SDK's local agent runtime ships inside
6
+ * `@cursor/sdk` and runs IN-PROCESS in the runner. Its shell tool spawns
7
+ * bash with `{...process.env, ...}` and its git layer does the same, so
8
+ * anything living in the runner's `process.env` is readable by every shell
9
+ * command the agent runs — and the SDK exposes no env option for local
10
+ * agents to scrub at the spawn boundary. Denylists on runner-owned spawn
11
+ * sites (shell-env.ts) cannot reach those vendor spawns. The only fix that
12
+ * covers every spawn surface, present and future, is for secrets not to
13
+ * LIVE in `process.env` at all: this module captures them at boot and is
14
+ * the sole holder afterwards.
15
+ *
16
+ * Custody rules:
17
+ *
18
+ * 1. `captureRunnerSecrets()` runs at every boot door (both public runner
19
+ * factories, plus main() for symmetry) — it MOVES every
20
+ * {@link RUNNER_SECRET_ENV_KEYS} value out of `process.env` into a
21
+ * module-private map. Idempotent; first call wins.
22
+ * 2. Reads go through {@link getRunnerSecret}: captured value first, live
23
+ * `process.env` as fallback. The fallback keeps the store honest rather
24
+ * than frozen — production sets these vars only at process start (the
25
+ * capture window), so the fallback is a dead path there, but tests and
26
+ * unusual embedders that plant a value later see it behave exactly like
27
+ * the env read it replaced.
28
+ * 3. Rotation writes go through {@link setRunnerSecret} (the
29
+ * runner-manager/static-renewal token channel that previously wrote
30
+ * `process.env.STIGMER_TOKEN` in lockstep with its tokenRef).
31
+ * 4. {@link runnerSecretsEnvView} adapts the store to the codebase's
32
+ * `env: NodeJS.ProcessEnv` injection seams (llm-backend,
33
+ * registry-endpoint). The view re-merges secrets over `process.env` —
34
+ * it exists for in-process CONFIG READS ONLY and must never be handed
35
+ * to a child process env or any spawn options.
36
+ *
37
+ * Embedder note: `@stigmer/runner` is a public library, and capture runs
38
+ * inside the factories, so embedding the runner scrubs the HOST process's
39
+ * env of runner secrets at boot. That is the point — agent shells run in
40
+ * the embedder's process — and host code that still needs a value reads it
41
+ * through this module.
42
+ */
43
+
44
+ import { RUNNER_SECRET_ENV_KEYS } from "./runner-credential-keys.js";
45
+
46
+ const captured = new Map<string, string>();
47
+ let captureRan = false;
48
+
49
+ /**
50
+ * Move every {@link RUNNER_SECRET_ENV_KEYS} value out of `process.env` into
51
+ * the store. Idempotent — only the first call captures, so a late caller
52
+ * cannot re-freeze values that rotation has since replaced.
53
+ */
54
+ export function captureRunnerSecrets(): void {
55
+ if (captureRan) return;
56
+ captureRan = true;
57
+ for (const name of RUNNER_SECRET_ENV_KEYS) {
58
+ const value = process.env[name];
59
+ if (value !== undefined) {
60
+ captured.set(name, value);
61
+ delete process.env[name];
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Read a runner secret: captured value first, live `process.env` fallback
68
+ * (see custody rule 2). Returns `undefined` when the secret is absent —
69
+ * callers own their missing-secret reaction, exactly as with the env reads
70
+ * this replaces.
71
+ */
72
+ export function getRunnerSecret(name: string): string | undefined {
73
+ return captured.get(name) ?? process.env[name];
74
+ }
75
+
76
+ /**
77
+ * Write (or with `null`, clear) a runner secret — the rotation channel.
78
+ * Ensures capture has run first so a rotated value can never sit in
79
+ * `process.env` because a writer beat the boot capture.
80
+ */
81
+ export function setRunnerSecret(name: string, value: string | null): void {
82
+ captureRunnerSecrets();
83
+ if (value === null) {
84
+ captured.delete(name);
85
+ } else {
86
+ captured.set(name, value);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * `process.env` with the captured secrets merged back over it — an adapter
92
+ * for the `env: NodeJS.ProcessEnv` injection seams so their defaults stay
93
+ * secret-aware after the boot scrub.
94
+ *
95
+ * IN-PROCESS CONFIG READS ONLY: never pass this to a spawn/exec env, a
96
+ * worker thread, or anything else that leaves the process — doing so would
97
+ * reopen exactly the leak the store closes.
98
+ */
99
+ export function runnerSecretsEnvView(): NodeJS.ProcessEnv {
100
+ const view: NodeJS.ProcessEnv = { ...process.env };
101
+ for (const [name, value] of captured) {
102
+ view[name] = value;
103
+ }
104
+ return view;
105
+ }
106
+
107
+ /**
108
+ * Test-only: forget everything captured and re-arm capture. Unit tests that
109
+ * plant secret env vars and boot pieces of the runner need each test to see
110
+ * its own values.
111
+ */
112
+ export function resetRunnerSecretsForTests(): void {
113
+ captured.clear();
114
+ captureRan = false;
115
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Harness-neutral service-tier semantics (stigmer/stigmer#357, extended to
3
+ * the native harness by #361).
4
+ *
5
+ * The platform contract: an execution's price-bearing tier is ALWAYS
6
+ * explicit by the time a provider request leaves the runner. UNSPECIFIED
7
+ * resolves to STANDARD here and ONLY here — every upstream layer preserves
8
+ * the caller's raw enum so "user chose standard" stays distinguishable
9
+ * from "platform default" all the way to the ledger.
10
+ *
11
+ * Each harness owns its translation of the EFFECTIVE tier into provider
12
+ * wire terms:
13
+ * - Cursor: explicit `ModelSelection.params` pinning the price-bearing
14
+ * variant booleans (`execute-cursor/service-tier.ts`).
15
+ * - Native: the provider's own request parameter, mapped in
16
+ * `model-client.ts` (`toOpenAiServiceTier` / `toAnthropicServiceTier`
17
+ * below) so no construction site can invent a third mapping.
18
+ */
19
+
20
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
+
22
+ /**
23
+ * The effective tier after platform-default resolution: never UNSPECIFIED.
24
+ */
25
+ export type EffectiveServiceTier = ServiceTier.STANDARD | ServiceTier.FAST;
26
+
27
+ /**
28
+ * Resolve the configured tier to its effective value. The single place in
29
+ * the platform where UNSPECIFIED becomes STANDARD.
30
+ */
31
+ export function resolveEffectiveServiceTier(
32
+ configured: ServiceTier | undefined,
33
+ ): EffectiveServiceTier {
34
+ return configured === ServiceTier.FAST ? ServiceTier.FAST : ServiceTier.STANDARD;
35
+ }
36
+
37
+ /** Human-readable tier label for logs and error messages. */
38
+ export function serviceTierLabel(tier: ServiceTier): string {
39
+ switch (tier) {
40
+ case ServiceTier.FAST:
41
+ return "fast";
42
+ case ServiceTier.STANDARD:
43
+ return "standard";
44
+ default:
45
+ return "unspecified";
46
+ }
47
+ }
48
+
49
+ /**
50
+ * OpenAI's `service_tier` request parameter for the effective tier.
51
+ *
52
+ * STANDARD maps to "default", NOT "auto": "auto" lets the ACCOUNT's
53
+ * project settings pick the processing tier — the exact
54
+ * account-default-decides-the-price hole #357 closed on the Cursor
55
+ * harness. FAST maps to "priority" (pay-as-you-go priority processing);
56
+ * create-time validation makes FAST unreachable until a registry entry
57
+ * prices it, so today's traffic always sends "default". "flex" (cheaper,
58
+ * slower) is a possible future tier, deliberately unmapped.
59
+ */
60
+ export function toOpenAiServiceTier(tier: EffectiveServiceTier): "default" | "priority" {
61
+ return tier === ServiceTier.FAST ? "priority" : "default";
62
+ }
63
+
64
+ /**
65
+ * Anthropic's `service_tier` request parameter for the effective tier.
66
+ *
67
+ * STANDARD maps to "standard_only": never consume priority-tier capacity,
68
+ * so the bill is the public standard rate regardless of what the account
69
+ * has purchased. FAST maps to "auto" — Anthropic's priority tier is
70
+ * PURCHASED capacity, and "auto" means "use it when available"; the
71
+ * response's `usage.service_tier` reports what actually served, which is
72
+ * what billing reconciles against. FAST is unreachable until a registry
73
+ * entry prices it (and the platform buys priority capacity — the #361
74
+ * half-2 hold).
75
+ */
76
+ export function toAnthropicServiceTier(tier: EffectiveServiceTier): "auto" | "standard_only" {
77
+ return tier === ServiceTier.FAST ? "auto" : "standard_only";
78
+ }