@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
  3. package/dist/agent-executor/antigravity-permissions.js +64 -0
  4. package/dist/agent-executor/contracts.d.ts +37 -6
  5. package/dist/agent-executor/event-translation.d.ts +6 -1
  6. package/dist/agent-executor/event-translation.js +3 -1
  7. package/dist/agent-executor/events.d.ts +5 -0
  8. package/dist/agent-executor/events.js +143 -87
  9. package/dist/agent-executor/launch.d.ts +5 -3
  10. package/dist/agent-executor/launch.js +315 -217
  11. package/dist/agent-executor/mcp-bridge-probe.js +51 -35
  12. package/dist/agent-executor/mcp-bridge.d.ts +18 -8
  13. package/dist/agent-executor/mcp-bridge.js +105 -65
  14. package/dist/agent-executor/run-output.d.ts +20 -0
  15. package/dist/agent-executor/run-output.js +80 -0
  16. package/dist/attachment-content.js +18 -15
  17. package/dist/core/read-only-tools.d.ts +15 -0
  18. package/dist/core/read-only-tools.js +29 -1
  19. package/dist/core/tool-audit.d.ts +3 -1
  20. package/dist/core/tool-audit.js +35 -22
  21. package/dist/core/tool-gaps.d.ts +60 -0
  22. package/dist/core/tool-gaps.js +199 -0
  23. package/dist/delegated-result-correlation.d.ts +26 -0
  24. package/dist/delegated-result-correlation.js +68 -0
  25. package/dist/delegated-tool-bridge.d.ts +6 -14
  26. package/dist/delegated-tool-bridge.js +155 -155
  27. package/dist/event-log.d.ts +45 -2
  28. package/dist/event-log.js +83 -4
  29. package/dist/http/attachments.d.ts +26 -1
  30. package/dist/http/attachments.js +129 -65
  31. package/dist/http/connectors.d.ts +1 -32
  32. package/dist/http/connectors.js +13 -68
  33. package/dist/http/delegated-tools.d.ts +8 -1
  34. package/dist/http/delegated-tools.js +30 -30
  35. package/dist/http/host-tools.d.ts +1 -0
  36. package/dist/http/host-tools.js +64 -59
  37. package/dist/http/index.d.ts +2 -2
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/media.js +48 -69
  40. package/dist/http/memory.js +45 -36
  41. package/dist/http/model-proxy-fields.d.ts +23 -0
  42. package/dist/http/model-proxy-fields.js +29 -0
  43. package/dist/http/model-proxy.js +193 -395
  44. package/dist/http/research-tavily.adapter.js +72 -49
  45. package/dist/http/research.js +33 -23
  46. package/dist/http/resource-request.d.ts +37 -0
  47. package/dist/http/resource-request.js +59 -0
  48. package/dist/http/routines.js +49 -90
  49. package/dist/http/runs.d.ts +7 -5
  50. package/dist/http/runs.js +21 -7
  51. package/dist/http/terminals.js +23 -48
  52. package/dist/http/tool-catalog.d.ts +5 -0
  53. package/dist/http/xai.d.ts +11 -1
  54. package/dist/http/xai.js +125 -124
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +1 -0
  57. package/dist/legacy-data-migration.js +41 -24
  58. package/dist/run/core/retry.js +51 -35
  59. package/dist/run/diagnostics/diagnostics.js +5 -17
  60. package/dist/run-credentials.js +49 -27
  61. package/dist/run-lifecycle.d.ts +31 -2
  62. package/dist/run-lifecycle.js +49 -42
  63. package/dist/store/agent-sessions/sqlite.d.ts +6 -0
  64. package/dist/store/agent-sessions/sqlite.js +6 -4
  65. package/dist/surface-exchanges.js +2 -1
  66. package/dist/terminal-session.d.ts +1 -1
  67. package/dist/terminal-session.js +26 -33
  68. package/dist/tool-audit.d.ts +1 -0
  69. package/dist/tool-audit.js +1 -0
  70. package/dist/tool-executor.d.ts +24 -3
  71. package/dist/tool-executor.js +276 -139
  72. package/dist/tool-recovery/recovery.js +64 -35
  73. package/dist/tool-recovery/stack.d.ts +2 -1
  74. package/dist/tool-recovery/stack.js +1 -1
  75. package/dist/tool-result-diagnostics.d.ts +27 -0
  76. package/dist/tool-result-diagnostics.js +77 -0
  77. package/dist/tool-result-media.d.ts +27 -14
  78. package/dist/tool-result-media.js +51 -38
  79. 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
