@stigmer/runner 3.12.2 → 3.12.4

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 (125) 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/index.js +61 -28
  11. package/dist/activities/execute-cursor/index.js.map +1 -1
  12. package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
  13. package/dist/activities/execute-cursor/service-tier.js +5 -21
  14. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  15. package/dist/activities/execute-cursor/skill-resolver.d.ts +15 -0
  16. package/dist/activities/execute-cursor/skill-resolver.js +45 -7
  17. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  18. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +9 -1
  19. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +13 -2
  20. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
  21. package/dist/activities/execute-deep-agent/setup.js +14 -0
  22. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  23. package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
  24. package/dist/activities/execute-deep-agent/shell-env.js +10 -7
  25. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +5 -1
  27. package/dist/activities/execute-deep-agent/subagent-wiring.js +9 -1
  28. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  29. package/dist/client/stigmer-client.d.ts +9 -1
  30. package/dist/client/stigmer-client.js +10 -0
  31. package/dist/client/stigmer-client.js.map +1 -1
  32. package/dist/config.js +10 -5
  33. package/dist/config.js.map +1 -1
  34. package/dist/encryption/config.js +7 -2
  35. package/dist/encryption/config.js.map +1 -1
  36. package/dist/main.js +12 -6
  37. package/dist/main.js.map +1 -1
  38. package/dist/middleware/index.d.ts +6 -5
  39. package/dist/middleware/index.js +8 -5
  40. package/dist/middleware/index.js.map +1 -1
  41. package/dist/middleware/tool-intent.d.ts +57 -0
  42. package/dist/middleware/tool-intent.js +152 -0
  43. package/dist/middleware/tool-intent.js.map +1 -0
  44. package/dist/payload-codecs.js +2 -1
  45. package/dist/payload-codecs.js.map +1 -1
  46. package/dist/runner-manager.js +20 -7
  47. package/dist/runner-manager.js.map +1 -1
  48. package/dist/runner.js +19 -6
  49. package/dist/runner.js.map +1 -1
  50. package/dist/shared/fingerprint-secret.d.ts +3 -2
  51. package/dist/shared/fingerprint-secret.js +5 -3
  52. package/dist/shared/fingerprint-secret.js.map +1 -1
  53. package/dist/shared/llm-backend.js +8 -1
  54. package/dist/shared/llm-backend.js.map +1 -1
  55. package/dist/shared/model-client.d.ts +15 -0
  56. package/dist/shared/model-client.js +57 -13
  57. package/dist/shared/model-client.js.map +1 -1
  58. package/dist/shared/registry-endpoint.d.ts +5 -0
  59. package/dist/shared/registry-endpoint.js +7 -1
  60. package/dist/shared/registry-endpoint.js.map +1 -1
  61. package/dist/shared/runner-credential-keys.d.ts +26 -1
  62. package/dist/shared/runner-credential-keys.js +34 -1
  63. package/dist/shared/runner-credential-keys.js.map +1 -1
  64. package/dist/shared/runner-credential-store.d.ts +77 -0
  65. package/dist/shared/runner-credential-store.js +111 -0
  66. package/dist/shared/runner-credential-store.js.map +1 -0
  67. package/dist/shared/service-tier.d.ts +55 -0
  68. package/dist/shared/service-tier.js +67 -0
  69. package/dist/shared/service-tier.js.map +1 -0
  70. package/dist/shared/skill-writer.js +2 -2
  71. package/dist/shared/skill-writer.js.map +1 -1
  72. package/dist/shared/zip-extract.d.ts +10 -3
  73. package/dist/shared/zip-extract.js +10 -3
  74. package/dist/shared/zip-extract.js.map +1 -1
  75. package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
  76. package/dist/workflow-engine/tasks/call-function.js +49 -5
  77. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  78. package/dist/workflow-engine/types.d.ts +6 -0
  79. package/dist/workflow-engine/types.js.map +1 -1
  80. package/dist/workflows/engine-core.js +36 -8
  81. package/dist/workflows/engine-core.js.map +1 -1
  82. package/package.json +2 -2
  83. package/src/activities/__tests__/call-http.test.ts +36 -0
  84. package/src/activities/__tests__/call-llm.test.ts +77 -0
  85. package/src/activities/call-http.ts +17 -0
  86. package/src/activities/call-llm.ts +78 -2
  87. package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
  88. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
  89. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +104 -1
  90. package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
  91. package/src/activities/execute-cursor/index.ts +66 -20
  92. package/src/activities/execute-cursor/service-tier.ts +5 -29
  93. package/src/activities/execute-cursor/skill-resolver.ts +52 -8
  94. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +13 -2
  95. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +21 -20
  96. package/src/activities/execute-deep-agent/setup.ts +15 -0
  97. package/src/activities/execute-deep-agent/shell-env.ts +10 -7
  98. package/src/activities/execute-deep-agent/subagent-wiring.ts +10 -1
  99. package/src/client/stigmer-client.ts +12 -1
  100. package/src/config.ts +10 -5
  101. package/src/encryption/config.ts +8 -2
  102. package/src/main.ts +16 -6
  103. package/src/middleware/__tests__/tool-intent.test.ts +266 -0
  104. package/src/middleware/index.ts +9 -5
  105. package/src/middleware/tool-intent.ts +174 -0
  106. package/src/payload-codecs.ts +2 -1
  107. package/src/runner-manager.ts +29 -6
  108. package/src/runner.ts +25 -6
  109. package/src/shared/__tests__/model-client.test.ts +99 -0
  110. package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
  111. package/src/shared/__tests__/zip-extract.test.ts +46 -11
  112. package/src/shared/fingerprint-secret.ts +5 -3
  113. package/src/shared/llm-backend.ts +7 -1
  114. package/src/shared/model-client.ts +76 -13
  115. package/src/shared/registry-endpoint.ts +9 -1
  116. package/src/shared/runner-credential-keys.ts +36 -1
  117. package/src/shared/runner-credential-store.ts +115 -0
  118. package/src/shared/service-tier.ts +78 -0
  119. package/src/shared/skill-writer.ts +2 -2
  120. package/src/shared/zip-extract.ts +14 -7
  121. package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
  122. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
  123. package/src/workflow-engine/tasks/call-function.ts +74 -13
  124. package/src/workflow-engine/types.ts +6 -0
  125. package/src/workflows/engine-core.ts +39 -8
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Tool-intent middleware — model-authored intent titles for shell tool calls
3
+ * (issue #276).
4
+ *
5
+ * The thread UI titles every shell row with the bare category label ("Shell");
6
+ * only the model knows *why* it is running a command, so a host-side rename
7
+ * can never close that gap. This middleware lets the model author the title:
8
+ * at model-call time it presents a schema-extended clone of the shell tool
9
+ * that adds one optional `description` argument. The model fills it in, the
10
+ * argument rides the tool call's args verbatim through checkpoints and the
11
+ * persisted ToolCall proto (no new state, no proto change), and the React SDK
12
+ * renders it as the row title with the command as secondary text.
13
+ *
14
+ * Why this seam:
15
+ * - The `execute` tool is owned by the deepagents library; its schema is not
16
+ * ours to edit. `wrapModelCall` is the framework's intended point for
17
+ * reshaping the model-visible tool list — langchain's own llmToolSelector
18
+ * middleware swaps `request.tools` through exactly this hook.
19
+ * - Execution is untouched by construction: the agent's ToolNode is built
20
+ * once from the ORIGINAL tools, and the original schema parses with strip
21
+ * semantics, so the extra argument is dropped before the backend's
22
+ * `execute(command)` ever runs. Approval fingerprints are equally
23
+ * unaffected (`description` is not a salient arg field).
24
+ * - The argument name deliberately matches the Cursor harness, whose built-in
25
+ * Shell tool already carries a model-authored `description` — both
26
+ * harnesses converge on one wire key and the SDK reads a single field.
27
+ *
28
+ * The swapped-in declaration is a `StructuredToolParams` object — langchain's
29
+ * first-class shape for a non-executable, bind-time-only tool definition
30
+ * ("the most minimal interface … to be passed to a LLM for tool calling").
31
+ * A same-name RUNNABLE replacement is rejected by the agent's wrapModelCall
32
+ * validation (it would threaten ToolNode execution identity); a params
33
+ * object is exactly the declaration-without-execution the validation exists
34
+ * to protect, and the graph keeps executing the untouched original. Schema
35
+ * extension happens at the JSON-schema level via @langchain/core's interop
36
+ * serializer — the runner's zod (v3) must never construct fields inside the
37
+ * library's zod (v4) schema object.
38
+ */
39
+
40
+ import { toJsonSchema } from "@langchain/core/utils/json_schema";
41
+ import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
42
+ import { classifyTool } from "../shared/tool-kind.js";
43
+ import type { StigmerMiddleware } from "./types.js";
44
+
45
+ /**
46
+ * The wire name of the intent argument. Shared with the Cursor harness's
47
+ * built-in Shell tool and read by the SDK's tool presentation layer — the
48
+ * three surfaces must agree on this key.
49
+ */
50
+ export const INTENT_ARG = "description";
51
+
52
+ /**
53
+ * The behavior-shaping prompt for the intent argument (owner-approved
54
+ * wording, issue #276). This is prompt engineering, not documentation:
55
+ * changing it changes what the model writes into every shell row title.
56
+ */
57
+ export const INTENT_ARG_PROMPT =
58
+ "A short present-tense phrase describing what this command does and why, " +
59
+ "shown to the user as the title of this action (5-10 words, e.g. " +
60
+ "'Run unit tests for the parser'). Do not restate the command syntax.";
61
+
62
+ /** Structural shape of a bindable structured tool, checked at runtime. */
63
+ interface StructuredToolLike {
64
+ readonly name: string;
65
+ readonly description: string;
66
+ readonly schema: unknown;
67
+ }
68
+
69
+ function isStructuredToolLike(candidate: unknown): candidate is StructuredToolLike {
70
+ if (candidate == null || typeof candidate !== "object") return false;
71
+ const t = candidate as Record<string, unknown>;
72
+ return (
73
+ typeof t.name === "string" &&
74
+ typeof t.description === "string" &&
75
+ "schema" in t
76
+ );
77
+ }
78
+
79
+ interface JsonObjectSchema {
80
+ readonly type: "object";
81
+ readonly properties?: Record<string, unknown>;
82
+ readonly [key: string]: unknown;
83
+ }
84
+
85
+ function isJsonObjectSchema(schema: unknown): schema is JsonObjectSchema {
86
+ return (
87
+ schema != null &&
88
+ typeof schema === "object" &&
89
+ (schema as Record<string, unknown>).type === "object"
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Returns a bind-time `StructuredToolParams` declaration for `original`
95
+ * whose schema carries the optional intent argument, or `original` itself
96
+ * when the tool is not a shell tool, is not schema-extendable, or already
97
+ * defines an argument with that name (a real argument must never be
98
+ * shadowed by presentation metadata).
99
+ *
100
+ * Unexpected schemas pass through unchanged on purpose: a missing intent
101
+ * title degrades to today's rendering, while a mangled schema would break
102
+ * the tool for the whole execution.
103
+ */
104
+ function maybeExtendShellTool(original: unknown): unknown {
105
+ if (!isStructuredToolLike(original)) return original;
106
+ if (classifyTool(original.name) !== ToolKind.SHELL) return original;
107
+
108
+ let jsonSchema: unknown;
109
+ try {
110
+ jsonSchema = toJsonSchema(original.schema as Parameters<typeof toJsonSchema>[0]);
111
+ } catch {
112
+ return original;
113
+ }
114
+ if (!isJsonObjectSchema(jsonSchema)) return original;
115
+
116
+ const properties = jsonSchema.properties ?? {};
117
+ if (INTENT_ARG in properties) return original;
118
+
119
+ // A plain frozen declaration, deliberately NOT an executable tool: the
120
+ // graph's ToolNode executes the ORIGINAL registered tool (it is built from
121
+ // the registered tools, not from the model request), and the agent's
122
+ // wrapModelCall validation only forbids swapping same-name EXECUTABLE
123
+ // instances. `isStructuredToolParams` recognizes this shape, so every
124
+ // provider's bindTools converts it exactly like a structured tool.
125
+ return Object.freeze({
126
+ name: original.name,
127
+ description: original.description,
128
+ schema: {
129
+ ...jsonSchema,
130
+ properties: {
131
+ ...properties,
132
+ [INTENT_ARG]: { type: "string", description: INTENT_ARG_PROMPT },
133
+ },
134
+ },
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Creates the middleware. Install on the parent stack AND on every sub-agent
140
+ * stack (subagent-wiring.ts) — sub-agent shell rows render in the same
141
+ * thread and must carry the same titles.
142
+ */
143
+ export function createToolIntentMiddleware(): StigmerMiddleware {
144
+ // One clone per original tool instance: repeated model calls (and repeated
145
+ // turns on the same graph) bind a referentially stable clone instead of
146
+ // re-serializing the schema every round.
147
+ const cloneCache = new WeakMap<object, unknown>();
148
+
149
+ const extendCached = (candidate: unknown): unknown => {
150
+ if (candidate == null || typeof candidate !== "object") return candidate;
151
+ const cached = cloneCache.get(candidate);
152
+ if (cached !== undefined) return cached;
153
+ const extended = maybeExtendShellTool(candidate);
154
+ cloneCache.set(candidate, extended);
155
+ return extended;
156
+ };
157
+
158
+ return {
159
+ name: "StigmerToolIntentMiddleware",
160
+ async wrapModelCall(request, handler) {
161
+ const tools = request.tools;
162
+ if (!tools || tools.length === 0) return handler(request);
163
+
164
+ let changed = false;
165
+ const mapped = tools.map((t) => {
166
+ const extended = extendCached(t);
167
+ if (extended !== t) changed = true;
168
+ return extended;
169
+ });
170
+
171
+ return handler(changed ? { ...request, tools: mapped } : request);
172
+ },
173
+ };
174
+ }
@@ -15,6 +15,7 @@
15
15
  import type { PayloadCodec } from "@temporalio/common";
16
16
  import type { Config } from "./config.js";
17
17
  import type { BootstrapKeyMaterial } from "./encryption/config.js";
18
+ import { getRunnerSecret } from "./shared/runner-credential-store.js";
18
19
 
19
20
  export async function createPayloadCodecs(
20
21
  config: Config,
@@ -30,7 +31,7 @@ export async function createPayloadCodecs(
30
31
  const encryptionConfig = loadPayloadEncryptionConfig(bootstrapKeys);
31
32
  if (encryptionConfig) {
32
33
  codecs.push(new EncryptionPayloadCodec(encryptionConfig));
33
- const source = process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY ? "env" : "bootstrap";
34
+ const source = getRunnerSecret("STIGMER_PAYLOAD_ENCRYPTION_KEY") ? "env" : "bootstrap";
34
35
  console.log(
35
36
  `[runner] Payload encryption enabled (source=${source}, ` +
36
37
  `key_id=${encryptionConfig.primary.keyId}` +
@@ -32,6 +32,11 @@ import type { WorkerActivities } from "./worker.js";
32
32
  import { resolveWorkflowSource, OTEL_WORKFLOW_INTERCEPTOR_MODULE } from "./workflow-source.js";
33
33
  import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
34
34
  import { assertLlmBackendsPreflight } from "./preflight.js";
35
+ import {
36
+ captureRunnerSecrets,
37
+ getRunnerSecret,
38
+ setRunnerSecret,
39
+ } from "./shared/runner-credential-store.js";
35
40
  import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
36
41
  // Per-task-queue in-flight activity tracking lives in ./in-flight.ts so the
37
42
  // activity interceptor (no manager-closure handle) and unit tests can reach it.
@@ -214,6 +219,12 @@ export async function createStigmerRunnerManager(
214
219
  ): Promise<StigmerRunnerManager> {
215
220
  validateManagerOptions(options);
216
221
 
222
+ // Take custody of runner secrets BEFORE anything else can read them from
223
+ // env — and before any agent code could spawn with them (#508). Runs here
224
+ // (not only in main.ts) because this factory is a public library boot door:
225
+ // in-process embedders like the desktop never execute main.ts.
226
+ captureRunnerSecrets();
227
+
217
228
  const { registerStigmerDeepagentsProfiles } = await import(
218
229
  "./activities/execute-deep-agent/deepagents-profiles.js"
219
230
  );
@@ -365,7 +376,7 @@ export async function createStigmerRunnerManager(
365
376
  if (
366
377
  !bootstrap.payloadEncryption &&
367
378
  bootstrap.runnerAccessToken &&
368
- !process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY
379
+ !getRunnerSecret("STIGMER_PAYLOAD_ENCRYPTION_KEY")
369
380
  ) {
370
381
  console.warn(
371
382
  "[runner-manager] Server minted a runner token but returned no payload " +
@@ -530,6 +541,13 @@ export async function createStigmerRunnerManager(
530
541
  await removeManaged(
531
542
  sessions, sessionId, SESSION_QUEUE_PREFIX + sessionId, "session",
532
543
  );
544
+ // The session is done on this host — release its parked agent (and
545
+ // the executor + MCP subprocesses the lease pins) immediately rather
546
+ // than waiting out the idle TTL (#215).
547
+ const { evictSessionAgent } = await import(
548
+ "./activities/execute-cursor/agent-session-cache.js"
549
+ );
550
+ evictSessionAgent(sessionId);
533
551
  },
534
552
 
535
553
  activeSessions(): string[] {
@@ -585,12 +603,11 @@ export async function createStigmerRunnerManager(
585
603
  // proxy credential follows it: only when no token has been minted (so the
586
604
  // pre-mint lockstep is preserved), never once the runner owns a minted
587
605
  // token. See runner-token-coordinator.ts and the staleness changelogs.
606
+ // The credential store is the second leg of the pair (it replaced the
607
+ // process.env.STIGMER_TOKEN write, #508): per-call readers like
608
+ // call-llm and the registry headers resolve the current token there.
588
609
  tokenRef.current = token;
589
- if (token) {
590
- process.env.STIGMER_TOKEN = token;
591
- } else {
592
- delete process.env.STIGMER_TOKEN;
593
- }
610
+ setRunnerSecret("STIGMER_TOKEN", token);
594
611
  tokenCoordinator.onControlPlaneTokenChanged(token);
595
612
  console.log("[runner-manager] Auth token updated");
596
613
  },
@@ -629,6 +646,12 @@ export async function createStigmerRunnerManager(
629
646
  }
630
647
 
631
648
  await Promise.all(shutdownPromises);
649
+ // Workers are drained — release every parked session agent so their
650
+ // executor leases dispose (stdio MCP subprocesses die with them) (#215).
651
+ const { closeAllCachedAgents } = await import(
652
+ "./activities/execute-cursor/agent-session-cache.js"
653
+ );
654
+ closeAllCachedAgents();
632
655
  sessions.clear();
633
656
  workflowExecutions.clear();
634
657
  poolControl = null;
package/src/runner.ts CHANGED
@@ -20,6 +20,10 @@ import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TI
20
20
  import type { WorkerActivities } from "./worker.js";
21
21
  import { resolveRunnerBootstrap } from "./bootstrap.js";
22
22
  import { assertLlmBackendsPreflight } from "./preflight.js";
23
+ import {
24
+ captureRunnerSecrets,
25
+ setRunnerSecret,
26
+ } from "./shared/runner-credential-store.js";
23
27
  import { markBoot, emitRunnerBootTiming } from "./shared/cold-start-timing.js";
24
28
 
25
29
  /**
@@ -118,11 +122,12 @@ export interface StigmerRunner {
118
122
  * Wire up self-renewal for a static cloud sandbox's control-plane credential
119
123
  * (see sandbox-token-renewal.ts for the model). The applied token reaches
120
124
  * 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.
125
+ * per-call sites (call-llm, registry-endpoint headers) resolve it through
126
+ * the runner credential store (which replaced the process.env.STIGMER_TOKEN
127
+ * channel, #508), artifact storage resolves the ref per call, and the two
128
+ * Cursor SDK interceptors are updated directly — a static runner has no
129
+ * {@code RunnerTokenCoordinator} minting a separate proxy credential, so its
130
+ * x-stigmer-auth IS this token.
126
131
  */
127
132
  async function startStaticSandboxTokenRenewal(
128
133
  config: Config,
@@ -157,7 +162,9 @@ async function startStaticSandboxTokenRenewal(
157
162
  client.getRunnerScopedToken({ renewal: true }, currentToken),
158
163
  applyToken: (token) => {
159
164
  tokenRef.current = token;
160
- process.env.STIGMER_TOKEN = token;
165
+ // Store write replaced the process.env.STIGMER_TOKEN write (#508) —
166
+ // same per-call readers (call-llm, registry headers), no env exposure.
167
+ setRunnerSecret("STIGMER_TOKEN", token);
161
168
  updateInterceptorToken(token);
162
169
  updateHttp2InterceptorToken(token);
163
170
  },
@@ -190,6 +197,12 @@ export async function createStigmerRunner(
190
197
  ): Promise<StigmerRunner> {
191
198
  validateOptions(options);
192
199
 
200
+ // Take custody of runner secrets BEFORE anything else can read them from
201
+ // env — and before any agent code could spawn with them (#508). Runs here
202
+ // (not only in main.ts) because this factory is a public library boot door
203
+ // for in-process embedders.
204
+ captureRunnerSecrets();
205
+
193
206
  const { registerStigmerDeepagentsProfiles } = await import(
194
207
  "./activities/execute-deep-agent/deepagents-profiles.js"
195
208
  );
@@ -327,6 +340,12 @@ export async function createStigmerRunner(
327
340
  emitRunnerBootTiming({ task_queue: config.taskQueue, mode: config.mode });
328
341
  await worker.run();
329
342
  console.log("Worker stopped");
343
+ // The worker has drained — release any parked session agent so its
344
+ // executor lease disposes with the process (#215).
345
+ const { closeAllCachedAgents } = await import(
346
+ "./activities/execute-cursor/agent-session-cache.js"
347
+ );
348
+ closeAllCachedAgents();
330
349
  },
331
350
  shutdown() {
332
351
  tokenRenewal?.stop();
@@ -23,6 +23,7 @@ vi.mock("@langchain/openai", () => ({
23
23
 
24
24
  import { buildChatModel } from "../model-client.js";
25
25
  import { _resetRegistryCache } from "../model-registry.js";
26
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
27
 
27
28
  interface MockModel {
28
29
  id: string;
@@ -306,6 +307,18 @@ describe("buildChatModel", () => {
306
307
  expect(lastAnthropicArgs().apiKey).toBe("");
307
308
  });
308
309
 
310
+ it("does NOT send service_tier on a backend adapter — tiers are Anthropic-first-party billing, and Vertex/Bedrock/Foundry have no tier dimension", async () => {
311
+ mockRegistryResponse([
312
+ { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
313
+ ]);
314
+
315
+ await buildChatModel({ modelName: "claude-sonnet-4.6", serviceTier: ServiceTier.STANDARD });
316
+
317
+ const args = lastAnthropicArgs();
318
+ expect(typeof args.createClient).toBe("function");
319
+ expect(args).not.toHaveProperty("invocationKwargs");
320
+ });
321
+
309
322
  it("yields to the proxy: a proxied call never consults the backend var", async () => {
310
323
  mockRegistryResponse([
311
324
  { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
@@ -359,4 +372,90 @@ describe("buildChatModel", () => {
359
372
  await expect(buildChatModel({ modelName: "claude-sonnet-4.6" }))
360
373
  .rejects.toThrow(/STIGMER_ANTHROPIC_BACKEND="verteks" is not a supported backend/);
361
374
  });
375
+
376
+ describe("service tier (stigmer/stigmer#361)", () => {
377
+ // The #357 contract on the native harness: when the caller passes the
378
+ // execution's effective tier, every provider request pins it
379
+ // explicitly, so the provider ACCOUNT's default can never pick the
380
+ // price. Each provider has its own spelling; both are pinned here so
381
+ // a wrapper upgrade that moves the slot fails loudly.
382
+
383
+ it("OpenAI: STANDARD pins service_tier 'default' — never 'auto', which would re-open the account-default hole", async () => {
384
+ mockRegistryResponse([
385
+ { id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
386
+ ]);
387
+
388
+ await buildChatModel({ modelName: "gpt-4.1", serviceTier: ServiceTier.STANDARD });
389
+
390
+ const args = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
391
+ expect(args).toMatchObject({ service_tier: "default" });
392
+ });
393
+
394
+ it("OpenAI: FAST pins service_tier 'priority'", async () => {
395
+ mockRegistryResponse([
396
+ { id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
397
+ ]);
398
+
399
+ await buildChatModel({ modelName: "gpt-4.1", serviceTier: ServiceTier.FAST });
400
+
401
+ const args = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
402
+ expect(args).toMatchObject({ service_tier: "priority" });
403
+ });
404
+
405
+ it("Anthropic: STANDARD rides invocationKwargs as 'standard_only' — priority capacity is never consumed implicitly", async () => {
406
+ mockRegistryResponse([
407
+ { id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
408
+ ]);
409
+
410
+ await buildChatModel({ modelName: "claude-haiku-4.5", serviceTier: ServiceTier.STANDARD });
411
+
412
+ expect(lastAnthropicArgs()).toMatchObject({
413
+ invocationKwargs: { service_tier: "standard_only" },
414
+ });
415
+ });
416
+
417
+ it("Anthropic: FAST rides invocationKwargs as 'auto' — use purchased priority capacity when available", async () => {
418
+ mockRegistryResponse([
419
+ { id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
420
+ ]);
421
+
422
+ await buildChatModel({ modelName: "claude-haiku-4.5", serviceTier: ServiceTier.FAST });
423
+
424
+ expect(lastAnthropicArgs()).toMatchObject({
425
+ invocationKwargs: { service_tier: "auto" },
426
+ });
427
+ });
428
+
429
+ it("an omitted tier sends NO tier parameter on either provider — platform-internal utility calls stay untiered", async () => {
430
+ mockRegistryResponse([
431
+ { id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
432
+ ]);
433
+ await buildChatModel({ modelName: "gpt-4.1" });
434
+ const openAiArgs = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
435
+ expect(openAiArgs).not.toHaveProperty("service_tier");
436
+
437
+ mockRegistryResponse([
438
+ { id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
439
+ ]);
440
+ await buildChatModel({ modelName: "claude-haiku-4.5" });
441
+ expect(lastAnthropicArgs()).not.toHaveProperty("invocationKwargs");
442
+ });
443
+
444
+ it("the tier rides the proxy path too — the request body parameter is what the proxy meters", async () => {
445
+ mockRegistryResponse([
446
+ { id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
447
+ ]);
448
+
449
+ await buildChatModel({
450
+ modelName: "claude-haiku-4.5",
451
+ proxyEndpoint: "https://api.stigmer.ai",
452
+ stigmerToken: "token",
453
+ serviceTier: ServiceTier.STANDARD,
454
+ });
455
+
456
+ expect(lastAnthropicArgs()).toMatchObject({
457
+ invocationKwargs: { service_tier: "standard_only" },
458
+ });
459
+ });
460
+ });
362
461
  });
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ captureRunnerSecrets,
4
+ getRunnerSecret,
5
+ setRunnerSecret,
6
+ runnerSecretsEnvView,
7
+ resetRunnerSecretsForTests,
8
+ } from "../runner-credential-store.js";
9
+ import {
10
+ RUNNER_CREDENTIAL_ENV_KEYS,
11
+ RUNNER_ENCRYPTION_ENV_KEYS,
12
+ RUNNER_SECRET_ENV_KEYS,
13
+ } from "../runner-credential-keys.js";
14
+
15
+ /** Snapshot/restore of every env slot these tests touch. */
16
+ const savedEnv = new Map<string, string | undefined>();
17
+
18
+ beforeEach(() => {
19
+ resetRunnerSecretsForTests();
20
+ for (const key of RUNNER_SECRET_ENV_KEYS) {
21
+ savedEnv.set(key, process.env[key]);
22
+ delete process.env[key];
23
+ }
24
+ });
25
+
26
+ afterEach(() => {
27
+ resetRunnerSecretsForTests();
28
+ for (const [key, value] of savedEnv) {
29
+ if (value === undefined) {
30
+ delete process.env[key];
31
+ } else {
32
+ process.env[key] = value;
33
+ }
34
+ }
35
+ savedEnv.clear();
36
+ });
37
+
38
+ describe("captureRunnerSecrets", () => {
39
+ it("moves every listed secret out of process.env and into the store", () => {
40
+ // Plant a value for every name in the source-of-truth list, so a key
41
+ // added to runner-credential-keys.ts is covered here automatically —
42
+ // the #385 test idiom, applied to custody.
43
+ for (const key of RUNNER_SECRET_ENV_KEYS) {
44
+ process.env[key] = `boot-${key}`;
45
+ }
46
+
47
+ captureRunnerSecrets();
48
+
49
+ for (const key of RUNNER_SECRET_ENV_KEYS) {
50
+ expect(
51
+ process.env[key],
52
+ `'${key}' must not remain in process.env after capture — anything ` +
53
+ `left there is readable by every agent shell command (issue #508)`,
54
+ ).toBeUndefined();
55
+ expect(getRunnerSecret(key)).toBe(`boot-${key}`);
56
+ }
57
+ });
58
+
59
+ it("is idempotent — a second capture cannot re-freeze rotated values", () => {
60
+ process.env.STIGMER_TOKEN = "boot-token";
61
+ captureRunnerSecrets();
62
+ setRunnerSecret("STIGMER_TOKEN", "rotated-token");
63
+
64
+ // A late (buggy or racing) boot-door call must be a no-op.
65
+ process.env.STIGMER_TOKEN = "stale-replant";
66
+ captureRunnerSecrets();
67
+
68
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBe("rotated-token");
69
+ // The replant stays in env (capture did not consume it) — the live-env
70
+ // fallback below deliberately does NOT apply when the store holds a
71
+ // value, so rotation always wins.
72
+ expect(process.env.STIGMER_TOKEN).toBe("stale-replant");
73
+ delete process.env.STIGMER_TOKEN;
74
+ });
75
+
76
+ it("scrubs the encryption keys, not only the #385 credentials (issue #508)", () => {
77
+ // Pins the #508 scope widening by name. The parameterized test above
78
+ // would pass even if the combined list regressed to the 8 credentials;
79
+ // this one cannot.
80
+ for (const key of [
81
+ "STIGMER_PAYLOAD_ENCRYPTION_KEY",
82
+ "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY",
83
+ ]) {
84
+ expect(RUNNER_ENCRYPTION_ENV_KEYS, `'${key}' must be a captured secret`).toContain(key);
85
+ }
86
+ for (const key of [...RUNNER_CREDENTIAL_ENV_KEYS, ...RUNNER_ENCRYPTION_ENV_KEYS]) {
87
+ expect(RUNNER_SECRET_ENV_KEYS, `'${key}' must be in the scrub list`).toContain(key);
88
+ }
89
+ });
90
+ });
91
+
92
+ describe("getRunnerSecret", () => {
93
+ it("falls back to a live process.env value the capture never saw", () => {
94
+ captureRunnerSecrets();
95
+ // Custody rule 2: a value planted after boot behaves like the env read
96
+ // the store replaced (test/embedder compatibility) — production only
97
+ // sets these at process start, so this path is dead there.
98
+ process.env.STIGMER_TOKEN = "late-planted";
99
+
100
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBe("late-planted");
101
+ });
102
+
103
+ it("returns undefined for an absent secret", () => {
104
+ captureRunnerSecrets();
105
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBeUndefined();
106
+ });
107
+ });
108
+
109
+ describe("setRunnerSecret", () => {
110
+ it("carries the rotation channel: set updates, null clears", () => {
111
+ captureRunnerSecrets();
112
+
113
+ setRunnerSecret("STIGMER_TOKEN", "minted-1");
114
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBe("minted-1");
115
+
116
+ setRunnerSecret("STIGMER_TOKEN", "minted-2");
117
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBe("minted-2");
118
+
119
+ setRunnerSecret("STIGMER_TOKEN", null);
120
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBeUndefined();
121
+ });
122
+
123
+ it("never lets a rotated value sit in process.env, even if a writer beats the boot capture", () => {
124
+ process.env.STIGMER_TOKEN = "boot-token";
125
+
126
+ // No captureRunnerSecrets() call yet — the writer forces it.
127
+ setRunnerSecret("STIGMER_TOKEN", "rotated-early");
128
+
129
+ expect(process.env.STIGMER_TOKEN).toBeUndefined();
130
+ expect(getRunnerSecret("STIGMER_TOKEN")).toBe("rotated-early");
131
+ });
132
+ });
133
+
134
+ describe("runnerSecretsEnvView", () => {
135
+ it("merges captured secrets over process.env without writing them back", () => {
136
+ process.env.STIGMER_TOKEN = "boot-token";
137
+ captureRunnerSecrets();
138
+
139
+ const view = runnerSecretsEnvView();
140
+
141
+ expect(view.STIGMER_TOKEN).toBe("boot-token");
142
+ expect(view.PATH).toBe(process.env.PATH);
143
+ expect(
144
+ process.env.STIGMER_TOKEN,
145
+ "building the view must not re-plant secrets into process.env",
146
+ ).toBeUndefined();
147
+ });
148
+
149
+ it("reflects rotation", () => {
150
+ captureRunnerSecrets();
151
+ setRunnerSecret("STIGMER_TOKEN", "rotated");
152
+
153
+ expect(runnerSecretsEnvView().STIGMER_TOKEN).toBe("rotated");
154
+ });
155
+ });