@stigmer/runner 3.12.3 → 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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-http.js +12 -0
- package/dist/activities/call-http.js.map +1 -1
- package/dist/activities/call-llm.d.ts +18 -0
- package/dist/activities/call-llm.js +56 -2
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
- package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
- package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
- package/dist/activities/execute-cursor/index.js +61 -28
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
- package/dist/activities/execute-cursor/service-tier.js +5 -21
- package/dist/activities/execute-cursor/service-tier.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +14 -0
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
- package/dist/activities/execute-deep-agent/shell-env.js +10 -7
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/config.js +10 -5
- package/dist/config.js.map +1 -1
- package/dist/encryption/config.js +7 -2
- package/dist/encryption/config.js.map +1 -1
- package/dist/main.js +12 -6
- package/dist/main.js.map +1 -1
- package/dist/payload-codecs.js +2 -1
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +20 -7
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +19 -6
- package/dist/runner.js.map +1 -1
- package/dist/shared/fingerprint-secret.d.ts +3 -2
- package/dist/shared/fingerprint-secret.js +5 -3
- package/dist/shared/fingerprint-secret.js.map +1 -1
- package/dist/shared/llm-backend.js +8 -1
- package/dist/shared/llm-backend.js.map +1 -1
- package/dist/shared/model-client.d.ts +15 -0
- package/dist/shared/model-client.js +57 -13
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/registry-endpoint.d.ts +5 -0
- package/dist/shared/registry-endpoint.js +7 -1
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +26 -1
- package/dist/shared/runner-credential-keys.js +34 -1
- package/dist/shared/runner-credential-keys.js.map +1 -1
- package/dist/shared/runner-credential-store.d.ts +77 -0
- package/dist/shared/runner-credential-store.js +111 -0
- package/dist/shared/runner-credential-store.js.map +1 -0
- package/dist/shared/service-tier.d.ts +55 -0
- package/dist/shared/service-tier.js +67 -0
- package/dist/shared/service-tier.js.map +1 -0
- package/dist/shared/skill-writer.js +2 -2
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/zip-extract.d.ts +10 -3
- package/dist/shared/zip-extract.js +10 -3
- package/dist/shared/zip-extract.js.map +1 -1
- package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
- package/dist/workflow-engine/tasks/call-function.js +49 -5
- package/dist/workflow-engine/tasks/call-function.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +6 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/engine-core.js +36 -8
- package/dist/workflows/engine-core.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/__tests__/call-http.test.ts +36 -0
- package/src/activities/__tests__/call-llm.test.ts +77 -0
- package/src/activities/call-http.ts +17 -0
- package/src/activities/call-llm.ts +78 -2
- package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
- package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
- package/src/activities/execute-cursor/index.ts +66 -20
- package/src/activities/execute-cursor/service-tier.ts +5 -29
- package/src/activities/execute-cursor/skill-resolver.ts +1 -1
- package/src/activities/execute-deep-agent/setup.ts +15 -0
- package/src/activities/execute-deep-agent/shell-env.ts +10 -7
- package/src/config.ts +10 -5
- package/src/encryption/config.ts +8 -2
- package/src/main.ts +16 -6
- package/src/payload-codecs.ts +2 -1
- package/src/runner-manager.ts +29 -6
- package/src/runner.ts +25 -6
- package/src/shared/__tests__/model-client.test.ts +99 -0
- package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
- package/src/shared/__tests__/zip-extract.test.ts +46 -11
- package/src/shared/fingerprint-secret.ts +5 -3
- package/src/shared/llm-backend.ts +7 -1
- package/src/shared/model-client.ts +76 -13
- package/src/shared/registry-endpoint.ts +9 -1
- package/src/shared/runner-credential-keys.ts +36 -1
- package/src/shared/runner-credential-store.ts +115 -0
- package/src/shared/service-tier.ts +78 -0
- package/src/shared/skill-writer.ts +2 -2
- package/src/shared/zip-extract.ts +14 -7
- package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
- package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
- package/src/workflow-engine/tasks/call-function.ts +74 -13
- package/src/workflow-engine/types.ts +6 -0
- package/src/workflows/engine-core.ts +39 -8
|
@@ -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.
|
|
132
|
-
// standard conventions (GCP: CLOUD_ML_REGION + ADC; AWS: AWS_REGION +
|
|
133
|
-
// the credential chain
|
|
134
|
-
// ANTHROPIC_FOUNDRY_API_KEY
|
|
135
|
-
//
|
|
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({
|
|
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 (!
|
|
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
|
-
...(
|
|
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
|
|
211
|
-
// the provider's own
|
|
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
|
-
? (
|
|
216
|
-
: (
|
|
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
|
-
...
|
|
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
|
|
|
@@ -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(
|
|
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
|
-
* -
|
|
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
|
+
}
|
|
@@ -162,7 +162,7 @@ async function extractZipToWorkspace(
|
|
|
162
162
|
): Promise<void> {
|
|
163
163
|
const entries = await extractZipFileEntries(zipBytes);
|
|
164
164
|
for (const entry of entries) {
|
|
165
|
-
await backend.
|
|
165
|
+
await backend.writeFileBuffer(`${targetDir}/${entry.path}`, Buffer.from(entry.content));
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -174,7 +174,7 @@ async function extractZipToWorkspaceExcluding(
|
|
|
174
174
|
): Promise<void> {
|
|
175
175
|
const entries = await extractZipFileEntries(zipBytes, { exclude: [excludeName] });
|
|
176
176
|
for (const entry of entries) {
|
|
177
|
-
await backend.
|
|
177
|
+
await backend.writeFileBuffer(`${targetDir}/${entry.path}`, Buffer.from(entry.content));
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill-artifact ZIP extraction:
|
|
2
|
+
* Skill-artifact ZIP extraction: byte-preserving entries with non-fatal
|
|
3
3
|
* structural failure.
|
|
4
4
|
*
|
|
5
|
+
* Entries are returned as raw bytes, never decoded: skill artifacts carry
|
|
6
|
+
* binary assets (images, fonts, archives) alongside text, and a UTF-8
|
|
7
|
+
* decode/encode round-trip silently corrupts anything that isn't valid
|
|
8
|
+
* UTF-8 (issue #683). No consumer parses entry content as text — SKILL.md
|
|
9
|
+
* is excluded from extraction on every path and written from the Skill
|
|
10
|
+
* spec instead — so there is deliberately no text accessor to misuse.
|
|
11
|
+
*
|
|
5
12
|
* Structural parsing lives in zip-structure.ts (central-directory-based —
|
|
6
13
|
* see that module's doc for why local-header walks are never acceptable,
|
|
7
14
|
* issue #450). This module owns the skill-artifact *policy* on top of it:
|
|
@@ -33,8 +40,8 @@ import { EOCD_MIN_SIZE, parseZipStructure, type ZipStructuralEntry } from "./zip
|
|
|
33
40
|
export interface ZipFileEntry {
|
|
34
41
|
/** Relative path within the archive (forward-slash separated). */
|
|
35
42
|
readonly path: string;
|
|
36
|
-
/**
|
|
37
|
-
readonly content:
|
|
43
|
+
/** Raw file bytes, exactly as stored in the archive. */
|
|
44
|
+
readonly content: Uint8Array;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -91,17 +98,17 @@ function isExcluded(name: string, excludeSet: ReadonlySet<string>): boolean {
|
|
|
91
98
|
|
|
92
99
|
// ─── Decompression ───────────────────────────────────────────────────────
|
|
93
100
|
|
|
94
|
-
async function decompressEntry(entry: ZipStructuralEntry): Promise<
|
|
101
|
+
async function decompressEntry(entry: ZipStructuralEntry): Promise<Uint8Array> {
|
|
95
102
|
if (entry.compressionMethod === 0) {
|
|
96
|
-
return
|
|
103
|
+
return entry.compressedData;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
if (entry.compressionMethod === 8) {
|
|
100
|
-
return new Promise<
|
|
107
|
+
return new Promise<Uint8Array>((resolve, reject) => {
|
|
101
108
|
const inflate = createInflateRaw();
|
|
102
109
|
const chunks: Buffer[] = [];
|
|
103
110
|
inflate.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
104
|
-
inflate.on("end", () => resolve(Buffer.concat(chunks)
|
|
111
|
+
inflate.on("end", () => resolve(Buffer.concat(chunks)));
|
|
105
112
|
inflate.on("error", reject);
|
|
106
113
|
inflate.end(Buffer.from(entry.compressedData));
|
|
107
114
|
});
|
|
@@ -523,8 +523,27 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
|
|
|
523
523
|
|
|
524
524
|
await executeDoTasks(model.do, null, state, model, evaluateExpressionBatch, ctx);
|
|
525
525
|
|
|
526
|
-
expect(mockCallFunction).toHaveBeenCalledTimes(
|
|
526
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(3);
|
|
527
527
|
expect(state.data.events_emitted).toBe(true);
|
|
528
|
+
|
|
529
|
+
// Delivery targets authored in the DSL (oss#530) must reach the
|
|
530
|
+
// function-call boundary verbatim: the runner's emit activity
|
|
531
|
+
// discriminates each entry structurally on its webhook/signal key.
|
|
532
|
+
const deliveryConfig = mockCallFunction.mock.calls[2][1] as Record<string, unknown>;
|
|
533
|
+
expect(deliveryConfig.delivery).toEqual([
|
|
534
|
+
{
|
|
535
|
+
webhook: {
|
|
536
|
+
url: "https://hooks.acme.com/orders",
|
|
537
|
+
headers: { "X-Source": "stigmer" },
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
signal: {
|
|
542
|
+
execution_id: "wfx_shipping_001",
|
|
543
|
+
signal_name: "order-fulfilled",
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
]);
|
|
528
547
|
});
|
|
529
548
|
|
|
530
549
|
it("#20 human-input — HITL approval gates with timeout policies", async () => {
|
|
@@ -208,6 +208,111 @@ describe("CallFunctionTaskBuilder", () => {
|
|
|
208
208
|
// The config resolver must never pre-evaluate them — the pre-fix
|
|
209
209
|
// behavior substituted the evaluated boolean back into the config and
|
|
210
210
|
// crashed the validate activity with `expr.includes is not a function`.
|
|
211
|
+
describe("llm on_invalid policy orchestration (#686)", () => {
|
|
212
|
+
const schemaTaskDef = (extra: Record<string, unknown>): CallFunctionTaskDef => ({
|
|
213
|
+
kind: "call:function",
|
|
214
|
+
call: "llm",
|
|
215
|
+
with: {
|
|
216
|
+
model: "gpt-4o-mini",
|
|
217
|
+
prompt: "Classify",
|
|
218
|
+
response_schema: { type: "object", properties: { answer: { type: "number" } } },
|
|
219
|
+
...extra,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const invalidResult = {
|
|
224
|
+
result: undefined, model: "gpt-4o-mini", provider: "openai",
|
|
225
|
+
input_tokens: 0, output_tokens: 0, parse_error: "answer: Required",
|
|
226
|
+
};
|
|
227
|
+
const validResult = {
|
|
228
|
+
result: { answer: 42 }, model: "gpt-4o-mini", provider: "openai",
|
|
229
|
+
input_tokens: 10, output_tokens: 5,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
it("ON_INVALID_RETRY re-prompts with the validation errors and succeeds", async () => {
|
|
233
|
+
mockCallFunction
|
|
234
|
+
.mockResolvedValueOnce(invalidResult)
|
|
235
|
+
.mockResolvedValueOnce(validResult);
|
|
236
|
+
|
|
237
|
+
const builder = new CallFunctionTaskBuilder(
|
|
238
|
+
"classify", schemaTaskDef({ on_invalid: "ON_INVALID_RETRY", max_retries: 2 }),
|
|
239
|
+
);
|
|
240
|
+
const result = await builder.build()(null, createState(), makeCtx());
|
|
241
|
+
|
|
242
|
+
expect(result).toMatchObject({ structured: { answer: 42 } });
|
|
243
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(2);
|
|
244
|
+
const retryConfig = mockCallFunction.mock.calls[1][1] as Record<string, unknown>;
|
|
245
|
+
expect(retryConfig.prompt).toContain("Classify");
|
|
246
|
+
expect(retryConfig.prompt).toContain("answer: Required");
|
|
247
|
+
expect(retryConfig.prompt).toContain("RETRY");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("ON_INVALID_RETRY exhausts max_retries then fails without a fallback_task", async () => {
|
|
251
|
+
mockCallFunction.mockResolvedValue(invalidResult);
|
|
252
|
+
|
|
253
|
+
const builder = new CallFunctionTaskBuilder(
|
|
254
|
+
"classify", schemaTaskDef({ on_invalid: "ON_INVALID_RETRY", max_retries: 2 }),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await expect(builder.build()(null, createState(), makeCtx()))
|
|
258
|
+
.rejects.toThrow(/validation failed after 3 attempt\(s\).*answer: Required/);
|
|
259
|
+
// first attempt + max_retries retries
|
|
260
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(3);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("ON_INVALID_RETRY defaults max_retries to 1 (proto contract)", async () => {
|
|
264
|
+
mockCallFunction.mockResolvedValue(invalidResult);
|
|
265
|
+
|
|
266
|
+
const builder = new CallFunctionTaskBuilder(
|
|
267
|
+
"classify", schemaTaskDef({ on_invalid: "ON_INVALID_RETRY" }),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
await expect(builder.build()(null, createState(), makeCtx())).rejects.toThrow();
|
|
271
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(2);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("exhausted retries branch to fallback_task when set", async () => {
|
|
275
|
+
mockCallFunction.mockResolvedValue(invalidResult);
|
|
276
|
+
|
|
277
|
+
const builder = new CallFunctionTaskBuilder(
|
|
278
|
+
"classify",
|
|
279
|
+
schemaTaskDef({ on_invalid: "ON_INVALID_RETRY", max_retries: 1, fallback_task: "human_review" }),
|
|
280
|
+
);
|
|
281
|
+
const result = await builder.build()(null, createState(), makeCtx());
|
|
282
|
+
|
|
283
|
+
expect(result).toEqual({
|
|
284
|
+
__flow_directive__: "human_review",
|
|
285
|
+
validation_errors: ["answer: Required"],
|
|
286
|
+
});
|
|
287
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("ON_INVALID_FALLBACK branches immediately without retrying", async () => {
|
|
291
|
+
mockCallFunction.mockResolvedValue(invalidResult);
|
|
292
|
+
|
|
293
|
+
const builder = new CallFunctionTaskBuilder(
|
|
294
|
+
"classify",
|
|
295
|
+
schemaTaskDef({ on_invalid: "ON_INVALID_FALLBACK", fallback_task: "human_review" }),
|
|
296
|
+
);
|
|
297
|
+
const result = await builder.build()(null, createState(), makeCtx());
|
|
298
|
+
|
|
299
|
+
expect(result).toMatchObject({ __flow_directive__: "human_review" });
|
|
300
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(1);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("a parse_error without a soft policy passes through as normal output (activity owns the failure)", async () => {
|
|
304
|
+
// Default policy: the activity throws LLM_SCHEMA_VALIDATION itself and
|
|
305
|
+
// never returns parse_error — the engine must not add a second layer.
|
|
306
|
+
mockCallFunction.mockResolvedValue(validResult);
|
|
307
|
+
|
|
308
|
+
const builder = new CallFunctionTaskBuilder("classify", schemaTaskDef({}));
|
|
309
|
+
const result = await builder.build()(null, createState(), makeCtx());
|
|
310
|
+
|
|
311
|
+
expect(result).toMatchObject({ structured: { answer: 42 } });
|
|
312
|
+
expect(mockCallFunction).toHaveBeenCalledTimes(1);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
211
316
|
describe("deferred expression fields", () => {
|
|
212
317
|
it("passes validate rules[].expression through unresolved while input and message interpolate", async () => {
|
|
213
318
|
mockCallFunction.mockResolvedValue({ valid: true, errors: [] });
|