@relayflows/sdk 2.0.49 → 2.0.51
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/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +4 -2
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-preflight.js +4 -4
- package/dist/authored-preflight.js.map +1 -1
- package/dist/authored-worker-step.d.ts +2 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +11 -5
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.js +2 -2
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +1 -1
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +8 -7
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +2 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +6 -2
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli-commands.d.ts +4 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +5 -1
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloud-http.d.ts +1 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +1 -1
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +6 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +56 -6
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-signal.d.ts +12 -0
- package/dist/cloud-signal.d.ts.map +1 -0
- package/dist/cloud-signal.js +117 -0
- package/dist/cloud-signal.js.map +1 -0
- package/dist/cloud-sync.d.ts +8 -1
- package/dist/cloud-sync.d.ts.map +1 -1
- package/dist/cloud-sync.js +36 -4
- package/dist/cloud-sync.js.map +1 -1
- package/dist/communication/environment.d.ts +1 -1
- package/dist/communication/environment.d.ts.map +1 -1
- package/dist/communication/environment.js +26 -3
- package/dist/communication/environment.js.map +1 -1
- package/dist/communication/worker.js +1 -1
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -2
- package/dist/compile.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +2 -0
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +26 -3
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +1 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/observer-link.d.ts +7 -7
- package/dist/observer-link.js +6 -6
- package/dist/preflight.d.ts +2 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +12 -7
- package/dist/preflight.js.map +1 -1
- package/dist/spec.d.ts +4 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.d.ts.map +1 -1
- package/dist/step-fields.js +1 -0
- package/dist/step-fields.js.map +1 -1
- package/dist/step-secrets.d.ts +20 -0
- package/dist/step-secrets.d.ts.map +1 -0
- package/dist/step-secrets.js +146 -0
- package/dist/step-secrets.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +10 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +5 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +43 -14
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +4 -2
- package/src/authored-preflight.ts +4 -4
- package/src/authored-worker-step.ts +17 -6
- package/src/cli/check.ts +3 -3
- package/src/cli/cli-probe.ts +9 -6
- package/src/cli/cloud-run.ts +7 -2
- package/src/cli-commands.ts +5 -1
- package/src/cli.ts +26 -8
- package/src/cloud-http.ts +1 -1
- package/src/cloud-run.ts +61 -8
- package/src/cloud-signal.ts +110 -0
- package/src/cloud-sync.ts +39 -4
- package/src/communication/environment.ts +28 -3
- package/src/communication/worker.ts +2 -1
- package/src/compile.ts +5 -2
- package/src/index.ts +1 -0
- package/src/journal-client.ts +29 -2
- package/src/llm-worker.ts +2 -1
- package/src/observer-link.ts +7 -7
- package/src/preflight.ts +13 -7
- package/src/spec.ts +4 -0
- package/src/step-fields.ts +1 -0
- package/src/step-secrets.ts +145 -0
- package/src/validate.ts +11 -0
- package/src/worker-cli.ts +41 -13
- package/src/worker.ts +3 -2
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { wrapperEnvironment } from '../wrapper-runtime.js';
|
|
3
|
+
import { deliveredNames, isCliLoginName, platformAgentNames, repositoryEnvironmentNames,
|
|
4
|
+
validateStepSecrets } from '../step-secrets.js';
|
|
3
5
|
|
|
4
6
|
const terminalNames = ['TERM', 'COLORTERM', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME'];
|
|
7
|
+
const networkNames = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
8
|
+
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy'];
|
|
5
9
|
const relayNames = ['RELAY_API_KEY', 'RELAY_WORKSPACE_KEY', 'AGENT_RELAY_WORKSPACE_KEY',
|
|
6
10
|
'RELAY_BROKER_API_KEY', 'RELAY_AGENT_TOKEN', 'RELAY_NODE_TOKEN', 'RELAY_WORKSPACES_JSON'];
|
|
7
11
|
// Multi-provider CLIs select their provider in their own configuration.
|
|
8
12
|
const multiProvider = ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL',
|
|
9
13
|
'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'] as const;
|
|
10
14
|
const providerNames: Readonly<Record<string, readonly string[]>> = {
|
|
11
|
-
claude: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR'
|
|
15
|
+
claude: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR',
|
|
16
|
+
'CLAUDE_CODE_USE_BEDROCK', 'AWS_BEARER_TOKEN_BEDROCK', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY',
|
|
17
|
+
'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION'],
|
|
12
18
|
codex: ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'CODEX_HOME'],
|
|
13
19
|
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'],
|
|
14
20
|
// AgentWorkforce/cloud#4026 passes both endpoint names unchanged as aliases
|
|
@@ -32,9 +38,28 @@ export function brokerEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv
|
|
|
32
38
|
return { ...Object.fromEntries(Object.keys(source).map(name => [name, undefined])),
|
|
33
39
|
...wrapperEnvironment(source), ...selected(source, [...terminalNames, 'RELAY_BASE_URL']) };
|
|
34
40
|
}
|
|
35
|
-
export function agentEnvironment(cli: string, source: NodeJS.ProcessEnv = process.env
|
|
41
|
+
export function agentEnvironment(cli: string, source: NodeJS.ProcessEnv = process.env,
|
|
42
|
+
grants?: string[], scopeSecrets = true): Record<string, string> {
|
|
36
43
|
// Broker transport credentials must not become CLI credentials. The harness
|
|
37
44
|
// protocol accepts string values, so clear those inherited names explicitly.
|
|
45
|
+
const cliName = basename(cli).replace(/\.exe$/i, '');
|
|
46
|
+
const credentials = { ...selected(source, providerNames[cliName] ?? []),
|
|
47
|
+
...selected(source, [...repositoryEnvironmentNames(source), ...networkNames]) };
|
|
48
|
+
if (!scopeSecrets && grants?.length) {
|
|
49
|
+
throw new Error('per-step secret grants require a daemon with step_secret_scoping support');
|
|
50
|
+
}
|
|
51
|
+
const names = deliveredNames(source);
|
|
52
|
+
const platform = platformAgentNames(source, cliName);
|
|
53
|
+
const allowed = scopeSecrets ? validateStepSecrets(grants, source) : [];
|
|
54
|
+
if (allowed.some(name => relayNames.includes(name))) {
|
|
55
|
+
throw new Error('Relay broker credentials cannot be granted to a communication agent');
|
|
56
|
+
}
|
|
57
|
+
for (const name of names ?? []) {
|
|
58
|
+
if (!allowed.includes(name) && !platform.has(name)
|
|
59
|
+
&& !isCliLoginName(cliName, name, providerNames[cliName])) {
|
|
60
|
+
delete credentials[name];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
38
63
|
return { ...Object.fromEntries(relayNames.map(name => [name, ''])),
|
|
39
|
-
...selected(source,
|
|
64
|
+
...credentials, ...selected(source, allowed) } as Record<string, string>;
|
|
40
65
|
}
|
|
@@ -103,7 +103,8 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
103
103
|
handle = await relay.broker.spawnPty({ name, cli: basename(cliIdentity).replace(/\.exe$/i, ''), task: prompt, channels: [], skipRelayPrompt: true,
|
|
104
104
|
model: resolveCliModel(cliIdentity, spec.model), cwd: directory,
|
|
105
105
|
harnessConfig: { runtime: 'pty', command: quote(command), args: [],
|
|
106
|
-
cwd: directory, env: { ...agentEnvironment(cliIdentity, environment ?? process.env
|
|
106
|
+
cwd: directory, env: { ...agentEnvironment(cliIdentity, environment ?? process.env,
|
|
107
|
+
spec.secrets, client.supportsFeature?.('step_secret_scoping') ?? true),
|
|
107
108
|
RELAYFLOW_COMMUNICATION_SOCKET: tools.path, RELAYFLOW_COMMUNICATION_TOKEN: tools.token },
|
|
108
109
|
delivery: { mode: 'pty-injection', format: 'relay-block' } } });
|
|
109
110
|
const ready = await handle.waitForReady(Math.min(instruction.timeoutMs, 90_000));
|
package/src/compile.ts
CHANGED
|
@@ -192,6 +192,7 @@ function compileStep(step: StepSpec): StepSpec {
|
|
|
192
192
|
const base = {
|
|
193
193
|
id: step.id,
|
|
194
194
|
type: step.type,
|
|
195
|
+
...(step.secrets?.length ? { secrets: step.secrets } : {}),
|
|
195
196
|
...((step.dependsOn !== undefined || step.input !== undefined)
|
|
196
197
|
? { dependsOn: step.input === undefined ? step.dependsOn : [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])] } : {}),
|
|
197
198
|
...(step.input !== undefined ? { input: step.input } : {}),
|
|
@@ -466,7 +467,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
466
467
|
const unionKeys = [
|
|
467
468
|
'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
|
|
468
469
|
'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
|
|
469
|
-
'cwd', 'resume', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity',
|
|
470
|
+
'cwd', 'resume', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity', 'secrets',
|
|
470
471
|
] as const;
|
|
471
472
|
const step = requireKernelObject(value, unionKeys, at);
|
|
472
473
|
if (step['cli_identity'] !== undefined) {
|
|
@@ -475,7 +476,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
475
476
|
], 'untrusted_cli_identity');
|
|
476
477
|
}
|
|
477
478
|
const type = step['type'];
|
|
478
|
-
const commonKeys = ['id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input'] as const;
|
|
479
|
+
const commonKeys = ['id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input', 'secrets'] as const;
|
|
479
480
|
const typeKeys = type === 'deterministic'
|
|
480
481
|
? ['command', 'timeout_ms', 'lease_ms', 'on_non_zero'] as const
|
|
481
482
|
: type === 'llm'
|
|
@@ -491,6 +492,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
491
492
|
id: step['id'],
|
|
492
493
|
type,
|
|
493
494
|
...(step['input'] !== undefined ? { input: step['input'] } : {}),
|
|
495
|
+
...(step['secrets'] !== undefined ? { secrets: step['secrets'] } : {}),
|
|
494
496
|
...(step['requirements'] !== undefined ? { requirements: kernelRequirementsToAuthoring(step['requirements'], `${at}.requirements`) } : {}),
|
|
495
497
|
...(dependsOn !== undefined && (!Array.isArray(dependsOn) || dependsOn.length > 0)
|
|
496
498
|
? { dependsOn }
|
|
@@ -636,6 +638,7 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
636
638
|
function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepSpec {
|
|
637
639
|
const common: ResolvedKernelStepCommon = {
|
|
638
640
|
id: step.id,
|
|
641
|
+
...(step.secrets?.length ? { secrets: step.secrets } : {}),
|
|
639
642
|
depends_on: step.input === undefined ? step.dependsOn ?? []
|
|
640
643
|
: [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])],
|
|
641
644
|
...(step.input !== undefined ? { input: step.input } : {}),
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ export {
|
|
|
81
81
|
} from './flow-extension-loader.js';
|
|
82
82
|
|
|
83
83
|
export { CloudFlowError, type CloudConnectionOptions, type CloudConfigurationReason } from './cloud-http.js';
|
|
84
|
+
export { postCloudSignal, waitForCloudSignal } from './cloud-signal.js';
|
|
84
85
|
export {
|
|
85
86
|
listCloudRuns, getCloudRunDetail, getCloudRunSteps, getCloudRunLog,
|
|
86
87
|
type CloudRunSummary, type CloudRunList, type CloudRunDetail, type CloudStep, type CloudStepAttempt,
|
package/src/journal-client.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
type StepUsage,
|
|
26
26
|
} from './protocol.js';
|
|
27
27
|
import type { KernelRunSpec, StepType } from './spec.js';
|
|
28
|
+
import { PLATFORM_AGENT_ENV_NAMES, SECRET_ENV_NAMES, SIGNAL_READ_TOKEN_ENV,
|
|
29
|
+
deliveredNames, validateStepSecrets } from './step-secrets.js';
|
|
28
30
|
|
|
29
31
|
export interface JournalClientOptions {
|
|
30
32
|
/** Opt-in total budget for read-only requests; interactive clients stay single-shot. */
|
|
@@ -78,6 +80,9 @@ export class JournalClient extends JournalConnection {
|
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/** Additive daemon capability negotiated by this session's hello. */
|
|
84
|
+
supportsFeature(name: string): boolean { return this.features.has(name); }
|
|
85
|
+
|
|
81
86
|
private request<V extends keyof VerbContract>(verb: V, params: VerbContract[V]['params'],
|
|
82
87
|
timeoutMs: number | null = this.requestTimeoutMs, signal?: AbortSignal): Promise<VerbContract[V]['result']> {
|
|
83
88
|
// Every allowlisted read takes the policy, even after the primary dropped: its
|
|
@@ -131,6 +136,16 @@ export class JournalClient extends JournalConnection {
|
|
|
131
136
|
admissionKey?: string,
|
|
132
137
|
watch = false,
|
|
133
138
|
): Promise<VerbContract['run.start']['result']> {
|
|
139
|
+
for (const step of spec.steps) {
|
|
140
|
+
for (const name of validateStepSecrets(step.secrets, process.env)) {
|
|
141
|
+
if (this.withheld.has(name)) {
|
|
142
|
+
throw new Error(`step secret ${JSON.stringify(name)} is withheld by the daemon`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!this.features.has('step_secret_scoping') && spec.steps.some(step => step.secrets?.length)) {
|
|
147
|
+
throw new Error('per-step secret grants require a daemon with step_secret_scoping support');
|
|
148
|
+
}
|
|
134
149
|
return this.request('run.start', {
|
|
135
150
|
spec,
|
|
136
151
|
...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
|
|
@@ -158,10 +173,22 @@ export class JournalClient extends JournalConnection {
|
|
|
158
173
|
* alike, so their values never cross the socket from this client.
|
|
159
174
|
*/
|
|
160
175
|
private stepEnv(): { env?: Record<string, string> } {
|
|
161
|
-
if (!this.features.has('step_env'))
|
|
176
|
+
if (!this.features.has('step_env')) {
|
|
177
|
+
// A client with no declared grants keeps compatibility with the
|
|
178
|
+
// currently pinned hosted runtime until Cloud upgrades the artifact.
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
162
181
|
const env: Record<string, string> = {};
|
|
182
|
+
const delivered = deliveredNames(process.env);
|
|
183
|
+
if (delivered !== undefined && this.withheld.has(SECRET_ENV_NAMES)) {
|
|
184
|
+
throw new Error(`${SECRET_ENV_NAMES} is withheld by the daemon; delivered secrets cannot be scoped`);
|
|
185
|
+
}
|
|
163
186
|
for (const [key, value] of Object.entries(process.env)) {
|
|
164
|
-
if (value !== undefined &&
|
|
187
|
+
if (value !== undefined && key !== SIGNAL_READ_TOKEN_ENV && key !== PLATFORM_AGENT_ENV_NAMES
|
|
188
|
+
&& !this.withheld.has(key)) {
|
|
189
|
+
env[key] = key === SECRET_ENV_NAMES && delivered !== undefined
|
|
190
|
+
? JSON.stringify([...delivered].filter(name => !this.withheld.has(name))) : value;
|
|
191
|
+
}
|
|
165
192
|
}
|
|
166
193
|
return { env };
|
|
167
194
|
}
|
package/src/llm-worker.ts
CHANGED
|
@@ -64,7 +64,8 @@ export class LlmWorker extends EventEmitter {
|
|
|
64
64
|
const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
|
|
65
65
|
typeof spec.cli === 'string' && typeof spec.prompt === 'string'
|
|
66
66
|
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm',
|
|
67
|
-
undefined, undefined, 'direct', undefined, this.environment, spec.cli_identity
|
|
67
|
+
undefined, undefined, 'direct', undefined, this.environment, spec.cli_identity,
|
|
68
|
+
undefined, undefined, spec.secrets, this.client.supportsFeature?.('step_secret_scoping') ?? true)
|
|
68
69
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'llm step has no declared CLI' }));
|
|
69
70
|
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
70
71
|
const cost = reportedCost(completed, effectiveModel);
|
package/src/observer-link.ts
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* scoped, short-lived observer token on their behalf and hand *that* to the
|
|
12
12
|
* URL. The workspace admin key never leaves the operator's machine.
|
|
13
13
|
*
|
|
14
|
-
* Everything here is best-effort. A missing workspace key means
|
|
15
|
-
* link
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Everything here is best-effort. A missing workspace key means no observer
|
|
15
|
+
* link; the human-facing CLI explains the alternatives for a plain local run.
|
|
16
|
+
* A mint failure emits a labeled diagnostic to stderr and continues; a run
|
|
17
|
+
* must never fail because the observer link failed.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
@@ -165,7 +165,7 @@ export interface ResolveObserverLinkEnvOptions {
|
|
|
165
165
|
* 2. Active workspace key from `~/.agentworkforce/relay/workspaces.json`
|
|
166
166
|
* (populated by `agent-relay cloud login` + `agent-relay workspace
|
|
167
167
|
* set_key`), when readable.
|
|
168
|
-
* 3. Nothing --
|
|
168
|
+
* 3. Nothing -- the caller decides whether its output mode warrants a hint.
|
|
169
169
|
*
|
|
170
170
|
* `FLOWS_NO_OBSERVER=1` still suppresses regardless of source.
|
|
171
171
|
*/
|
|
@@ -189,7 +189,7 @@ export function resolveObserverLinkEnv(
|
|
|
189
189
|
/**
|
|
190
190
|
* Read the observer-link config from the environment. Both trims and
|
|
191
191
|
* emptiness-checks the workspace key so a `RELAYCAST_WORKSPACE_KEY=` with no
|
|
192
|
-
* value reads the same as unset (no observer link
|
|
192
|
+
* value reads the same as unset (no observer link).
|
|
193
193
|
*/
|
|
194
194
|
export function readObserverLinkEnv(
|
|
195
195
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -258,7 +258,7 @@ export interface MintObserverOutcome {
|
|
|
258
258
|
/**
|
|
259
259
|
* A short diagnostic to log when mint failed. `undefined` on success or on
|
|
260
260
|
* "no observer link is possible" (missing key/suppressed) so the caller can
|
|
261
|
-
* distinguish
|
|
261
|
+
* distinguish an unavailable link from "warn about a mint failure".
|
|
262
262
|
*/
|
|
263
263
|
warning?: string;
|
|
264
264
|
}
|
package/src/preflight.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface PreflightProbes {
|
|
|
91
91
|
* model in scope, so readiness answers "can this CLI use THIS model" rather
|
|
92
92
|
* than the weaker "is this CLI authenticated at all".
|
|
93
93
|
*/
|
|
94
|
-
cli(cli: string, source: CliResolutionSource, model?: string, execution?: 'managed'): CliProbeResult;
|
|
94
|
+
cli(cli: string, source: CliResolutionSource, model?: string, execution?: 'managed', secrets?: string[]): CliProbeResult;
|
|
95
95
|
executor(trigger: TriggerSpec): boolean;
|
|
96
96
|
command(binary: string): boolean;
|
|
97
97
|
}
|
|
@@ -340,7 +340,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
340
340
|
}
|
|
341
341
|
const resolution = resolutionByStep.get(step.id)!;
|
|
342
342
|
probeResolvedCli(resolution, options.probes, cliProbeResults, diagnostics,
|
|
343
|
-
step.type === 'agent' && communicationInstruction(step.instruction) !== undefined);
|
|
343
|
+
step.type === 'agent' && communicationInstruction(step.instruction) !== undefined, step.secrets);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
for (const trigger of compiled.triggers ?? []) {
|
|
@@ -529,8 +529,9 @@ function resolveCli(
|
|
|
529
529
|
return undefined;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
export function cliProbeKey(resolution: CliResolution, managed = false): string {
|
|
533
|
-
return JSON.stringify([resolution.cli, resolution.source, resolution.model ?? null, managed
|
|
532
|
+
export function cliProbeKey(resolution: CliResolution, managed = false, secrets?: readonly string[]): string {
|
|
533
|
+
return JSON.stringify([resolution.cli, resolution.source, resolution.model ?? null, managed,
|
|
534
|
+
...(secrets?.length ? [[...secrets].sort()] : [])]);
|
|
534
535
|
}
|
|
535
536
|
|
|
536
537
|
function probeResolvedCli(
|
|
@@ -539,6 +540,7 @@ function probeResolvedCli(
|
|
|
539
540
|
cache: Map<string, CliProbeOutcome>,
|
|
540
541
|
diagnostics: PreflightDiagnostic[],
|
|
541
542
|
managed = false,
|
|
543
|
+
secrets?: string[],
|
|
542
544
|
): void {
|
|
543
545
|
// Source is load-bearing: the same relative CLI string resolves from the
|
|
544
546
|
// flow directory for step/named/flow declarations and the config directory for
|
|
@@ -546,13 +548,17 @@ function probeResolvedCli(
|
|
|
546
548
|
// Model is part of the key: the same CLI probed with two different models
|
|
547
549
|
// is two different questions, and caching on the CLI alone would let a
|
|
548
550
|
// model that the CLI cannot resolve inherit an earlier model's pass.
|
|
549
|
-
const cacheKey = cliProbeKey(resolution, managed);
|
|
551
|
+
const cacheKey = cliProbeKey(resolution, managed, secrets);
|
|
550
552
|
let outcome = cache.get(cacheKey);
|
|
551
553
|
if (outcome === undefined) {
|
|
552
554
|
try {
|
|
553
555
|
outcome = { result: managed
|
|
554
|
-
?
|
|
555
|
-
|
|
556
|
+
? secrets?.length
|
|
557
|
+
? probes.cli(resolution.cli, resolution.source, resolution.model, 'managed', secrets)
|
|
558
|
+
: probes.cli(resolution.cli, resolution.source, resolution.model, 'managed')
|
|
559
|
+
: secrets?.length
|
|
560
|
+
? probes.cli(resolution.cli, resolution.source, resolution.model, undefined, secrets)
|
|
561
|
+
: probes.cli(resolution.cli, resolution.source, resolution.model) };
|
|
556
562
|
} catch (error) {
|
|
557
563
|
const detail = error instanceof CliProbeError ? error.detail : undefined;
|
|
558
564
|
outcome = { failure: detail ?? null };
|
package/src/spec.ts
CHANGED
|
@@ -217,6 +217,8 @@ export interface KernelPlacementRequirements extends Omit<PlacementRequirements,
|
|
|
217
217
|
|
|
218
218
|
/** Fields shared by every step on the ladder. */
|
|
219
219
|
export interface BaseStepSpec {
|
|
220
|
+
/** Names of run secrets granted to this step; values never enter the spec. */
|
|
221
|
+
secrets?: string[];
|
|
220
222
|
/** Named values selected from earlier steps' declared, verified outputs. */
|
|
221
223
|
input?: Record<string, OutputBinding>;
|
|
222
224
|
requirements?: PlacementRequirements;
|
|
@@ -478,6 +480,8 @@ export interface KernelVerificationSpec {
|
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
export interface KernelStepCommon {
|
|
483
|
+
/** Names only; the worker/kernel selects values from the run environment. */
|
|
484
|
+
secrets?: string[];
|
|
481
485
|
input?: Record<string, OutputBinding>;
|
|
482
486
|
requirements?: KernelPlacementRequirements;
|
|
483
487
|
memory?: KernelMemorySpec;
|
package/src/step-fields.ts
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { CliAdapterKind } from './cli-adapter.js';
|
|
2
|
+
|
|
3
|
+
/** Cloud supplies this name-only manifest alongside delivered envSecrets. */
|
|
4
|
+
export const SECRET_ENV_NAMES = 'RELAY_SECRET_ENV_NAMES';
|
|
5
|
+
/** Cloud's name-only exception for model login it selected from delivered values. */
|
|
6
|
+
export const PLATFORM_AGENT_ENV_NAMES = 'RELAY_PLATFORM_AGENT_ENV_NAMES';
|
|
7
|
+
/** CLI-only hosted signal read capability. Never a run or step secret. */
|
|
8
|
+
export const SIGNAL_READ_TOKEN_ENV = 'RELAYFLOW_V2_SIGNAL_READ_TOKEN';
|
|
9
|
+
const VALID_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
const BEDROCK_LOGIN = new Set([
|
|
11
|
+
'CLAUDE_CODE_USE_BEDROCK', 'AWS_BEARER_TOKEN_BEDROCK',
|
|
12
|
+
'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN',
|
|
13
|
+
'AWS_REGION', 'AWS_DEFAULT_REGION',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const REPOSITORY_ENV = new Set([
|
|
17
|
+
'GH_TOKEN', 'GH_CONFIG_DIR', 'GH_PROMPT_DISABLED', 'GITLAB_TOKEN',
|
|
18
|
+
'RELAYFLOW_V2_GITLAB_API_URL',
|
|
19
|
+
]);
|
|
20
|
+
export function repositoryEnvironmentNames(source: NodeJS.ProcessEnv): string[] {
|
|
21
|
+
return Object.keys(source).filter(name => REPOSITORY_ENV.has(name) || name.startsWith('GIT_')
|
|
22
|
+
|| name.startsWith('RELAYFLOW_V2_REPOSITORY_'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const BASE_ENV = [
|
|
26
|
+
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TERM', 'LANG', 'LANGUAGE',
|
|
27
|
+
'LC_ALL', 'LC_CTYPE', 'TZ', 'TMPDIR', 'TMP', 'TEMP', 'XDG_RUNTIME_DIR',
|
|
28
|
+
'XDG_CACHE_HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'HOSTNAME',
|
|
29
|
+
'NODE_PATH', 'NODE_ENV', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE',
|
|
30
|
+
'SSL_CERT_DIR', 'NVM_DIR', 'NVM_BIN', 'NVM_INC',
|
|
31
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
32
|
+
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
|
|
33
|
+
'NVM_SYMLINK_CURRENT', 'PIPX_HOME', 'PIPX_BIN_DIR', 'PYTHON_PATH',
|
|
34
|
+
'PYTHON_VERSION', 'PYTHON_SHA256', 'RELAYFILE_MOUNT_PATH',
|
|
35
|
+
'RELAYFILE_MOUNT_PATHS', 'RELAYFILE_MOUNT_ROOT', 'WORKSPACE_ROOT',
|
|
36
|
+
'RELAY_BASE_URL',
|
|
37
|
+
'SystemRoot', 'ComSpec', 'PATHEXT', 'WINDIR',
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
// Model login is an intrinsic capability of the selected CLI. It is not a
|
|
41
|
+
// run's delivered envSecret; only that CLI gets its own login variables.
|
|
42
|
+
const CLI_LOGIN: Record<CliAdapterKind, readonly string[]> = {
|
|
43
|
+
claude: [
|
|
44
|
+
'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR', 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN',
|
|
45
|
+
'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK',
|
|
46
|
+
'AWS_BEARER_TOKEN_BEDROCK', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY',
|
|
47
|
+
'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION',
|
|
48
|
+
],
|
|
49
|
+
codex: ['CODEX_HOME', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENROUTER_API_KEY'],
|
|
50
|
+
'relayflows-wrapper-v1': [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** A selected CLI can use its own delivered login; generic cloud credentials need a grant. */
|
|
54
|
+
export function isCliLoginName(kind: string, name: string, providerNames?: readonly string[]): boolean {
|
|
55
|
+
if (BEDROCK_LOGIN.has(name) || name.startsWith('AWS_')
|
|
56
|
+
|| name === 'GOOGLE_APPLICATION_CREDENTIALS' || name === 'GOOGLE_API_KEY') return false;
|
|
57
|
+
const names = providerNames ?? (Object.hasOwn(CLI_LOGIN, kind) ? CLI_LOGIN[kind as CliAdapterKind] : []);
|
|
58
|
+
return names.includes(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function deliveredNames(source: NodeJS.ProcessEnv): ReadonlySet<string> | undefined {
|
|
62
|
+
const raw = source[SECRET_ENV_NAMES];
|
|
63
|
+
if (raw === undefined) return undefined; // self-hosted runs may grant present env names
|
|
64
|
+
let parsed: unknown;
|
|
65
|
+
try { parsed = JSON.parse(raw); } catch { throw new Error(`${SECRET_ENV_NAMES} must be a JSON array of names`); }
|
|
66
|
+
if (!Array.isArray(parsed) || !parsed.every(name => typeof name === 'string' && VALID_NAME.test(name))) {
|
|
67
|
+
throw new Error(`${SECRET_ENV_NAMES} must be a JSON array of valid names`);
|
|
68
|
+
}
|
|
69
|
+
if (new Set(parsed).size !== parsed.length || parsed.includes(SECRET_ENV_NAMES)
|
|
70
|
+
|| parsed.includes(PLATFORM_AGENT_ENV_NAMES)
|
|
71
|
+
|| parsed.includes(SIGNAL_READ_TOKEN_ENV)) {
|
|
72
|
+
throw new Error(`${SECRET_ENV_NAMES} contains a duplicate or reserved name`);
|
|
73
|
+
}
|
|
74
|
+
return new Set(parsed);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Only Cloud-selected Claude Bedrock login may override the delivered filter. */
|
|
78
|
+
export function platformAgentNames(source: NodeJS.ProcessEnv, kind: string): ReadonlySet<string> {
|
|
79
|
+
const raw = source[PLATFORM_AGENT_ENV_NAMES];
|
|
80
|
+
if (raw === undefined) return new Set();
|
|
81
|
+
let parsed: unknown;
|
|
82
|
+
try { parsed = JSON.parse(raw); } catch { throw new Error(`${PLATFORM_AGENT_ENV_NAMES} must be a JSON array of names`); }
|
|
83
|
+
if (!Array.isArray(parsed) || !parsed.every(name => typeof name === 'string' && BEDROCK_LOGIN.has(name)
|
|
84
|
+
&& typeof source[name] === 'string') || new Set(parsed).size !== parsed.length) {
|
|
85
|
+
throw new Error(`${PLATFORM_AGENT_ENV_NAMES} contains an invalid or unavailable model login name`);
|
|
86
|
+
}
|
|
87
|
+
return kind === 'claude' ? new Set(parsed) : new Set();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Refuse a misspelled, unavailable or platform-owned name before a child runs. */
|
|
91
|
+
export function validateStepSecrets(declaration: unknown, source: NodeJS.ProcessEnv): string[] {
|
|
92
|
+
const known = deliveredNames(source);
|
|
93
|
+
if (declaration === undefined) return [];
|
|
94
|
+
if (!Array.isArray(declaration) || !declaration.every(name => typeof name === 'string' && VALID_NAME.test(name))) {
|
|
95
|
+
throw new Error('step secrets must be an array of environment variable names');
|
|
96
|
+
}
|
|
97
|
+
const names = declaration as string[];
|
|
98
|
+
if (new Set(names).size !== names.length) throw new Error('step secrets contain duplicate names');
|
|
99
|
+
for (const name of names) {
|
|
100
|
+
if (name === SECRET_ENV_NAMES || name === PLATFORM_AGENT_ENV_NAMES || name === SIGNAL_READ_TOKEN_ENV
|
|
101
|
+
|| !Object.hasOwn(source, name) || typeof source[name] !== 'string'
|
|
102
|
+
|| (known !== undefined && !known.has(name))) {
|
|
103
|
+
throw new Error(`step secret ${JSON.stringify(name)} is not available to this run`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [...names];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One provider CLI starts from an allowlist, then receives only its grants. */
|
|
110
|
+
export function agentStepEnvironment(
|
|
111
|
+
source: NodeJS.ProcessEnv,
|
|
112
|
+
kind: CliAdapterKind,
|
|
113
|
+
declaration?: unknown,
|
|
114
|
+
): NodeJS.ProcessEnv {
|
|
115
|
+
const known = deliveredNames(source);
|
|
116
|
+
const platform = platformAgentNames(source, kind);
|
|
117
|
+
const grants = validateStepSecrets(declaration, source);
|
|
118
|
+
const env: NodeJS.ProcessEnv = {};
|
|
119
|
+
for (const name of [...BASE_ENV, ...CLI_LOGIN[kind], ...repositoryEnvironmentNames(source)]) {
|
|
120
|
+
if (known?.has(name) && !grants.includes(name) && !platform.has(name)
|
|
121
|
+
&& !isCliLoginName(kind, name)) continue;
|
|
122
|
+
if (source[name] !== undefined) env[name] = source[name];
|
|
123
|
+
}
|
|
124
|
+
for (const name of grants) env[name] = source[name];
|
|
125
|
+
return env;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Provider readiness probes run before worker attach, but obey the step's grants. */
|
|
129
|
+
export function agentProbeEnvironment(
|
|
130
|
+
source: NodeJS.ProcessEnv,
|
|
131
|
+
kind: CliAdapterKind,
|
|
132
|
+
declaration?: unknown,
|
|
133
|
+
): NodeJS.ProcessEnv {
|
|
134
|
+
const env = { ...source };
|
|
135
|
+
const allowed = new Set(validateStepSecrets(declaration, source));
|
|
136
|
+
const platform = platformAgentNames(source, kind);
|
|
137
|
+
for (const name of deliveredNames(source) ?? []) {
|
|
138
|
+
if (!allowed.has(name) && !platform.has(name) && !isCliLoginName(kind, name)) delete env[name];
|
|
139
|
+
}
|
|
140
|
+
delete env[SECRET_ENV_NAMES];
|
|
141
|
+
delete env[PLATFORM_AGENT_ENV_NAMES];
|
|
142
|
+
delete env[SIGNAL_READ_TOKEN_ENV];
|
|
143
|
+
delete env.RELAY_AGENT_TOKEN;
|
|
144
|
+
return env;
|
|
145
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -388,6 +388,14 @@ class Validator {
|
|
|
388
388
|
const type = st['type'] as StepType;
|
|
389
389
|
this.checkKeys(st, [...STEP_COMMON_FIELDS, ...STEP_FIELDS_BY_TYPE[type]], at);
|
|
390
390
|
|
|
391
|
+
if (st['secrets'] !== undefined) {
|
|
392
|
+
const names = st['secrets'];
|
|
393
|
+
if (!Array.isArray(names) || !names.every(name => typeof name === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
394
|
+
|| new Set(names).size !== names.length || names.includes('RELAY_SECRET_ENV_NAMES')) {
|
|
395
|
+
this.fail(`${at}.secrets: expected distinct environment variable names`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
391
399
|
if (st['requirements'] !== undefined) this.validateRequirements(st['requirements'], `${at}.requirements`);
|
|
392
400
|
if (st['memory'] !== undefined) this.validateMemory(st['memory'], `${at}.memory`);
|
|
393
401
|
|
|
@@ -491,6 +499,9 @@ class Validator {
|
|
|
491
499
|
}
|
|
492
500
|
|
|
493
501
|
private validateAgent(st: AgentStepSpec, at: string): void {
|
|
502
|
+
if (st.transport === 'relay' && st.secrets?.length) {
|
|
503
|
+
this.fail(`${at}.secrets: relay transport cannot deliver per-step secrets`);
|
|
504
|
+
}
|
|
494
505
|
if (st.timeoutMs !== undefined) {
|
|
495
506
|
if (!isPosInt(st.timeoutMs) || st.timeoutMs > 60 * 60_000) {
|
|
496
507
|
this.fail(`${at}.timeoutMs: expected a positive integer at most 3600000 (60 minutes)`);
|
package/src/worker-cli.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './wrapper-session.js';
|
|
32
32
|
import { wrapperEnvironment } from './wrapper-runtime.js';
|
|
33
33
|
import { applyStepEnvironment } from './step-env.js';
|
|
34
|
+
import { PLATFORM_AGENT_ENV_NAMES, SIGNAL_READ_TOKEN_ENV, agentStepEnvironment } from './step-secrets.js';
|
|
34
35
|
import { TAIL_CLOSE_TIMEOUT_MS, openTranscriptTail, type TranscriptTailWriter } from './transcript-tail.js';
|
|
35
36
|
import {
|
|
36
37
|
isRetryableSpawnError,
|
|
@@ -139,12 +140,33 @@ export async function runAgentCli(
|
|
|
139
140
|
stepTimeoutMs?: number,
|
|
140
141
|
/** A recorded CLI session to continue (`f.agent` `resume`); agent mode only. */
|
|
141
142
|
resume?: string,
|
|
143
|
+
/** Names of run secrets this step was granted by its journaled spec. */
|
|
144
|
+
secrets?: string[],
|
|
145
|
+
/** A featureless daemon retains its legacy child environment for plain runs. */
|
|
146
|
+
scopeSecrets = true,
|
|
142
147
|
): Promise<WorkerCliResult> {
|
|
143
148
|
signal?.throwIfAborted();
|
|
144
149
|
if (signal !== undefined && process.platform === 'win32') {
|
|
145
150
|
throw new Error('Lease-bound agent execution requires macOS or Linux process-group cancellation; Windows is unsupported.');
|
|
146
151
|
}
|
|
147
152
|
const kind = cliAdapterKind(cliIdentity ?? cli);
|
|
153
|
+
if (!scopeSecrets && secrets?.length) {
|
|
154
|
+
return { exit_code: null, stdout_tail: '',
|
|
155
|
+
stderr_tail: 'per-step secret grants require a daemon with step_secret_scoping support' };
|
|
156
|
+
}
|
|
157
|
+
let childEnvironment: NodeJS.ProcessEnv;
|
|
158
|
+
try {
|
|
159
|
+
childEnvironment = scopeSecrets
|
|
160
|
+
? agentStepEnvironment(processEnvironment, kind, secrets)
|
|
161
|
+
: { ...processEnvironment };
|
|
162
|
+
// The old-daemon compatibility path retains other legacy variables, but
|
|
163
|
+
// this CLI-only signal capability must never enter an agent subprocess.
|
|
164
|
+
delete childEnvironment[SIGNAL_READ_TOKEN_ENV];
|
|
165
|
+
delete childEnvironment[PLATFORM_AGENT_ENV_NAMES];
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return { exit_code: null, stdout_tail: '',
|
|
168
|
+
stderr_tail: error instanceof Error ? error.message : 'step secret grant is invalid' };
|
|
169
|
+
}
|
|
148
170
|
const effectiveModel = resolveCliModel(cliIdentity ?? cli, model);
|
|
149
171
|
const argv0 = cliInvocationArgv0(cli, cliIdentity);
|
|
150
172
|
|
|
@@ -206,7 +228,8 @@ export async function runAgentCli(
|
|
|
206
228
|
if (kind === 'relayflows-wrapper-v1') {
|
|
207
229
|
// The closed allowlist admits no ambient RELAYFLOW_* value; the four
|
|
208
230
|
// discovery names are set from this dispatch, exactly as for a direct spawn.
|
|
209
|
-
const wrapperEnv = wrapperEnvironment(
|
|
231
|
+
const wrapperEnv = wrapperEnvironment(childEnvironment);
|
|
232
|
+
for (const name of secrets ?? []) wrapperEnv[name] = childEnvironment[name];
|
|
210
233
|
applyStepEnvironment(wrapperEnv, sidechannel);
|
|
211
234
|
const session = await runWrapperSession(
|
|
212
235
|
cli,
|
|
@@ -225,7 +248,7 @@ export async function runAgentCli(
|
|
|
225
248
|
return requirePricedUsage(session.transport?.cause === 'timeout' ? { ...decoded, stdout_tail: '' } : decoded, effectiveModel);
|
|
226
249
|
}
|
|
227
250
|
|
|
228
|
-
const env: NodeJS.ProcessEnv = { ...
|
|
251
|
+
const env: NodeJS.ProcessEnv = { ...childEnvironment };
|
|
229
252
|
delete env[WAKE_CONTEXT_ENV];
|
|
230
253
|
delete env[MODEL_ENV];
|
|
231
254
|
// Where this attempt's journal is, so the agent can run `flows status` on
|
|
@@ -256,9 +279,13 @@ export async function runAgentCli(
|
|
|
256
279
|
const args = [...invocation.args];
|
|
257
280
|
args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
|
|
258
281
|
const completion = kind === 'claude' ? claudeResultOutcome : undefined;
|
|
282
|
+
// Retain the source values for output redaction even though the child
|
|
283
|
+
// receives only its filtered environment. A provider can echo a value from
|
|
284
|
+
// its prompt or fixture that was not granted as an environment variable.
|
|
285
|
+
const redactionEnvironment = { ...processEnvironment, ...env };
|
|
259
286
|
return requirePricedUsage(decodeProviderResult(await spawnInvocation(
|
|
260
|
-
cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion, argv0, kind,
|
|
261
|
-
), kind,
|
|
287
|
+
cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion, argv0, kind, redactionEnvironment,
|
|
288
|
+
), kind, redactionEnvironment), effectiveModel);
|
|
262
289
|
}
|
|
263
290
|
}
|
|
264
291
|
|
|
@@ -399,6 +426,7 @@ async function spawnInvocation(
|
|
|
399
426
|
completion?: (line: string) => { failed: boolean } | undefined,
|
|
400
427
|
argv0?: string,
|
|
401
428
|
kind?: CliAdapterKind,
|
|
429
|
+
redactionEnvironment: NodeJS.ProcessEnv = env,
|
|
402
430
|
): Promise<WorkerCliResult> {
|
|
403
431
|
const pendingInput: Buffer[] = [];
|
|
404
432
|
let acceptingDrive = true;
|
|
@@ -436,7 +464,7 @@ async function spawnInvocation(
|
|
|
436
464
|
};
|
|
437
465
|
try {
|
|
438
466
|
writer = sidechannel?.attempt === undefined ? undefined
|
|
439
|
-
: await openTranscriptWriter(transcriptPath(sidechannel, sidechannel.attempt),
|
|
467
|
+
: await openTranscriptWriter(transcriptPath(sidechannel, sidechannel.attempt), redactionEnvironment);
|
|
440
468
|
channel = sidechannel === undefined ? undefined : await openSidechannel({
|
|
441
469
|
...sidechannel,
|
|
442
470
|
onDrive() { driven = true; sidechannel.onDrive(); },
|
|
@@ -597,7 +625,7 @@ async function spawnInvocation(
|
|
|
597
625
|
const transport = transportEvidence({
|
|
598
626
|
phase: 'abort', cause: 'lease_lost', exitCode: null, signal: null,
|
|
599
627
|
stderr: 'Agent execution aborted: lease ownership lost.', retryable: false,
|
|
600
|
-
},
|
|
628
|
+
}, redactionEnvironment);
|
|
601
629
|
finishAfterStop(
|
|
602
630
|
{ exit_code: null, stdout_tail: '', stderr_tail: 'Agent execution aborted: lease ownership lost.', transport },
|
|
603
631
|
'kill',
|
|
@@ -635,11 +663,11 @@ async function spawnInvocation(
|
|
|
635
663
|
phase: 'result_exit_grace', cause: 'result_exit_timeout', exitCode, signal: null,
|
|
636
664
|
stderr: stderrText,
|
|
637
665
|
retryable: false,
|
|
638
|
-
},
|
|
666
|
+
}, redactionEnvironment);
|
|
639
667
|
finishAfterStop({
|
|
640
668
|
exit_code: exitCode,
|
|
641
669
|
stdout_tail: Buffer.concat(stdout).toString('utf8'),
|
|
642
|
-
stderr_tail: redactText(stderrText,
|
|
670
|
+
stderr_tail: redactText(stderrText, redactionEnvironment),
|
|
643
671
|
transport,
|
|
644
672
|
}, 'terminate');
|
|
645
673
|
}, RESULT_EXIT_GRACE_MS);
|
|
@@ -671,11 +699,11 @@ async function spawnInvocation(
|
|
|
671
699
|
const transport = transportEvidence({
|
|
672
700
|
phase: 'spawn', cause: child.pid === undefined ? 'spawn_error' : 'process_error',
|
|
673
701
|
exitCode: null, signal: null, stderr: error.message, errorCode: code, retryable,
|
|
674
|
-
},
|
|
702
|
+
}, redactionEnvironment);
|
|
675
703
|
finishOnChildExit({
|
|
676
704
|
exit_code: null,
|
|
677
705
|
stdout_tail: Buffer.concat(stdout).toString('utf8'),
|
|
678
|
-
stderr_tail: redactText(error.message,
|
|
706
|
+
stderr_tail: redactText(error.message, redactionEnvironment),
|
|
679
707
|
transport,
|
|
680
708
|
});
|
|
681
709
|
});
|
|
@@ -692,11 +720,11 @@ async function spawnInvocation(
|
|
|
692
720
|
const retryable = codexStdinLifecycle || signalName !== null || code === null;
|
|
693
721
|
const transport = transportEvidence({
|
|
694
722
|
phase: 'close', cause, exitCode: code, signal: signalName, stderr: stderrText, retryable,
|
|
695
|
-
},
|
|
723
|
+
}, redactionEnvironment);
|
|
696
724
|
finishOnChildExit({
|
|
697
725
|
exit_code: code,
|
|
698
726
|
stdout_tail: Buffer.concat(stdout).toString('utf8'),
|
|
699
|
-
stderr_tail: redactText(stderrText,
|
|
727
|
+
stderr_tail: redactText(stderrText, redactionEnvironment),
|
|
700
728
|
transport,
|
|
701
729
|
});
|
|
702
730
|
});
|
|
@@ -710,7 +738,7 @@ async function spawnInvocation(
|
|
|
710
738
|
phase: 'timeout', cause: 'timeout', exitCode: null, signal: null,
|
|
711
739
|
stderr: `${Buffer.concat(stderr).toString('utf8')}\n${timeoutMessage}`,
|
|
712
740
|
retryable: false,
|
|
713
|
-
},
|
|
741
|
+
}, redactionEnvironment);
|
|
714
742
|
finishAfterStop({
|
|
715
743
|
exit_code: null,
|
|
716
744
|
stdout_tail: Buffer.concat(stdout).toString('utf8'),
|