- const envOptions = options.hostEnv ? { env: options.hostEnv } : {};
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, ...(options.piGracefulShutdownMs !== undefined ? { gracefulShutdownMs: options.piGracefulShutdownMs } : {}),
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
- // Matches claude.ts buildArgs' own `typeof x === 'string' && x` truthiness check, so an empty
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
- ...(hasResumeSessionId ? { resumeSessionId } : {}),
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, runtimeContext) {
325
- return promptAugmenter?.systemOverlay?.({
326
- agentId,
327
- turnIndex: runtimeContext?.hasPriorAssistantTurn ? 1 : 0,
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
- ...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
343
- ...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
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 Object.keys(options).length === 0 ? undefined : options;
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, overlay, prompt, resumeSessionId } = input;
395
- if (typeof overlay !== 'string' || overlay.length === 0) {
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
- const capabilityKey = systemPromptDelivery.capabilityKey;
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
- const isContinuingExistingSession = (resumesSessionViaCli === true || resumesSessionViaAcpLoad === true) &&
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
- ...(options === undefined ? {} : { options }),
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 fsPromises.mkdtemp(join(tmpdir(), `jini-system-prompt-overlay-${safeRunId}-`));
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
- await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
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
- await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
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
- * `AgentExecutor.run()` — see that interface method's own doc for the
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
- const { def, streamFormat } = await resolveDefAndStreamFormat({ input: { runId: input.runId, agentId: input.agentId }, deps: { getAgentDef: getAgentDefFn, failBeforeSpawn } });
671
- // Computed once, before anything downstream ever looks at "the prompt" or "the allowed
672
- // dirs" — a no-op (`{prompt: input.prompt, extraAllowedDirs: input.extraAllowedDirs}`,
673
- // literally unchanged) unless `def.imageDelivery === 'prompt-path'` AND `input.imagePaths`
674
- // is non-empty, so this can never affect a 'native'-delivery def (ACP, pi-rpc, qoder) or a
675
- // run with no attachments. See `image-prompt-delivery.ts`'s own doc for the full mechanism;
676
- // every use of `input.prompt`/`input.extraAllowedDirs` below that reflects what the CLI
677
- // actually receives reads `imageDelivery.*` instead — the two ACP/pi-rpc `wire*Lifecycle`
678
- // calls further down deliberately keep reading `input.prompt` verbatim, since those two
679
- // defs' own native protocol already delivers the image and must never also get this
680
- // treatment (the double-delivery hazard this mechanism exists to avoid).
681
- const imageDelivery = await resolveImageDeliveryAndArgvBudget({ input: { runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, deps: { failBeforeSpawn } });
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`'s third argument is the pre-staging `RuntimeContext`: it reads
691
- // only `hasPriorAssistantTurn`, which `computeRuntimeContext` never populates from any of the
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. Passing the narrower context makes that
694
- // independence explicit rather than relying on the ordering staying lucky.
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
- ...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }),
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 resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv({ input: input, hostEnv }) };
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
- // Resolve from the launch environment before staging or sending any prompt. Pin the same
721
- // concrete ID that the picker displays; an opaque native route cannot silently start a run.
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
- try {
724
- const selectModel = options.resolveModelForLaunch ?? ((input) => resolveModelForLaunch(input, options.runtimeState ? { deps: getRuntimeModelDiscoveryDeps({ state: options.runtimeState }) } : {}));
725
- const selection = await selectModel({ def, context: {
726
- executable: launch.launchPath, cwd: input.cwd, env: spawnEnv,
727
- ...(input.model && input.model !== 'default' ? { model: input.model } : {}),
728
- ...(input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}),
729
- ...(input.settingSources ? { settingSources: input.settingSources } : {}),
730
- ...(input.settings ? { settings: input.settings } : {}),
731
- } });
732
- input.model = selection.model;
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: spawnEnv, mcpBridge: mcpBridge }, {
898
- ...(preparedCodexHome === null ? {} : { codexHomeDir: preparedCodexHome.path }),
899
- systemPromptEnvOverrides,
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 formatForStdin = { streamFormat };
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
- await runAcpDispatch({ input: {
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
- await runPiRpcDispatch({ input: {
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
- // The overlay reaches stdin only for a def that declares stdin as its prompt transport. The
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
  }