@jini-ai/daemon 0.6.8 → 0.6.10
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/CHANGELOG.md +148 -0
- package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
- package/dist/agent-executor/antigravity-permissions.js +64 -0
- package/dist/agent-executor/contracts.d.ts +37 -6
- package/dist/agent-executor/event-translation.d.ts +6 -1
- package/dist/agent-executor/event-translation.js +3 -1
- package/dist/agent-executor/events.d.ts +5 -0
- package/dist/agent-executor/events.js +143 -87
- package/dist/agent-executor/launch.d.ts +5 -3
- package/dist/agent-executor/launch.js +315 -217
- package/dist/agent-executor/mcp-bridge-probe.js +51 -35
- package/dist/agent-executor/mcp-bridge.d.ts +18 -8
- package/dist/agent-executor/mcp-bridge.js +105 -65
- package/dist/agent-executor/run-output.d.ts +20 -0
- package/dist/agent-executor/run-output.js +80 -0
- package/dist/attachment-content.js +18 -15
- package/dist/core/read-only-tools.d.ts +15 -0
- package/dist/core/read-only-tools.js +29 -1
- package/dist/core/tool-audit.d.ts +3 -1
- package/dist/core/tool-audit.js +35 -22
- package/dist/core/tool-gaps.d.ts +60 -0
- package/dist/core/tool-gaps.js +199 -0
- package/dist/delegated-result-correlation.d.ts +26 -0
- package/dist/delegated-result-correlation.js +68 -0
- package/dist/delegated-tool-bridge.d.ts +6 -14
- package/dist/delegated-tool-bridge.js +155 -155
- package/dist/event-log.d.ts +45 -2
- package/dist/event-log.js +83 -4
- package/dist/http/attachments.d.ts +26 -1
- package/dist/http/attachments.js +129 -65
- package/dist/http/connectors.d.ts +1 -32
- package/dist/http/connectors.js +13 -68
- package/dist/http/delegated-tools.d.ts +8 -1
- package/dist/http/delegated-tools.js +30 -30
- package/dist/http/host-tools.d.ts +1 -0
- package/dist/http/host-tools.js +64 -59
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/media.js +48 -69
- package/dist/http/memory.js +45 -36
- package/dist/http/model-proxy-fields.d.ts +23 -0
- package/dist/http/model-proxy-fields.js +29 -0
- package/dist/http/model-proxy.js +193 -395
- package/dist/http/research-tavily.adapter.js +72 -49
- package/dist/http/research.js +33 -23
- package/dist/http/resource-request.d.ts +37 -0
- package/dist/http/resource-request.js +59 -0
- package/dist/http/routines.js +49 -90
- package/dist/http/runs.d.ts +7 -5
- package/dist/http/runs.js +21 -7
- package/dist/http/terminals.js +23 -48
- package/dist/http/tool-catalog.d.ts +5 -0
- package/dist/http/xai.d.ts +11 -1
- package/dist/http/xai.js +125 -124
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/legacy-data-migration.js +41 -24
- package/dist/run/core/retry.js +51 -35
- package/dist/run/diagnostics/diagnostics.js +5 -17
- package/dist/run-credentials.js +49 -27
- package/dist/run-lifecycle.d.ts +31 -2
- package/dist/run-lifecycle.js +49 -42
- package/dist/store/agent-sessions/sqlite.d.ts +6 -0
- package/dist/store/agent-sessions/sqlite.js +6 -4
- package/dist/surface-exchanges.js +2 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.js +26 -33
- package/dist/tool-audit.d.ts +1 -0
- package/dist/tool-audit.js +1 -0
- package/dist/tool-executor.d.ts +24 -3
- package/dist/tool-executor.js +276 -139
- package/dist/tool-recovery/recovery.js +64 -35
- package/dist/tool-recovery/stack.d.ts +2 -1
- package/dist/tool-recovery/stack.js +1 -1
- package/dist/tool-result-diagnostics.d.ts +27 -0
- package/dist/tool-result-diagnostics.js +77 -0
- package/dist/tool-result-media.d.ts +27 -14
- package/dist/tool-result-media.js +51 -38
- package/package.json +14 -14
|
@@ -139,7 +139,9 @@ function waitForSpawnOrError(child) {
|
|
|
139
139
|
*/
|
|
140
140
|
function resolveAgentRuntimeDeps(options) {
|
|
141
141
|
const runtimeOptions = options.runtimeState ? { state: options.runtimeState } : {};
|
|
142
|
-
|
|
142
|
+
// `createAgentExecutor` always resolves both before calling here (host env falls back to the
|
|
143
|
+
// process env, the pi shutdown grace to its env override or 5000 ms), so neither is optional.
|
|
144
|
+
const envOptions = { env: options.hostEnv };
|
|
143
145
|
const lookupDef = options.getAgentDef ?? getAgentDef;
|
|
144
146
|
return {
|
|
145
147
|
getAgentDef: (input) => {
|
|
@@ -154,7 +156,7 @@ function resolveAgentRuntimeDeps(options) {
|
|
|
154
156
|
applyAgentLaunchEnv: options.applyAgentLaunchEnv ?? ((input, optional = {}) => applyAgentLaunchEnv(input, { appendPathDirs: userToolchainBinDirs({}, { ...runtimeOptions, ...envOptions }), ...optional })),
|
|
155
157
|
attachAcpSession: options.attachAcpSession ?? attachAcpSession,
|
|
156
158
|
attachPiRpcSession: (required, optional = {}) => (options.attachPiRpcSession ?? attachPiRpcSession)(required, {
|
|
157
|
-
...optional,
|
|
159
|
+
...optional, gracefulShutdownMs: options.piGracefulShutdownMs,
|
|
158
160
|
}),
|
|
159
161
|
preparePromptFileForAgent: options.preparePromptFileForAgent ?? preparePromptFileForAgent,
|
|
160
162
|
prepareAgentLogFile: options.prepareAgentLogFile ?? prepareAgentLogFile,
|
|
@@ -283,6 +285,16 @@ export function computeChildEnv({ spawnEnv, mcpBridge }, { codexHomeDir, systemP
|
|
|
283
285
|
const claudeConfigDirApplied = claudeConfigDir === undefined ? codexHomeApplied : { ...codexHomeApplied, CLAUDE_CONFIG_DIR: claudeConfigDir };
|
|
284
286
|
return systemPromptEnvOverrides === undefined ? claudeConfigDirApplied : { ...claudeConfigDirApplied, ...systemPromptEnvOverrides };
|
|
285
287
|
}
|
|
288
|
+
/** Session fields use the same nonempty-string rule as the CLI buildArgs boundary. */
|
|
289
|
+
function sessionRuntimeContext({ resumeSessionId, newSessionId }, _options = {}) {
|
|
290
|
+
// Matches claude.ts buildArgs' own `typeof x === 'string' && x` truthiness check, so an empty
|
|
291
|
+
// string or explicit `null` (no resume target yet) is treated as absent here too, rather than
|
|
292
|
+
// manufacturing a context that carries a session field the def would ignore anyway.
|
|
293
|
+
return {
|
|
294
|
+
...(typeof resumeSessionId === 'string' && resumeSessionId.length > 0 ? { resumeSessionId } : {}),
|
|
295
|
+
...(typeof newSessionId === 'string' && newSessionId.length > 0 ? { newSessionId } : {}),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
286
298
|
/**
|
|
287
299
|
* Phase 6b: the `RuntimeContext` `buildArgs` receives — `undefined` unless a file, bridge path, or
|
|
288
300
|
* session id was staged. Pure.
|
|
@@ -295,37 +307,42 @@ export function computeChildEnv({ spawnEnv, mcpBridge }, { codexHomeDir, systemP
|
|
|
295
307
|
* case for a resumed turn.
|
|
296
308
|
*/
|
|
297
309
|
export function computeRuntimeContext({ preparedPromptFile, preparedLogFile, mcpBridge }, { resumeSessionId, newSessionId } = {}) {
|
|
298
|
-
|
|
299
|
-
// string or explicit `null` (no resume target yet) is treated as absent here too, rather than
|
|
300
|
-
// manufacturing a context that carries a session field the def would ignore anyway.
|
|
301
|
-
const hasResumeSessionId = typeof resumeSessionId === 'string' && resumeSessionId.length > 0;
|
|
302
|
-
const hasNewSessionId = typeof newSessionId === 'string' && newSessionId.length > 0;
|
|
303
|
-
if (!preparedPromptFile
|
|
304
|
-
&& !preparedLogFile
|
|
305
|
-
&& mcpBridge?.kind !== 'claude-mcp-json'
|
|
306
|
-
&& !hasResumeSessionId
|
|
307
|
-
&& !hasNewSessionId) {
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
return {
|
|
310
|
+
const context = {
|
|
311
311
|
...(preparedPromptFile ? { promptFilePath: preparedPromptFile.path } : {}),
|
|
312
312
|
...(preparedLogFile ? { agentLogFilePath: preparedLogFile.path } : {}),
|
|
313
313
|
// Safe to pass before the file exists: `writeMcpJsonForRun` runs after buildArgs but still
|
|
314
314
|
// before spawn, so the path is real by the time the child process starts.
|
|
315
315
|
...(mcpBridge?.kind === 'claude-mcp-json' ? { mcpJsonPath: mcpBridge.mcpJsonPath } : {}),
|
|
316
|
-
...(
|
|
317
|
-
...(hasNewSessionId ? { newSessionId } : {}),
|
|
316
|
+
...sessionRuntimeContext({ resumeSessionId, newSessionId }, {}),
|
|
318
317
|
};
|
|
318
|
+
return Object.keys(context).length === 0 ? undefined : context;
|
|
319
319
|
}
|
|
320
320
|
/**
|
|
321
321
|
* Phase 8: the host's `PromptAugmenter.systemOverlay()` result, if configured — see
|
|
322
322
|
* `CreateAgentExecutorOptions.promptAugmenter`'s doc for `turnIndex`'s coarse 0/1 proxy.
|
|
323
323
|
*/
|
|
324
|
-
function computeSystemPromptOverlay(promptAugmenter, agentId
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
324
|
+
function computeSystemPromptOverlay(promptAugmenter, agentId) {
|
|
325
|
+
// Always turn 0: the only prior-turn signal, `RuntimeContext.hasPriorAssistantTurn`, is never
|
|
326
|
+
// populated by `computeRuntimeContext` (it carries staged-file, MCP and session-id fields only).
|
|
327
|
+
return promptAugmenter?.systemOverlay?.({ agentId, turnIndex: 0 });
|
|
328
|
+
}
|
|
329
|
+
/** Native policy forwarding also enforces parity's permission and tool-access prerequisites. */
|
|
330
|
+
function nativeBuildOptions({ input, permissionMode }, _options = {}) {
|
|
331
|
+
return {
|
|
332
|
+
...(input.nativeToolAccess !== undefined ? { nativeToolAccess: input.nativeToolAccess } : {}),
|
|
333
|
+
...(input.nativeCapabilities ? { nativeCapabilities: input.nativeCapabilities } : {}),
|
|
334
|
+
...(input.cliParity === true && permissionMode === 'bypass' && input.nativeToolAccess !== false ? { cliParity: true } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** Preserve explicit empty restrictions and settings: they have meaning to the native CLI. */
|
|
338
|
+
function settingsBuildOptions({ input }, _options = {}) {
|
|
339
|
+
return {
|
|
340
|
+
...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
|
|
341
|
+
...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
|
|
342
|
+
...(input.settingSources !== undefined ? { settingSources: input.settingSources } : {}),
|
|
343
|
+
...(input.settings !== undefined ? { settings: input.settings } : {}),
|
|
344
|
+
...(input.chromeIntegration !== undefined ? { chromeIntegration: input.chromeIntegration } : {}),
|
|
345
|
+
};
|
|
329
346
|
}
|
|
330
347
|
/** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
|
|
331
348
|
* that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
|
|
@@ -339,13 +356,30 @@ export function buildAgentBuildArgsOptions({ input, systemPromptOverlay }) {
|
|
|
339
356
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
340
357
|
...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
|
|
341
358
|
permissionMode,
|
|
342
|
-
...(
|
|
343
|
-
...(
|
|
344
|
-
...(input.settingSources !== undefined ? { settingSources: input.settingSources } : {}),
|
|
345
|
-
...(input.settings !== undefined ? { settings: input.settings } : {}),
|
|
359
|
+
...nativeBuildOptions({ input, permissionMode }, {}),
|
|
360
|
+
...settingsBuildOptions({ input }, {}),
|
|
346
361
|
...(hasOverlay ? { systemPromptOverlay } : {}),
|
|
347
362
|
};
|
|
348
|
-
return
|
|
363
|
+
return options;
|
|
364
|
+
}
|
|
365
|
+
function hasSystemPromptOverlay(args, _options = {}) {
|
|
366
|
+
return typeof args.overlay === 'string' && args.overlay.length > 0;
|
|
367
|
+
}
|
|
368
|
+
function appendFlagOverlay({ defId, systemPromptDelivery, overlay, prompt }, { state } = {}) {
|
|
369
|
+
const capabilityKey = systemPromptDelivery.capabilityKey;
|
|
370
|
+
// `!== false`, not a truthiness check: mirrors `claude.ts`'s own pre-existing
|
|
371
|
+
// `agentCapabilities.get('claude') || {}` gate exactly (moved here, not changed) — an
|
|
372
|
+
// undetected/never-probed capability defaults to allowed, and only an EXPLICIT `false` (the
|
|
373
|
+
// `--help` probe ran and did not find the flag) withholds it. `capabilityKey === undefined`
|
|
374
|
+
// (e.g. `pi`'s existing `--append-system-prompt`, trusted unconditionally) always passes, same
|
|
375
|
+
// as an absent key.
|
|
376
|
+
const capabilityOk = capabilityKey === undefined || (state?.capabilities ?? agentCapabilities).get(defId)?.[capabilityKey] !== false;
|
|
377
|
+
return { promptPrefix: '', prompt, extraArgs: capabilityOk ? [systemPromptDelivery.flag, overlay] : [], envOverrides: {} };
|
|
378
|
+
}
|
|
379
|
+
function isContinuingOverlaySession({ resumesSessionViaCli, resumesSessionViaAcpLoad, resumeSessionId }, _options = {}) {
|
|
380
|
+
return (resumesSessionViaCli === true || resumesSessionViaAcpLoad === true) &&
|
|
381
|
+
typeof resumeSessionId === 'string' &&
|
|
382
|
+
resumeSessionId.length > 0;
|
|
349
383
|
}
|
|
350
384
|
/**
|
|
351
385
|
* **The single dispatch point from a computed system-prompt overlay to its delivery mechanism** —
|
|
@@ -391,20 +425,13 @@ export function buildAgentBuildArgsOptions({ input, systemPromptOverlay }) {
|
|
|
391
425
|
* @overallScore 100/100
|
|
392
426
|
*/
|
|
393
427
|
export function resolveSystemPromptOverlayDelivery(input, { state } = {}) {
|
|
394
|
-
const { defId, systemPromptDelivery, resumesSessionViaCli, resumesSessionViaAcpLoad,
|
|
395
|
-
if (
|
|
428
|
+
const { defId, systemPromptDelivery, resumesSessionViaCli, resumesSessionViaAcpLoad, prompt, resumeSessionId } = input;
|
|
429
|
+
if (!hasSystemPromptOverlay(input, {})) {
|
|
396
430
|
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
397
431
|
}
|
|
432
|
+
const overlay = input.overlay;
|
|
398
433
|
if (systemPromptDelivery?.strategy === 'append-flag') {
|
|
399
|
-
|
|
400
|
-
// `!== false`, not a truthiness check: mirrors `claude.ts`'s own pre-existing
|
|
401
|
-
// `agentCapabilities.get('claude') || {}` gate exactly (moved here, not changed) — an
|
|
402
|
-
// undetected/never-probed capability defaults to allowed, and only an EXPLICIT `false` (the
|
|
403
|
-
// `--help` probe ran and did not find the flag) withholds it. `capabilityKey === undefined`
|
|
404
|
-
// (e.g. `pi`'s existing `--append-system-prompt`, trusted unconditionally) always passes, same
|
|
405
|
-
// as an absent key.
|
|
406
|
-
const capabilityOk = capabilityKey === undefined || (state?.capabilities ?? agentCapabilities).get(defId)?.[capabilityKey] !== false;
|
|
407
|
-
return { promptPrefix: '', prompt, extraArgs: capabilityOk ? [systemPromptDelivery.flag, overlay] : [], envOverrides: {} };
|
|
434
|
+
return appendFlagOverlay({ defId, systemPromptDelivery, overlay, prompt }, { state });
|
|
408
435
|
}
|
|
409
436
|
if (systemPromptDelivery?.strategy === 'env-var') {
|
|
410
437
|
// No capability gate, unlike `'append-flag'`: an unrecognized env var is inert to a CLI (it
|
|
@@ -425,10 +452,7 @@ export function resolveSystemPromptOverlayDelivery(input, { state } = {}) {
|
|
|
425
452
|
// `imageDelivery`'s doc calls out for its own native-vs-fallback split.
|
|
426
453
|
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
427
454
|
}
|
|
428
|
-
|
|
429
|
-
typeof resumeSessionId === 'string' &&
|
|
430
|
-
resumeSessionId.length > 0;
|
|
431
|
-
if (isContinuingExistingSession) {
|
|
455
|
+
if (isContinuingOverlaySession({ resumesSessionViaCli, resumesSessionViaAcpLoad, resumeSessionId }, {})) {
|
|
432
456
|
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
433
457
|
}
|
|
434
458
|
// KNOWN TRADE-OFF, deliberate: for a resume-capable def with no `'append-flag'`/`'env-var'`
|
|
@@ -465,7 +489,7 @@ export async function buildRunArgs({ input, deps }) {
|
|
|
465
489
|
const options = buildAgentBuildArgsOptions({ input: input.runInput, systemPromptOverlay: input.systemPromptOverlay });
|
|
466
490
|
const args = input.def.buildArgs({ prompt: delivery.prompt, imagePaths: [...(input.imagePaths ?? [])] }, {
|
|
467
491
|
...(input.imageDelivery.extraAllowedDirs === undefined ? {} : { extraAllowedDirs: [...input.imageDelivery.extraAllowedDirs] }),
|
|
468
|
-
|
|
492
|
+
options,
|
|
469
493
|
...(input.runtimeContext === undefined ? {} : { runtimeContext: input.runtimeContext }),
|
|
470
494
|
});
|
|
471
495
|
// `'append-flag'` delivery's extra argv (empty for every other def/strategy) is appended after
|
|
@@ -497,6 +521,7 @@ export async function buildRunArgs({ input, deps }) {
|
|
|
497
521
|
* a defensive extra.
|
|
498
522
|
* @param input.def - Only used for its `id`, in the failure message, and its `systemPromptDelivery` declaration.
|
|
499
523
|
* @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result for this run.
|
|
524
|
+
* @param deps.mkdtemp - Creates the run-scoped directory from a prefix; default a fresh `mkdtemp` under `os.tmpdir()`.
|
|
500
525
|
* @complexity O(1) plus one directory creation and one file write.
|
|
501
526
|
* @overallScore 100/100
|
|
502
527
|
*/
|
|
@@ -508,7 +533,7 @@ export async function prepareSystemPromptOverlayFileIfNeeded({ input, deps }) {
|
|
|
508
533
|
}
|
|
509
534
|
try {
|
|
510
535
|
const safeRunId = input.runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
|
|
511
|
-
const dir = await
|
|
536
|
+
const dir = await (deps.mkdtemp ?? defaultMkdtempInTmpdir)(`jini-system-prompt-overlay-${safeRunId}-`);
|
|
512
537
|
const filePath = join(dir, 'overlay.md');
|
|
513
538
|
await fsPromises.writeFile(filePath, input.overlay, { encoding: 'utf8', mode: 0o600 });
|
|
514
539
|
return {
|
|
@@ -523,13 +548,18 @@ export async function prepareSystemPromptOverlayFileIfNeeded({ input, deps }) {
|
|
|
523
548
|
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a system-prompt overlay file for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
524
549
|
}
|
|
525
550
|
}
|
|
551
|
+
/** A fresh, randomly named directory under `os.tmpdir()`: the real staging location. */
|
|
552
|
+
function defaultMkdtempInTmpdir(prefix) {
|
|
553
|
+
return fsPromises.mkdtemp(join(tmpdir(), prefix));
|
|
554
|
+
}
|
|
526
555
|
/** Phase 11: post-`buildArgs` guard for argv-bound defs whose resolved binary is a Windows shim/.exe — a no-op off-Windows and for non-argv-bound defs. */
|
|
527
556
|
export async function guardWindowsCommandLineBudget({ input, deps }) {
|
|
528
557
|
const windowsBudgetError = checkWindowsCmdShimCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args }) ??
|
|
529
558
|
checkWindowsDirectExeCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args });
|
|
530
559
|
if (windowsBudgetError) {
|
|
531
560
|
await deps.releaseStagedResources();
|
|
532
|
-
|
|
561
|
+
// `failBeforeSpawn` always rejects (`Promise<never>`); returning it propagates that rejection.
|
|
562
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
|
|
533
563
|
}
|
|
534
564
|
}
|
|
535
565
|
/**
|
|
@@ -579,9 +609,40 @@ export async function confirmChildSpawned({ input, deps }) {
|
|
|
579
609
|
}
|
|
580
610
|
catch (err) {
|
|
581
611
|
await deps.releaseStagedResources();
|
|
582
|
-
|
|
612
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
|
|
583
613
|
}
|
|
584
614
|
}
|
|
615
|
+
/** Only include declared capability restrictions; absence has a distinct downstream contract. */
|
|
616
|
+
function nativeCapabilityInput({ input }, _options = {}) {
|
|
617
|
+
return { ...(input.nativeCapabilities !== undefined ? { nativeCapabilities: input.nativeCapabilities } : {}) };
|
|
618
|
+
}
|
|
619
|
+
/** Correlation projects retained stdout, so exact-byte journals need a separate blob writer. */
|
|
620
|
+
function validateStdoutJournalOptions(options, _options = {}) {
|
|
621
|
+
if (options.delegatedResultCorrelation && options.journal && !options.stdoutJournalBlobs) {
|
|
622
|
+
throw new Error('delegated result correlation with a byte journal requires stdoutJournalBlobs');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Model discovery consumes the exact spawn environment and caller-supplied settings. */
|
|
626
|
+
function launchModelContext({ input, launch, spawnEnv }, _options = {}) {
|
|
627
|
+
return {
|
|
628
|
+
executable: launch.launchPath, cwd: input.cwd, env: spawnEnv,
|
|
629
|
+
...(input.model && input.model !== 'default' ? { model: input.model } : {}),
|
|
630
|
+
...(input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}),
|
|
631
|
+
...(input.settingSources ? { settingSources: input.settingSources } : {}),
|
|
632
|
+
...(input.settings ? { settings: input.settings } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/** Paths are available only after staging; retain the strategy gate for instructions files. */
|
|
636
|
+
function stagedChildEnvOptions({ def, preparedCodexHome, preparedClaudeConfigDir, preparedSystemPromptOverlayFile, systemPromptEnvOverrides }, _options = {}) {
|
|
637
|
+
return {
|
|
638
|
+
...(preparedCodexHome === null ? {} : { codexHomeDir: preparedCodexHome.path }),
|
|
639
|
+
systemPromptEnvOverrides,
|
|
640
|
+
...(preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
|
|
641
|
+
? { stagedInstructionsFile: { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path } }
|
|
642
|
+
: {}),
|
|
643
|
+
...(preparedClaudeConfigDir === null ? {} : { claudeConfigDir: preparedClaudeConfigDir.path }),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
585
646
|
/**
|
|
586
647
|
* Creates the `AgentExecutor` reference implementation: an in-process
|
|
587
648
|
* `RunLifecycle` driver over real (by default) `@jini-ai/agent-runtime`
|
|
@@ -609,6 +670,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
609
670
|
const { getAgentDef: getAgentDefFn, resolveAgentLaunch: resolveAgentLaunchFn, ensureAgentCapabilities: ensureAgentCapabilitiesFn, applyAgentLaunchEnv: applyAgentLaunchEnvFn, attachAcpSession: attachAcpSessionFn, attachPiRpcSession: attachPiRpcSessionFn, preparePromptFileForAgent: preparePromptFileForAgentFn, prepareAgentLogFile: prepareAgentLogFileFn, } = resolveAgentRuntimeDeps({ ...options, hostEnv });
|
|
610
671
|
const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
|
|
611
672
|
const { onCleanupFailure: onCleanupFailureFn, bufferedStdoutMaxBytes } = resolveMiscExecutorDeps(options);
|
|
673
|
+
validateStdoutJournalOptions(options);
|
|
612
674
|
const journal = options.journal;
|
|
613
675
|
const continuation = options.continuation;
|
|
614
676
|
const classifyFailure = options.classifyFailure;
|
|
@@ -647,38 +709,29 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
647
709
|
*/
|
|
648
710
|
const liveRunInputs = new Map();
|
|
649
711
|
/** `AgentExecutor.sendUserMessage()` — see that interface method's own doc. @complexity O(1). */
|
|
650
|
-
async function sendUserMessage({ runId, text }) {
|
|
712
|
+
async function sendUserMessage({ runId, text }, { imageContents } = {}) {
|
|
651
713
|
const deliver = liveRunInputs.get(runId);
|
|
652
|
-
return deliver ? deliver(text) : 'not-running';
|
|
714
|
+
return deliver ? deliver(text, imageContents) : 'not-running';
|
|
653
715
|
}
|
|
654
|
-
/**
|
|
655
|
-
|
|
656
|
-
* public contract. Implementation note on shape: every guard below
|
|
657
|
-
* returns `failBeforeSpawn(...)` directly (a `Promise<never>`, valid
|
|
658
|
-
* wherever `Promise<void>` is expected) rather than `await`-then-`return`,
|
|
659
|
-
* so each failure path reads as a single, obviously-terminal statement.
|
|
660
|
-
* @param input - `{runId, agentId, prompt, cwd, model?, reasoning?, imagePaths?, extraAllowedDirs?, uploadRoot?, env?}` — `runId` must already be `lifecycle.start()`-ed.
|
|
661
|
-
* @throws {@link AgentExecutorError} — see index.ts module doc's Invariant; never a bare `Error`.
|
|
662
|
-
* @complexity O(1) setup (registry lookup, launch resolution, one spawn call); steady-state cost thereafter belongs to {@link wireChildLifecycle}.
|
|
663
|
-
* @overallScore 100/100
|
|
664
|
-
*/
|
|
665
|
-
async function run(requiredArgs, optionalArgs = {}) {
|
|
716
|
+
/** Validates launch input and selects the definition synchronously before yielding. */
|
|
717
|
+
async function prepareRunContext({ requiredArgs, optionalArgs }, _options = {}) {
|
|
666
718
|
const input = { ...requiredArgs, ...optionalArgs, permissionMode: optionalArgs.permissionMode ?? 'restricted' };
|
|
667
719
|
if (input.permissionMode !== 'restricted' && input.permissionMode !== 'bypass') {
|
|
668
720
|
return failBeforeSpawn({ runId: input.runId, code: 'AGENT_PERMISSION_MODE_INVALID', message: 'AgentExecutor: permissionMode must be restricted or bypass' });
|
|
669
721
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
722
|
+
// Lookup runs before the first await so a subsequent registry replacement cannot change this run's definition.
|
|
723
|
+
const resolved = await resolveDefAndStreamFormat({ input, deps: { getAgentDef: getAgentDefFn, failBeforeSpawn } });
|
|
724
|
+
return { input, ...resolved };
|
|
725
|
+
}
|
|
726
|
+
async function enforceNativeToolPolicy({ input, def }, _options = {}) {
|
|
727
|
+
// Never silently ignore a host's locked native-tool policy on another CLI.
|
|
728
|
+
// Parity cannot override an explicit lock or a restricted run.
|
|
729
|
+
input.cliParity = input.cliParity === true && input.permissionMode === 'bypass' && input.nativeToolAccess !== false;
|
|
730
|
+
if ((input.nativeToolAccess !== undefined && !input.cliParity || Object.values(input.nativeCapabilities ?? {}).some(value => value === false)) && def.supportsNativeToolAccess !== true) {
|
|
731
|
+
return failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED', message: 'This CLI has no native-tool policy and audit adapter. Choose a CLI with native-tool policy support.' });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function resolveRunOverlay({ input, def, imageDelivery }, _options = {}) {
|
|
682
735
|
// Phase 8, hoisted deliberately above EVERY step that writes or sends the prompt. Four
|
|
683
736
|
// different channels carry a prompt in this driver, and they do not all run at the same point:
|
|
684
737
|
// the `promptViaFile` staging below writes its file BEFORE `buildArgs`, `buildArgs` itself runs
|
|
@@ -687,21 +740,18 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
687
740
|
// inside `buildRunArgs`) could only ever reach `buildArgs`, so the 17 defs that discard that
|
|
688
741
|
// argument, plus grok-build's staged file, silently received no overlay at all.
|
|
689
742
|
//
|
|
690
|
-
// `computeSystemPromptOverlay`
|
|
691
|
-
//
|
|
743
|
+
// `computeSystemPromptOverlay` reads no `RuntimeContext` at all: the one field it could use,
|
|
744
|
+
// `hasPriorAssistantTurn`, is never populated by `computeRuntimeContext` from any of the
|
|
692
745
|
// staged-file/MCP inputs added to the fuller context built further down, so nothing between
|
|
693
|
-
// here and there can change the overlay this returns.
|
|
694
|
-
//
|
|
746
|
+
// here and there can change the overlay this returns. The pre-staging context below is kept
|
|
747
|
+
// for the overlay delivery's `resumeSessionId` only.
|
|
695
748
|
//
|
|
696
749
|
// `turnIndex` is a coarse 0/1 proxy (no exact turn counter exists on this driver) — sufficient
|
|
697
750
|
// because every `PromptAugmenter.systemOverlay()` implementation this seam has today wants the
|
|
698
751
|
// same overlay on every turn, not a first-turn-only one; a caller that needs finer-grained turn
|
|
699
752
|
// numbering can track it itself and ignore this arg.
|
|
700
|
-
const preStagingRuntimeContext = computeRuntimeContext({ preparedPromptFile: null, preparedLogFile: null, mcpBridge: null }, {
|
|
701
|
-
|
|
702
|
-
...(input.newSessionId === undefined ? {} : { newSessionId: input.newSessionId }),
|
|
703
|
-
});
|
|
704
|
-
const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, preStagingRuntimeContext);
|
|
753
|
+
const preStagingRuntimeContext = computeRuntimeContext({ preparedPromptFile: null, preparedLogFile: null, mcpBridge: null }, sessionRuntimeContext(input, {}));
|
|
754
|
+
const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id);
|
|
705
755
|
const overlayDelivery = resolveSystemPromptOverlayDelivery({
|
|
706
756
|
defId: def.id,
|
|
707
757
|
systemPromptDelivery: def.systemPromptDelivery,
|
|
@@ -711,30 +761,187 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
711
761
|
prompt: imageDelivery.prompt,
|
|
712
762
|
resumeSessionId: preStagingRuntimeContext?.resumeSessionId,
|
|
713
763
|
}, options.runtimeState ? { state: options.runtimeState } : {});
|
|
764
|
+
return { systemPromptOverlay, overlayDelivery };
|
|
765
|
+
}
|
|
766
|
+
async function resolveRunLaunch({ input, def }, _options = {}) {
|
|
714
767
|
// The def's own declared env (e.g. claude's `ENABLE_TOOL_SEARCH=false`) is a default the run's
|
|
715
768
|
// env can override. Detection already spawns with it (`detection.ts`); without this the run
|
|
716
769
|
// spawn silently dropped it. Declared by the def itself, so it never widens SEC-001's allowlist.
|
|
717
|
-
const
|
|
770
|
+
const defaults = { ...(def.env ?? {}) };
|
|
771
|
+
// Eager loading was cheap only for the isolated bridge. Let the user's CLI decide in parity.
|
|
772
|
+
if (input.cliParity)
|
|
773
|
+
delete defaults.ENABLE_TOOL_SEARCH;
|
|
774
|
+
const resolvedEnv = { ...defaults, ...resolveRunEnv({ input: input, hostEnv }) };
|
|
718
775
|
const launch = await resolveLaunch({ input: { runId: input.runId, def, resolvedEnv }, deps: { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn } });
|
|
719
776
|
const spawnEnv = applyAgentLaunchEnvFn({ env: { ...resolvedEnv }, launch });
|
|
720
|
-
|
|
721
|
-
|
|
777
|
+
return { launch, spawnEnv };
|
|
778
|
+
}
|
|
779
|
+
async function selectRunModel({ input, def, launch, spawnEnv }, _options = {}) {
|
|
780
|
+
// Locked runs pin the picker's concrete ID before any prompt is sent. Trusted parity keeps
|
|
781
|
+
// the CLI's own configured default and effort, without requiring discovery to succeed.
|
|
782
|
+
if (input.cliParity && input.model === 'default')
|
|
783
|
+
delete input.model;
|
|
722
784
|
let modelCatalog;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
modelCatalog = selection.catalog;
|
|
734
|
-
}
|
|
735
|
-
catch {
|
|
736
|
-
return failBeforeSpawn({ runId: input.runId, code: 'AGENT_MODEL_UNRESOLVED', message: 'AgentExecutor: pick a concrete model; the starting model could not be resolved for this launch.' });
|
|
785
|
+
if (!input.cliParity) {
|
|
786
|
+
try {
|
|
787
|
+
const selectModel = options.resolveModelForLaunch ?? ((input) => resolveModelForLaunch(input, options.runtimeState ? { deps: getRuntimeModelDiscoveryDeps({ state: options.runtimeState }) } : {}));
|
|
788
|
+
const selection = await selectModel({ def, context: launchModelContext({ input, launch, spawnEnv }, {}) });
|
|
789
|
+
input.model = selection.model;
|
|
790
|
+
modelCatalog = selection.catalog;
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
return failBeforeSpawn({ runId: input.runId, code: 'AGENT_MODEL_UNRESOLVED', message: 'AgentExecutor: pick a concrete model; the starting model could not be resolved for this launch.' });
|
|
794
|
+
}
|
|
737
795
|
}
|
|
796
|
+
return modelCatalog;
|
|
797
|
+
}
|
|
798
|
+
function bridgeProbeEntryForRun({ mcpBridge }, _options = {}) {
|
|
799
|
+
// probeMcpBridge itself strips the run credential and swaps in a diagnostic run id.
|
|
800
|
+
return mcpBridge?.kind === 'claude-mcp-json' && mcpJsonInjection?.probeOnFailure !== false
|
|
801
|
+
? mcpBridge.serverEntry
|
|
802
|
+
: undefined;
|
|
803
|
+
}
|
|
804
|
+
function wireRunInput({ input, def, streamFormat, child, modelCatalog, cleanupStagedFiles, writtenMcpJsonPath, mcpBridge }, _options = {}) {
|
|
805
|
+
const formatForStdin = { streamFormat };
|
|
806
|
+
const stdinHandle = isStdinDrivenFormat(formatForStdin)
|
|
807
|
+
? wireChildLifecycle({
|
|
808
|
+
...(input.model ? { startingModel: input.model } : {}),
|
|
809
|
+
...(modelCatalog ? { modelCatalog } : {}),
|
|
810
|
+
runId: input.runId,
|
|
811
|
+
def,
|
|
812
|
+
streamFormat: formatForStdin.streamFormat,
|
|
813
|
+
child,
|
|
814
|
+
lifecycle,
|
|
815
|
+
listProcessSnapshots: listProcessSnapshotsFn,
|
|
816
|
+
collectProcessTreePids: collectProcessTreePidsFn,
|
|
817
|
+
stopProcesses: stopProcessesFn,
|
|
818
|
+
onCleanupFailure: onCleanupFailureFn,
|
|
819
|
+
cleanupStagedFiles,
|
|
820
|
+
journal,
|
|
821
|
+
...(options.delegatedResultCorrelation ? { delegatedResultCorrelation: options.delegatedResultCorrelation } : {}),
|
|
822
|
+
...(options.stdoutJournalBlobs ? { stdoutJournalBlobs: options.stdoutJournalBlobs } : {}),
|
|
823
|
+
continuation,
|
|
824
|
+
classifyFailure,
|
|
825
|
+
bufferedStdoutMaxBytes,
|
|
826
|
+
expectsJiniBridge: writtenMcpJsonPath !== undefined,
|
|
827
|
+
bridgeProbeEntry: bridgeProbeEntryForRun({ mcpBridge }, {}),
|
|
828
|
+
probeMcpBridge: options.probeMcpBridge ?? probeMcpBridge,
|
|
829
|
+
})
|
|
830
|
+
: null;
|
|
831
|
+
// Registered with the lifecycle wiring above, before the spawn-confirmation await, so a child
|
|
832
|
+
// that closes immediately never leaves a stale entry behind.
|
|
833
|
+
const runId = input.runId;
|
|
834
|
+
liveRunInputs.set(runId, stdinHandle ? stdinHandle.sendUserMessage : async () => 'unsupported');
|
|
835
|
+
child.once('close', () => liveRunInputs.delete(runId));
|
|
836
|
+
return stdinHandle;
|
|
837
|
+
}
|
|
838
|
+
async function dispatchAcpPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, mcpBridge, overlayDelivery }, _options = {}) {
|
|
839
|
+
await runAcpDispatch({ input: {
|
|
840
|
+
runId: input.runId,
|
|
841
|
+
agentId: def.id,
|
|
842
|
+
child,
|
|
843
|
+
// `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
|
|
844
|
+
// this call site deliberately sends the raw input prompt rather than the image-delivery
|
|
845
|
+
// rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
|
|
846
|
+
// images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
|
|
847
|
+
// is the part of the overlay decision that applies to whatever prompt text a transport was
|
|
848
|
+
// already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
|
|
849
|
+
// sending byte-identical text and never receive the overlay twice.
|
|
850
|
+
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
851
|
+
cwd: input.cwd,
|
|
852
|
+
permissionMode: input.permissionMode,
|
|
853
|
+
cliParity: input.cliParity,
|
|
854
|
+
model: input.model,
|
|
855
|
+
...(modelCatalog ? { modelCatalog } : {}),
|
|
856
|
+
imagePaths: input.imagePaths ?? [],
|
|
857
|
+
envFormat: def.acpMcpEnvFormat,
|
|
858
|
+
mcpBridge,
|
|
859
|
+
}, deps: {
|
|
860
|
+
lifecycle,
|
|
861
|
+
attachAcpSession: attachAcpSessionFn,
|
|
862
|
+
onPermissionRequest: options.acpPermissionHandler,
|
|
863
|
+
listProcessSnapshots: listProcessSnapshotsFn,
|
|
864
|
+
collectProcessTreePids: collectProcessTreePidsFn,
|
|
865
|
+
stopProcesses: stopProcessesFn,
|
|
866
|
+
onCleanupFailure: onCleanupFailureFn,
|
|
867
|
+
cleanupStagedFiles,
|
|
868
|
+
journal,
|
|
869
|
+
classifyFailure,
|
|
870
|
+
releaseStagedResources,
|
|
871
|
+
failBeforeSpawn,
|
|
872
|
+
} });
|
|
873
|
+
}
|
|
874
|
+
async function dispatchPiPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, overlayDelivery }, _options = {}) {
|
|
875
|
+
await runPiRpcDispatch({ input: {
|
|
876
|
+
runId: input.runId,
|
|
877
|
+
agentId: def.id,
|
|
878
|
+
child,
|
|
879
|
+
// Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
|
|
880
|
+
// so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
|
|
881
|
+
// rides `--append-system-prompt` instead, exactly once.
|
|
882
|
+
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
883
|
+
cwd: input.cwd,
|
|
884
|
+
model: input.model,
|
|
885
|
+
...(modelCatalog ? { modelCatalog } : {}),
|
|
886
|
+
imagePaths: input.imagePaths ?? [],
|
|
887
|
+
uploadRoot: input.uploadRoot,
|
|
888
|
+
}, deps: {
|
|
889
|
+
lifecycle,
|
|
890
|
+
attachPiRpcSession: attachPiRpcSessionFn,
|
|
891
|
+
listProcessSnapshots: listProcessSnapshotsFn,
|
|
892
|
+
collectProcessTreePids: collectProcessTreePidsFn,
|
|
893
|
+
stopProcesses: stopProcessesFn,
|
|
894
|
+
onCleanupFailure: onCleanupFailureFn,
|
|
895
|
+
cleanupStagedFiles,
|
|
896
|
+
journal,
|
|
897
|
+
classifyFailure,
|
|
898
|
+
releaseStagedResources,
|
|
899
|
+
failBeforeSpawn,
|
|
900
|
+
} });
|
|
901
|
+
}
|
|
902
|
+
function dispatchStdinPrompt({ input, def, child, overlayDelivery, imageDelivery, stdinHandle }, _options = {}) {
|
|
903
|
+
// The overlay reaches stdin only for a def that declares stdin as its prompt transport. The
|
|
904
|
+
// four `plain`-format defs that do not (`aider`/`antigravity`/`deepseek` put the prompt in
|
|
905
|
+
// argv, `grok-build` in a staged file) are still written to and closed here exactly as before,
|
|
906
|
+
// because this driver always spawns with `stdio: ['pipe','pipe','pipe']` and their CLIs need
|
|
907
|
+
// the EOF — but their prompt already carried the overlay through argv or the staged file, so
|
|
908
|
+
// sending the overlaid text here too would deliver it twice. This is the one place the
|
|
909
|
+
// resolver's own `''`-prefix rule is not sufficient on its own: those four defs are on the
|
|
910
|
+
// fallback strategy, so their prefix is genuinely non-empty; what makes stdin the wrong
|
|
911
|
+
// channel for them is the def's declared transport, not the strategy.
|
|
912
|
+
const stdinPrompt = def.promptViaStdin === true ? overlayDelivery.prompt : imageDelivery.prompt;
|
|
913
|
+
if (input.imageContents !== undefined)
|
|
914
|
+
stdinHandle.imageContents = input.imageContents;
|
|
915
|
+
writePromptToStdin(def, child, stdinPrompt, stdinHandle);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* `AgentExecutor.run()` — see that interface method's own doc for the
|
|
919
|
+
* public contract. Implementation note on shape: every guard below
|
|
920
|
+
* returns `failBeforeSpawn(...)` directly (a `Promise<never>`, valid
|
|
921
|
+
* wherever `Promise<void>` is expected) rather than `await`-then-`return`,
|
|
922
|
+
* so each failure path reads as a single, obviously-terminal statement.
|
|
923
|
+
* @param input - `{runId, agentId, prompt, cwd, model?, reasoning?, imagePaths?, extraAllowedDirs?, uploadRoot?, env?}` — `runId` must already be `lifecycle.start()`-ed.
|
|
924
|
+
* @throws {@link AgentExecutorError} — see index.ts module doc's Invariant; never a bare `Error`.
|
|
925
|
+
* @complexity O(1) setup (registry lookup, launch resolution, one spawn call); steady-state cost thereafter belongs to {@link wireChildLifecycle}.
|
|
926
|
+
* @overallScore 100/100
|
|
927
|
+
*/
|
|
928
|
+
async function run(requiredArgs, optionalArgs = {}) {
|
|
929
|
+
const { input, def, streamFormat } = await prepareRunContext({ requiredArgs, optionalArgs }, {});
|
|
930
|
+
// Computed once, before anything downstream ever looks at "the prompt" or "the allowed
|
|
931
|
+
// dirs" — a no-op (`{prompt: input.prompt, extraAllowedDirs: input.extraAllowedDirs}`,
|
|
932
|
+
// literally unchanged) unless `def.imageDelivery === 'prompt-path'` AND `input.imagePaths`
|
|
933
|
+
// is non-empty, so this can never affect a 'native'-delivery def (ACP, pi-rpc, qoder) or a
|
|
934
|
+
// run with no attachments. See `image-prompt-delivery.ts`'s own doc for the full mechanism;
|
|
935
|
+
// every use of `input.prompt`/`input.extraAllowedDirs` below that reflects what the CLI
|
|
936
|
+
// actually receives reads `imageDelivery.*` instead — the two ACP/pi-rpc `wire*Lifecycle`
|
|
937
|
+
// calls further down deliberately keep reading `input.prompt` verbatim, since those two
|
|
938
|
+
// defs' own native protocol already delivers the image and must never also get this
|
|
939
|
+
// treatment (the double-delivery hazard this mechanism exists to avoid).
|
|
940
|
+
await enforceNativeToolPolicy({ input, def }, {});
|
|
941
|
+
const imageDelivery = await resolveImageDeliveryAndArgvBudget({ input: { runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, deps: { failBeforeSpawn } });
|
|
942
|
+
const { systemPromptOverlay, overlayDelivery } = resolveRunOverlay({ input, def, imageDelivery }, {});
|
|
943
|
+
const { launch, spawnEnv } = await resolveRunLaunch({ input, def }, {});
|
|
944
|
+
const modelCatalog = await selectRunModel({ input, def, launch, spawnEnv }, {});
|
|
738
945
|
// Fill the def's `--help` capability gate before `buildArgs` reads it. Without this, only a
|
|
739
946
|
// host that happened to call `detectAgents` in THIS process ever had the gate filled, so e.g.
|
|
740
947
|
// `claude` never got `--include-partial-messages` (no streamed text). Probes once per binary;
|
|
@@ -822,10 +1029,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
822
1029
|
// the five mechanisms ends up carrying it. `null` for an unconfigured host or a def declaring
|
|
823
1030
|
// no strategy — see `buildMcpBridgeDelivery`'s doc.
|
|
824
1031
|
const mcpBridge = await resolveMcpBridgeForRun({ input: { runId: input.runId, cwd: input.cwd, def }, deps: { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn } });
|
|
825
|
-
const runtimeContext = computeRuntimeContext({ preparedPromptFile: preparedPromptFile, preparedLogFile: preparedLogFile, mcpBridge: mcpBridge }, {
|
|
826
|
-
...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }),
|
|
827
|
-
...(input.newSessionId === undefined ? {} : { newSessionId: input.newSessionId }),
|
|
828
|
-
});
|
|
1032
|
+
const runtimeContext = computeRuntimeContext({ preparedPromptFile: preparedPromptFile, preparedLogFile: preparedLogFile, mcpBridge: mcpBridge }, sessionRuntimeContext(input, {}));
|
|
829
1033
|
// A `runtimeLock` def's buildArgs mutates process-global state its own CLI reads back at
|
|
830
1034
|
// startup, so the mutex must be held from before buildArgs until the spawned child has
|
|
831
1035
|
// demonstrably consumed it — see `RuntimeLock`'s own doc for the concrete race. Undefined for
|
|
@@ -862,13 +1066,13 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
862
1066
|
// points at a real file. Skipped entirely for the other four mechanisms and whenever no bridge
|
|
863
1067
|
// was resolved at all. `writtenMcpJsonPath` is set only once the write actually happens, so
|
|
864
1068
|
// `cleanupStagedFiles` knows there is a live-token file to remove afterward.
|
|
865
|
-
writtenMcpJsonPath = await writeMcpJsonIfNeeded({ input: { runId: input.runId, cwd: input.cwd, def, mcpBridge }, deps: { mcpJsonInjection, releaseStagedResources, failBeforeSpawn } });
|
|
1069
|
+
writtenMcpJsonPath = await writeMcpJsonIfNeeded({ input: { runId: input.runId, cwd: input.cwd, nativeToolAccess: input.nativeToolAccess, ...nativeCapabilityInput({ input }, {}), def, mcpBridge }, deps: { mcpJsonInjection, releaseStagedResources, failBeforeSpawn } });
|
|
866
1070
|
// Mechanism 5 of 5's one effect — stage this run's scratch `CODEX_HOME` directory. Skipped
|
|
867
1071
|
// entirely for the other four mechanisms and whenever no bridge was resolved at all.
|
|
868
1072
|
// `codex.ts`'s `buildArgs` needs no argv change for this (CODEX_HOME is an env var, not a flag),
|
|
869
1073
|
// so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
|
|
870
1074
|
// constraint of its own; it is placed here only to keep the two staging steps adjacent.
|
|
871
|
-
preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, def, mcpBridge }, deps: { mcpJsonInjection, hostEnv, releaseStagedResources, failBeforeSpawn } });
|
|
1075
|
+
preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, nativeToolAccess: input.nativeToolAccess, ...nativeCapabilityInput({ input }, {}), def, mcpBridge }, deps: { mcpJsonInjection, hostEnv: input.cliParity ? spawnEnv : hostEnv, releaseStagedResources, failBeforeSpawn } });
|
|
872
1076
|
// Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stage this run's scratch
|
|
873
1077
|
// `CLAUDE_CONFIG_DIR` directory. Unconditional for a `claude`-id run (unlike CODEX_HOME above,
|
|
874
1078
|
// this does not depend on `mcpJsonInjection` being configured at all — see
|
|
@@ -894,14 +1098,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
894
1098
|
// `McpBridgeDelivery`'s `'codex-toml'` variant doc for why it cannot be pre-computed the way
|
|
895
1099
|
// `'claude-mcp-json'`'s deterministic path is). Nothing between the old, earlier call site and
|
|
896
1100
|
// here ever read `childEnv`, so moving the call cost nothing.
|
|
897
|
-
const childEnv = computeChildEnv({ spawnEnv
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
...(preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
|
|
901
|
-
? { stagedInstructionsFile: { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path } }
|
|
902
|
-
: {}),
|
|
903
|
-
...(preparedClaudeConfigDir === null ? {} : { claudeConfigDir: preparedClaudeConfigDir.path }),
|
|
904
|
-
});
|
|
1101
|
+
const childEnv = computeChildEnv({ spawnEnv, mcpBridge }, stagedChildEnvOptions({
|
|
1102
|
+
def, preparedCodexHome, preparedClaudeConfigDir, preparedSystemPromptOverlayFile, systemPromptEnvOverrides,
|
|
1103
|
+
}, {}));
|
|
905
1104
|
// Post-buildArgs guard for argv-bound defs whose resolved binary is a
|
|
906
1105
|
// Windows .cmd/.bat shim or a direct .exe: a prompt under the raw byte
|
|
907
1106
|
// budget can still expand past CreateProcess's command-line cap once
|
|
@@ -924,38 +1123,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
924
1123
|
// grandchild inheriting them can delay arbitrarily). A spawn that never produced a process at
|
|
925
1124
|
// all emits no `'exit'`, and is covered instead by `releaseStagedResources` on the reject path.
|
|
926
1125
|
child.once('exit', releaseRuntimeLock);
|
|
927
|
-
const
|
|
928
|
-
const stdinHandle = isStdinDrivenFormat(formatForStdin)
|
|
929
|
-
? wireChildLifecycle({
|
|
930
|
-
startingModel: input.model,
|
|
931
|
-
modelCatalog,
|
|
932
|
-
runId: input.runId,
|
|
933
|
-
def,
|
|
934
|
-
streamFormat: formatForStdin.streamFormat,
|
|
935
|
-
child,
|
|
936
|
-
lifecycle,
|
|
937
|
-
listProcessSnapshots: listProcessSnapshotsFn,
|
|
938
|
-
collectProcessTreePids: collectProcessTreePidsFn,
|
|
939
|
-
stopProcesses: stopProcessesFn,
|
|
940
|
-
onCleanupFailure: onCleanupFailureFn,
|
|
941
|
-
cleanupStagedFiles,
|
|
942
|
-
journal,
|
|
943
|
-
continuation,
|
|
944
|
-
classifyFailure,
|
|
945
|
-
bufferedStdoutMaxBytes,
|
|
946
|
-
expectsJiniBridge: writtenMcpJsonPath !== undefined,
|
|
947
|
-
// probeMcpBridge itself strips the run credential and swaps in a diagnostic run id.
|
|
948
|
-
bridgeProbeEntry: mcpBridge?.kind === 'claude-mcp-json' && mcpJsonInjection?.probeOnFailure !== false
|
|
949
|
-
? mcpBridge.serverEntry
|
|
950
|
-
: undefined,
|
|
951
|
-
probeMcpBridge: options.probeMcpBridge ?? probeMcpBridge,
|
|
952
|
-
})
|
|
953
|
-
: null;
|
|
954
|
-
// Registered with the lifecycle wiring above, before the spawn-confirmation await, so a child
|
|
955
|
-
// that closes immediately never leaves a stale entry behind.
|
|
1126
|
+
const stdinHandle = wireRunInput({ input, def, streamFormat, child, modelCatalog, cleanupStagedFiles, writtenMcpJsonPath, mcpBridge }, {});
|
|
956
1127
|
const runId = input.runId;
|
|
957
|
-
liveRunInputs.set(runId, stdinHandle ? stdinHandle.sendUserMessage : async () => 'unsupported');
|
|
958
|
-
child.once('close', () => liveRunInputs.delete(runId));
|
|
959
1128
|
try {
|
|
960
1129
|
await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
|
|
961
1130
|
}
|
|
@@ -970,83 +1139,12 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
970
1139
|
// worse than releasing early (see `RuntimeLockHold.waitForHandoff`'s own doc).
|
|
971
1140
|
armHandoffWatcher({ runtimeLockHold: runtimeLockHold, handoffInput: { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, release: releaseRuntimeLock });
|
|
972
1141
|
if (streamFormat === 'acp-json-rpc') {
|
|
973
|
-
|
|
974
|
-
runId: input.runId,
|
|
975
|
-
agentId: def.id,
|
|
976
|
-
child,
|
|
977
|
-
// `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
|
|
978
|
-
// this call site deliberately sends the raw input prompt rather than the image-delivery
|
|
979
|
-
// rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
|
|
980
|
-
// images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
|
|
981
|
-
// is the part of the overlay decision that applies to whatever prompt text a transport was
|
|
982
|
-
// already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
|
|
983
|
-
// sending byte-identical text and never receive the overlay twice.
|
|
984
|
-
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
985
|
-
cwd: input.cwd,
|
|
986
|
-
permissionMode: input.permissionMode,
|
|
987
|
-
model: input.model,
|
|
988
|
-
modelCatalog,
|
|
989
|
-
imagePaths: input.imagePaths ?? [],
|
|
990
|
-
envFormat: def.acpMcpEnvFormat,
|
|
991
|
-
mcpBridge,
|
|
992
|
-
}, deps: {
|
|
993
|
-
lifecycle,
|
|
994
|
-
attachAcpSession: attachAcpSessionFn,
|
|
995
|
-
onPermissionRequest: options.acpPermissionHandler,
|
|
996
|
-
listProcessSnapshots: listProcessSnapshotsFn,
|
|
997
|
-
collectProcessTreePids: collectProcessTreePidsFn,
|
|
998
|
-
stopProcesses: stopProcessesFn,
|
|
999
|
-
onCleanupFailure: onCleanupFailureFn,
|
|
1000
|
-
cleanupStagedFiles,
|
|
1001
|
-
journal,
|
|
1002
|
-
classifyFailure,
|
|
1003
|
-
releaseStagedResources,
|
|
1004
|
-
failBeforeSpawn,
|
|
1005
|
-
} });
|
|
1006
|
-
return;
|
|
1142
|
+
return dispatchAcpPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, mcpBridge, overlayDelivery }, {});
|
|
1007
1143
|
}
|
|
1008
1144
|
if (streamFormat === 'pi-rpc') {
|
|
1009
|
-
|
|
1010
|
-
runId: input.runId,
|
|
1011
|
-
agentId: def.id,
|
|
1012
|
-
child,
|
|
1013
|
-
// Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
|
|
1014
|
-
// so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
|
|
1015
|
-
// rides `--append-system-prompt` instead, exactly once.
|
|
1016
|
-
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
1017
|
-
cwd: input.cwd,
|
|
1018
|
-
model: input.model,
|
|
1019
|
-
modelCatalog,
|
|
1020
|
-
imagePaths: input.imagePaths ?? [],
|
|
1021
|
-
uploadRoot: input.uploadRoot,
|
|
1022
|
-
}, deps: {
|
|
1023
|
-
lifecycle,
|
|
1024
|
-
attachPiRpcSession: attachPiRpcSessionFn,
|
|
1025
|
-
listProcessSnapshots: listProcessSnapshotsFn,
|
|
1026
|
-
collectProcessTreePids: collectProcessTreePidsFn,
|
|
1027
|
-
stopProcesses: stopProcessesFn,
|
|
1028
|
-
onCleanupFailure: onCleanupFailureFn,
|
|
1029
|
-
cleanupStagedFiles,
|
|
1030
|
-
journal,
|
|
1031
|
-
classifyFailure,
|
|
1032
|
-
releaseStagedResources,
|
|
1033
|
-
failBeforeSpawn,
|
|
1034
|
-
} });
|
|
1035
|
-
return;
|
|
1145
|
+
return dispatchPiPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, overlayDelivery }, {});
|
|
1036
1146
|
}
|
|
1037
|
-
|
|
1038
|
-
// four `plain`-format defs that do not (`aider`/`antigravity`/`deepseek` put the prompt in
|
|
1039
|
-
// argv, `grok-build` in a staged file) are still written to and closed here exactly as before,
|
|
1040
|
-
// because this driver always spawns with `stdio: ['pipe','pipe','pipe']` and their CLIs need
|
|
1041
|
-
// the EOF — but their prompt already carried the overlay through argv or the staged file, so
|
|
1042
|
-
// sending the overlaid text here too would deliver it twice. This is the one place the
|
|
1043
|
-
// resolver's own `''`-prefix rule is not sufficient on its own: those four defs are on the
|
|
1044
|
-
// fallback strategy, so their prefix is genuinely non-empty; what makes stdin the wrong
|
|
1045
|
-
// channel for them is the def's declared transport, not the strategy.
|
|
1046
|
-
const stdinPrompt = def.promptViaStdin === true ? overlayDelivery.prompt : imageDelivery.prompt;
|
|
1047
|
-
if (input.imageContents !== undefined)
|
|
1048
|
-
stdinHandle.imageContents = input.imageContents;
|
|
1049
|
-
writePromptToStdin(def, child, stdinPrompt, stdinHandle);
|
|
1147
|
+
dispatchStdinPrompt({ input, def, child, overlayDelivery, imageDelivery, stdinHandle }, {});
|
|
1050
1148
|
}
|
|
1051
1149
|
return { run, sendUserMessage };
|
|
1052
1150
|
}
|