@jini-ai/daemon 0.6.15-rc.2 → 0.6.15

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 (49) hide show
  1. package/dist/agent-executor/codex/home-config.js +3 -2
  2. package/dist/agent-executor/codex/home-staging.js +2 -2
  3. package/dist/agent-executor/codex/isolation.js +6 -4
  4. package/dist/agent-executor/event-translation.js +2 -2
  5. package/dist/agent-executor/events.js +64 -48
  6. package/dist/agent-executor/launch.js +30 -46
  7. package/dist/agent-executor/live-session-state.js +2 -2
  8. package/dist/agent-executor/mcp-bridge.js +5 -0
  9. package/dist/agent-executor/message-relay.js +5 -6
  10. package/dist/agent-executor/run-output.js +7 -2
  11. package/dist/close-status.js +6 -2
  12. package/dist/core/omit-keys.d.ts +7 -0
  13. package/dist/core/omit-keys.js +12 -0
  14. package/dist/core/read-only-tools.js +5 -1
  15. package/dist/core/tool-gaps.js +1 -1
  16. package/dist/delegated-result-correlation.js +2 -2
  17. package/dist/http/agents.d.ts +4 -0
  18. package/dist/http/attachments.js +13 -12
  19. package/dist/http/component-catalog.js +1 -1
  20. package/dist/http/connectors.js +7 -5
  21. package/dist/http/db-ops.js +5 -0
  22. package/dist/http/delegated-tools.js +5 -0
  23. package/dist/http/frontend-sessions.js +7 -5
  24. package/dist/http/host-tools.js +7 -5
  25. package/dist/http/memory.js +3 -1
  26. package/dist/http/model-proxy.js +13 -8
  27. package/dist/http/research-tavily.adapter.js +2 -1
  28. package/dist/http/routines.js +7 -5
  29. package/dist/http/terminals.js +5 -0
  30. package/dist/http/tool-catalog.js +1 -1
  31. package/dist/http/xai.js +10 -7
  32. package/dist/legacy-data-migration.d.ts +0 -12
  33. package/dist/legacy-data-migration.js +21 -13
  34. package/dist/remote-tool-executor.js +2 -1
  35. package/dist/routines/schedule.js +2 -1
  36. package/dist/routines/scheduler.js +6 -7
  37. package/dist/run-lifecycle.js +2 -1
  38. package/dist/store/agent-sessions/sql.js +7 -3
  39. package/dist/store/event-log/sqlite-kernel.js +5 -1
  40. package/dist/store/event-log/sqlite.js +6 -2
  41. package/dist/surface-exchanges.js +3 -3
  42. package/dist/terminal-session.js +4 -5
  43. package/dist/tool-executor.js +47 -3
  44. package/dist/tool-recovery/recovery.js +5 -3
  45. package/dist/tool-recovery/redaction.js +1 -2
  46. package/dist/tool-recovery/stack.js +5 -1
  47. package/dist/tool-result-media.js +7 -1
  48. package/dist/tool-result-surfaces.js +10 -9
  49. package/package.json +20 -15
@@ -103,7 +103,7 @@ function scanBareTomlKeyChar(scan, ch) {
103
103
  scan.current += ch;
104
104
  }
105
105
  // An inline or dotted root `mcp_servers` declaration, which the table scanner below cannot rewrite.
106
- const INLINE_MCP_SERVERS = /^\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*(?:\.|=)/;
106
+ const INLINE_MCP_SERVERS = /^\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*[.=]/;
107
107
  // `[<key>]` optionally followed only by a `# comment`; `[[...]]` never matches (see below).
108
108
  const TOML_TABLE_HEADER = /^\s*\[([^[\]]+)\]\s*(#.*)?$/;
109
109
  /**
@@ -203,7 +203,8 @@ function stripExistingJiniMcpServerTable(existingRaw, optional = {}) {
203
203
  */
204
204
  export function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp = true, inheritUserSettings = true }, _optional = {}) {
205
205
  const base = stripExistingJiniMcpServerTable(existingRaw ?? '', { allServers: !inheritUserMcp, mcpOnly: !inheritUserSettings });
206
- const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
206
+ const blockBreak = base.endsWith('\n') ? '\n' : '\n\n';
207
+ const separator = base.length === 0 ? '' : blockBreak;
207
208
  return `${base}${separator}${buildCodexMcpServerToml({ entry: entry })}`;
208
209
  }
209
210
  //# sourceMappingURL=home-config.js.map
@@ -5,6 +5,7 @@ import { errorMessage } from '../values.js';
5
5
  import { defaultReadMcpJsonFile, defaultWriteMcpJsonFile } from '../mcp-bridge.js';
6
6
  import { buildCodexHomeConfigToml } from './home-config.js';
7
7
  import { assertCodexHomeConfigParses, assertStagedHomeIsolated, CodexMcpIsolationError, codexIsolationRefusalMessage } from './isolation.js';
8
+ import { omitKeys } from '../../core/omit-keys.js';
8
9
  /**
9
10
  * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
10
11
  * never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
@@ -27,8 +28,7 @@ export function resolveSourceCodexHomeDir({ hostEnv }) {
27
28
  * `mcp_skill_dependencies.rs`), and `codex exec` is `codex_exec` without it.
28
29
  */
29
30
  export function withStagedCodexHome({ env, codexHomeDir }, _options = {}) {
30
- const { CODEX_INTERNAL_ORIGINATOR_OVERRIDE: _originator, ...rest } = env;
31
- return { ...rest, CODEX_HOME: codexHomeDir };
31
+ return { ...omitKeys(env, ['CODEX_INTERNAL_ORIGINATOR_OVERRIDE']), CODEX_HOME: codexHomeDir };
32
32
  }
33
33
  function defaultMkdtempCodexHome({ prefix }) {
34
34
  return fsPromises.mkdtemp(join(tmpdir(), prefix));
@@ -1,4 +1,4 @@
1
- import { parse } from 'smol-toml';
1
+ import { createRequire } from 'node:module';
2
2
  import { JINI_MCP_SERVER_KEY } from '../mcp-bridge.js';
3
3
  import { isRecord } from '../values.js';
4
4
  /** A run with personal MCP off that Codex cannot keep to the bridge alone: refused, never run with a leak. */
@@ -15,7 +15,9 @@ export function codexIsolationRefusalMessage(reason) {
15
15
  * known, and Codex could not load it either.
16
16
  * @complexity O(n) in the file length.
17
17
  */
18
- function parseCodexLayer(raw, path) {
18
+ function parseCodexLayer({ raw, path }, _optional = {}) {
19
+ // The optional parser is needed only for Codex isolation, so memory-only hosts can still boot.
20
+ const { parse } = createRequire(import.meta.url)('smol-toml');
19
21
  try {
20
22
  return parse(raw);
21
23
  }
@@ -41,7 +43,7 @@ function assertTrustsNoProject(config, path) {
41
43
  */
42
44
  export function assertCodexHomeConfigParses({ raw, path }) {
43
45
  if (raw !== undefined)
44
- parseCodexLayer(raw, path);
46
+ parseCodexLayer({ raw, path });
45
47
  }
46
48
  /**
47
49
  * The staged home config, parsed: it must declare no MCP server but the bridge and trust no
@@ -54,7 +56,7 @@ export function assertStagedHomeIsolated({ homeConfig, path }) {
54
56
  const refusal = new CodexMcpIsolationError(`${path} declares MCP servers that can't be removed safely.`);
55
57
  let config;
56
58
  try {
57
- config = parse(homeConfig);
59
+ config = parseCodexLayer({ raw: homeConfig, path });
58
60
  }
59
61
  catch {
60
62
  throw refusal;
@@ -227,9 +227,9 @@ export function applyAgentTranslationSideEffects({ payload, sessionId, sink }) {
227
227
  sink.onSessionId({ sessionId });
228
228
  if (payload.type === 'tool_use') {
229
229
  sink.onToolCall({ toolUse: payload });
230
+ return;
230
231
  }
231
- else if ((payload.type === 'text_delta' || payload.type === 'thinking_delta') && payload.delta.length > 0) {
232
+ if ((payload.type === 'text_delta' || payload.type === 'thinking_delta') && payload.delta.length > 0)
232
233
  sink.onUserVisibleOutput();
233
- }
234
234
  }
235
235
  //# sourceMappingURL=event-translation.js.map
@@ -8,7 +8,7 @@ import { observeRunOutput } from './run-output.js';
8
8
  import { messageContentWithImages } from '../attachment-content.js';
9
9
  import { redactSecrets, } from '@jini-ai/core';
10
10
  import { createClaudeStreamHandler, createCopilotStreamHandler, createJsonEventStreamHandler, createQoderStreamHandler, attachAcpSession, attachPiRpcSession, } from '@jini-ai/agent-runtime';
11
- import { collectProcessTreePids, listProcessSnapshots, stopProcesses, } from '@jini-ai/platform/process';
11
+ import { listProcessSnapshots, stopProcesses, } from '@jini-ai/platform/process';
12
12
  import { classifyRunCloseStatus, } from '../close-status.js';
13
13
  import { resolveContinuationTransport, } from '../continuation/continuation-transport.js';
14
14
  import { resultContent, } from '../delegated-tool-bridge.js';
@@ -48,6 +48,11 @@ function createStreamHandlerForDef(def, streamFormat, onEvent) {
48
48
  return createQoderStreamHandler({ onEvent: onEvent });
49
49
  case 'json-event-stream':
50
50
  return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: onEvent });
51
+ default: {
52
+ // Unreachable: every stream format has a case above, and a new one fails typecheck here.
53
+ const unknownFormat = streamFormat;
54
+ throw new TypeError(`Unknown agent stream format: ${String(unknownFormat)}`);
55
+ }
51
56
  }
52
57
  }
53
58
  /**
@@ -463,9 +468,8 @@ export function wireChildLifecycle(ctx) {
463
468
  */
464
469
  function handleTurnEnd(stopReason) {
465
470
  const toolUse = pendingToolUse;
466
- const shouldInject = stopReason === 'tool_use' &&
467
- toolUse !== undefined &&
468
- continuation !== undefined &&
471
+ const toolTurnPending = stopReason === 'tool_use' && toolUse !== undefined && continuation !== undefined;
472
+ const shouldInject = toolTurnPending &&
469
473
  resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
470
474
  continuation.autonomousToolNames.has(toolUse.name);
471
475
  if (!shouldInject) {
@@ -547,10 +551,10 @@ export function wireChildLifecycle(ctx) {
547
551
  return lifecycle.emit({ runId, input: { event: 'agent', data: statusWithChildIdentity({ payload, childPid: ctx.child.pid }, {}) } });
548
552
  });
549
553
  }
550
- else if (translation.kind === 'error') {
554
+ if (translation.kind === 'error') {
551
555
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
552
556
  }
553
- else if (translation.kind === 'turn-end') {
557
+ if (translation.kind === 'turn-end') {
554
558
  handleTurnEnd(translation.stopReason);
555
559
  }
556
560
  });
@@ -607,7 +611,8 @@ export function wireChildLifecycle(ctx) {
607
611
  }
608
612
  /**
609
613
  * Emits the accumulated `'until-close'` stdout — sanitized — as exactly one raw `'stdout'` echo
610
- * plus one `text_delta`, through the same `enqueueEmit` FIFO queue every other emit uses, so the
614
+ * plus one `text_delta` for plain output, or through the declared structured parser. Both use
615
+ * the same `enqueueEmit` FIFO queue every other emit uses, so the
611
616
  * flush is ordered after every already-queued event and before `finish()`'s `'end'`. A no-op for
612
617
  * every live-streaming def (nothing was ever accumulated) and for a buffered run that produced
613
618
  * no stdout at all — an empty `text_delta` is noise, not information.
@@ -622,10 +627,19 @@ export function wireChildLifecycle(ctx) {
622
627
  const safe = sanitizeBufferedStdout ? sanitizeBufferedStdout({ fullText: bufferedStdout }) : bufferedStdout;
623
628
  bufferedStdout = '';
624
629
  bufferedStdoutBytes = 0;
625
- const text = droppedStdoutBytes > 0
626
- ? `${safe}${bufferedStdoutTruncationNotice(droppedStdoutBytes, ctx.bufferedStdoutMaxBytes)}`
627
- : safe;
630
+ const dropped = droppedStdoutBytes;
628
631
  droppedStdoutBytes = 0;
632
+ if (streamHandler !== null) {
633
+ // Redact the complete wire text before parsing: structured errors can contain sign-in URLs too.
634
+ stdoutDiagnostics.feed({ text: safe });
635
+ streamHandler.feed({ chunk: safe });
636
+ if (dropped > 0)
637
+ enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'error', data: {
638
+ message: bufferedStdoutTruncationNotice(dropped, ctx.bufferedStdoutMaxBytes),
639
+ } } }));
640
+ return;
641
+ }
642
+ const text = dropped > 0 ? `${safe}${bufferedStdoutTruncationNotice(dropped, ctx.bufferedStdoutMaxBytes)}` : safe;
629
643
  if (text.length === 0)
630
644
  return;
631
645
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
@@ -634,27 +648,27 @@ export function wireChildLifecycle(ctx) {
634
648
  child.stdout?.on('data', (chunk) => {
635
649
  const text = chunk.toString('utf8');
636
650
  stdoutDiagnostics.record({ chunk, text });
637
- if (streamFormat === 'plain') {
638
- if (text.length > 0)
639
- userVisibleOutputSeen = true;
640
- if (bufferStdoutUntilClose) {
641
- // Nothing is emitted on *either* channel yet — see this function's doc on why holding the
642
- // raw echo back matters as much as holding back the chat copy.
643
- //
644
- // Whole chunks only: a chunk that would cross the ceiling is dropped entirely rather than
645
- // sliced to fit, which keeps the accumulator free of half-written multi-byte characters (a
646
- // `data` event boundary already need not align with one) and makes the kept prefix exactly
647
- // the bytes some prefix of chunks produced. Everything after the first refusal is dropped
648
- // too — the point is a hard ceiling on resident bytes, not a best-effort tail.
649
- const chunkBytes = Buffer.byteLength(text, 'utf8');
650
- if (droppedStdoutBytes > 0 || bufferedStdoutBytes + chunkBytes > ctx.bufferedStdoutMaxBytes) {
651
- droppedStdoutBytes += chunkBytes;
652
- return;
653
- }
654
- bufferedStdout += text;
655
- bufferedStdoutBytes += chunkBytes;
651
+ if (streamFormat === 'plain' && text.length > 0)
652
+ userVisibleOutputSeen = true;
653
+ if (bufferStdoutUntilClose) {
654
+ // Nothing is emitted on *either* channel yet — see this function's doc on why holding the
655
+ // raw echo back matters as much as holding back the chat copy.
656
+ //
657
+ // Whole chunks only: a chunk that would cross the ceiling is dropped entirely rather than
658
+ // sliced to fit, which keeps the accumulator free of half-written multi-byte characters (a
659
+ // `data` event boundary already need not align with one) and makes the kept prefix exactly
660
+ // the bytes some prefix of chunks produced. Everything after the first refusal is dropped
661
+ // too — the point is a hard ceiling on resident bytes, not a best-effort tail.
662
+ const chunkBytes = Buffer.byteLength(text, 'utf8');
663
+ if (droppedStdoutBytes > 0 || bufferedStdoutBytes + chunkBytes > ctx.bufferedStdoutMaxBytes) {
664
+ droppedStdoutBytes += chunkBytes;
656
665
  return;
657
666
  }
667
+ bufferedStdout += text;
668
+ bufferedStdoutBytes += chunkBytes;
669
+ return;
670
+ }
671
+ if (streamFormat === 'plain') {
658
672
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
659
673
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
660
674
  return;
@@ -702,12 +716,12 @@ export function wireChildLifecycle(ctx) {
702
716
  feedSessionLine(controlScanBuffer);
703
717
  controlScanBuffer = '';
704
718
  }
719
+ flushBufferedStdout();
705
720
  streamHandler?.flush();
706
721
  session.endSession();
707
722
  stdoutDiagnostics.flush();
708
- // Queued before `await emitQueue` so the flushed text is durably appended ahead of
709
- // `finish()`'s `'end'` event, exactly like every live-path emit already is.
710
- flushBufferedStdout();
723
+ // The buffered flush is queued before `await emitQueue` so its sanitized events are durable
724
+ // ahead of `finish()`'s `'end'`, exactly like every live-path emit already is.
711
725
  await emitQueue;
712
726
  if (cancelRequested)
713
727
  await cancelUnresolvedToolCalls();
@@ -843,7 +857,10 @@ function wireAcpLifecycle(ctx) {
843
857
  unsubscribeCancel();
844
858
  // Guarded for the same reasons as the child-driven handler above.
845
859
  await cleanupStagedFilesSafely(ctx);
846
- const status = await output.settle({ status: cancelRequested ? 'cancelled' : controller?.completedSuccessfully() ? 'succeeded' : 'failed' });
860
+ let finalStatus = 'cancelled';
861
+ if (!cancelRequested)
862
+ finalStatus = controller?.completedSuccessfully() ? 'succeeded' : 'failed';
863
+ const status = await output.settle({ status: finalStatus });
847
864
  const resumable = status === 'failed' && classifyFailure !== undefined
848
865
  ? await classifyFailureSafely(ctx, classifyFailure, {
849
866
  runId,
@@ -870,7 +887,7 @@ function wireAcpLifecycle(ctx) {
870
887
  } });
871
888
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
872
889
  }
873
- else if (translation.kind === 'error') {
890
+ if (translation.kind === 'error') {
874
891
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
875
892
  }
876
893
  return;
@@ -942,7 +959,10 @@ function wirePiRpcLifecycle(ctx) {
942
959
  unsubscribeCancel();
943
960
  // Guarded for the same reasons as the child-driven handler above.
944
961
  await cleanupStagedFilesSafely(ctx);
945
- const status = await output.settle({ status: cancelRequested ? 'cancelled' : session?.hasFatalError() ? 'failed' : 'succeeded' });
962
+ let finalStatus = 'cancelled';
963
+ if (!cancelRequested)
964
+ finalStatus = session?.hasFatalError() ? 'failed' : 'succeeded';
965
+ const status = await output.settle({ status: finalStatus });
946
966
  const resumable = status === 'failed' && classifyFailure !== undefined
947
967
  ? await classifyFailureSafely(ctx, classifyFailure, {
948
968
  runId,
@@ -961,18 +981,14 @@ function wirePiRpcLifecycle(ctx) {
961
981
  const receipt = modelReceipt(translation.payload);
962
982
  if (receipt)
963
983
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: receipt } }));
964
- if (translation.sessionId !== undefined)
965
- capturedSessionId = translation.sessionId;
966
- if (translation.payload.type === 'tool_use') {
967
- toolCallSeen = true;
968
- }
969
- else if ((translation.payload.type === 'text_delta' || translation.payload.type === 'thinking_delta') &&
970
- translation.payload.delta.length > 0) {
971
- userVisibleOutputSeen = true;
972
- }
984
+ applyAgentTranslationSideEffects({ payload: translation.payload, sessionId: translation.sessionId, sink: {
985
+ onSessionId: ({ sessionId }) => { capturedSessionId = sessionId; },
986
+ onToolCall: () => { toolCallSeen = true; },
987
+ onUserVisibleOutput: () => { userVisibleOutputSeen = true; },
988
+ } });
973
989
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
974
990
  }
975
- else if (translation.kind === 'error') {
991
+ if (translation.kind === 'error') {
976
992
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
977
993
  }
978
994
  } }, { cwd: ctx.cwd, ...(ctx.model !== undefined ? { model: ctx.model } : {}), ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}), ...(ctx.uploadRoot !== undefined ? { uploadRoot: ctx.uploadRoot } : {}) });
@@ -1047,8 +1063,8 @@ export async function runAcpDispatch({ input, deps }) {
1047
1063
  // fired-and-forgotten (SEC-007: "await where lifecycle ordering allows it").
1048
1064
  await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'acp-attach-failure', deps.onCleanupFailure);
1049
1065
  await deps.releaseStagedResources();
1050
- // `failBeforeSpawn` always rejects (`Promise<never>`); returning it propagates that rejection.
1051
- return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
1066
+ // `failBeforeSpawn` always rejects (`Promise<never>`); awaiting it propagates that rejection.
1067
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
1052
1068
  }
1053
1069
  }
1054
1070
  /** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
@@ -1080,7 +1096,7 @@ export async function runPiRpcDispatch({ input, deps }) {
1080
1096
  // fire-and-forget (SEC-007).
1081
1097
  await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'pi-rpc-attach-failure', deps.onCleanupFailure);
1082
1098
  await deps.releaseStagedResources();
1083
- return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
1099
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
1084
1100
  }
1085
1101
  }
1086
1102
  //# sourceMappingURL=events.js.map
@@ -6,7 +6,7 @@ import { spawn as nodeSpawn, } from 'node:child_process';
6
6
  import { promises as fsPromises, } from 'node:fs';
7
7
  import { tmpdir, } from 'node:os';
8
8
  import { join, } from 'node:path';
9
- import { agentCapabilities, createClaudeFilesystemSupportProbe, claudeFilesystemRuntimePaths, claudeFilesystemSettings, validateFilesystemLaunch, validateReadOnlyExtraDirs, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
9
+ import { agentCapabilities, assessNativeToolPolicy, resolveNativeToolPolicy, createClaudeFilesystemSupportProbe, claudeFilesystemRuntimePaths, claudeFilesystemSettings, validateFilesystemLaunch, validateReadOnlyExtraDirs, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
10
10
  import { createCommandInvocation } from '@jini-ai/platform/command';
11
11
  import { collectProcessTreePids, listProcessSnapshots, stopProcesses, } from '@jini-ai/platform/process';
12
12
  import { applyImagePromptDelivery, } from '../image-prompt-delivery.js';
@@ -580,8 +580,8 @@ export async function guardWindowsCommandLineBudget({ input, deps }) {
580
580
  checkWindowsDirectExeCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args });
581
581
  if (windowsBudgetError) {
582
582
  await deps.releaseStagedResources();
583
- // `failBeforeSpawn` always rejects (`Promise<never>`); returning it propagates that rejection.
584
- return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
583
+ // `failBeforeSpawn` always rejects (`Promise<never>`); awaiting it propagates that rejection.
584
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
585
585
  }
586
586
  }
587
587
  /**
@@ -631,29 +631,13 @@ export async function confirmChildSpawned({ input, deps }) {
631
631
  }
632
632
  catch (err) {
633
633
  await deps.releaseStagedResources();
634
- return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
634
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
635
635
  }
636
636
  }
637
637
  /** Only include declared capability restrictions; absence has a distinct downstream contract. */
638
638
  function nativeCapabilityInput({ input }, _options = {}) {
639
639
  return { ...(input.nativeCapabilities !== undefined ? { nativeCapabilities: input.nativeCapabilities } : {}) };
640
640
  }
641
- /** Switch names a person sees for each native capability (the host's settings use the same words). */
642
- const NATIVE_CAPABILITY_LABELS = {
643
- shell: 'Bash/shell', fileWrite: 'File edit/write', userMcp: 'Your MCP servers', subagents: 'Subagents/Task',
644
- userSettings: 'Your skills and settings', web: 'Web fetch/search', workflows: 'Workflows', loginEnvironment: 'Your shell environment',
645
- };
646
- /** The refusal a person reads when a CLI without a native-tool policy cannot honor this run's switches:
647
- * it names the CLI and the switch to turn on. Owner 2026-10-27: the old "no native-tool policy and
648
- * audit adapter" told a Gemini user nothing. Pure, O(c) for c OFF switches.
649
- * @example nativeToolPolicyRefusal({ cliName: 'Antigravity', offSwitches: ['userMcp'] }, {});
650
- */
651
- function nativeToolPolicyRefusal({ cliName, offSwitches }, _options = {}) {
652
- const switches = offSwitches.map(key => `'${NATIVE_CAPABILITY_LABELS[key] ?? key}'`);
653
- const blocked = switches.length > 0 ? `with ${new Intl.ListFormat('en', { type: 'conjunction' }).format(switches)} off` : `with this chat's CLI tool limits`;
654
- const action = switches.length === 0 ? `Turn on 'Full CLI access'` : `Turn ${switches.length === 1 ? 'it' : 'them'} on`;
655
- return `${cliName} can't run ${blocked}. ${action} in the assistant settings, or choose Claude or Codex.`;
656
- }
657
641
  /** Correlation projects retained stdout, so exact-byte journals need a separate blob writer. */
658
642
  function validateStdoutJournalOptions(options, _options = {}) {
659
643
  if (options.delegatedResultCorrelation && options.journal && !options.stdoutJournalBlobs) {
@@ -709,13 +693,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
709
693
  const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
710
694
  const { onCleanupFailure: onCleanupFailureFn, bufferedStdoutMaxBytes } = resolveMiscExecutorDeps(options);
711
695
  validateStdoutJournalOptions(options);
712
- const journal = options.journal;
713
- const continuation = options.continuation;
714
- const classifyFailure = options.classifyFailure;
715
- const mcpJsonInjection = options.mcpJsonInjection;
716
- const claudeConfigDirIsolation = options.claudeConfigDirIsolation;
696
+ const { journal, continuation, classifyFailure, mcpJsonInjection, claudeConfigDirIsolation, promptAugmenter } = options;
717
697
  const claudeConfigDirIsolationEnabled = options.claudeConfigDirIsolationEnabled ?? false;
718
- const promptAugmenter = options.promptAugmenter;
719
698
  const filesystemTemps = new Map();
720
699
  const verifyClaudeFilesystem = options.assertClaudeFilesystemSupport ?? createClaudeFilesystemSupportProbe({});
721
700
  /** Remove this run's private temp directories on startup failure, exit, crash or cancellation.
@@ -765,7 +744,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
765
744
  /** `AgentExecutor.sendUserMessage()` — see that interface method's own doc. @complexity O(1). */
766
745
  async function sendUserMessage({ runId, text, id = randomUUID(), attachmentIds }, { imageContents } = {}) {
767
746
  const deliver = liveRunInputs.get(runId);
768
- return deliver ? deliver({ text, id, ...(attachmentIds ? { attachmentIds } : {}) }, imageContents ? { imageContents } : {}) : 'not-running';
747
+ if (!deliver)
748
+ return 'not-running';
749
+ return deliver({ text, id, ...(attachmentIds ? { attachmentIds } : {}) }, imageContents ? { imageContents } : {});
769
750
  }
770
751
  /** Validates launch input and selects the definition synchronously before yielding. */
771
752
  async function prepareRunContext({ requiredArgs, optionalArgs }, _options = {}) {
@@ -778,14 +759,13 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
778
759
  return { input, ...resolved };
779
760
  }
780
761
  async function enforceNativeToolPolicy({ input, def }, _options = {}) {
762
+ Object.assign(input, resolveNativeToolPolicy({ def, policy: input }));
781
763
  // Never silently ignore a host's locked native-tool policy on another CLI.
782
764
  // Parity cannot override an explicit lock or a restricted run.
783
765
  input.cliParity = input.cliParity === true && input.permissionMode === 'bypass' && input.nativeToolAccess !== false;
784
- if (def.supportsNativeToolAccess === true)
785
- return;
786
- const offSwitches = Object.entries(input.nativeCapabilities ?? {}).filter(([, value]) => value === false).map(([key]) => key);
787
- if (input.nativeToolAccess !== undefined && !input.cliParity || offSwitches.length > 0) {
788
- return failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED', message: nativeToolPolicyRefusal({ cliName: def.name, offSwitches }, {}) });
766
+ const refusal = assessNativeToolPolicy({ def, policy: input });
767
+ if (refusal !== null) {
768
+ await failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED', message: refusal });
789
769
  }
790
770
  }
791
771
  /** Validate every directory after image augmentation, before model probes or resource staging.
@@ -804,7 +784,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
804
784
  input.cwd = input.filesystemRoot;
805
785
  }
806
786
  catch {
807
- return failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_INVALID', message: 'Folder-limited launch refused: invalid filesystem root, working directory or attachment path.' });
787
+ await failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_INVALID', message: 'Folder-limited launch refused: invalid filesystem root, working directory or attachment path.' });
808
788
  }
809
789
  }
810
790
  /** Apply supported confinement without removing providers; unknown versions keep the settings.
@@ -829,12 +809,14 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
829
809
  spawnEnv.TMP = input.filesystemTempDir;
830
810
  spawnEnv.TEMP = input.filesystemTempDir;
831
811
  if (def.id === 'claude' && process.platform !== 'win32') {
832
- // Claude shortens long Bash socket paths to shared scratch. Keep even the /private alias
833
- // plus /claude-<uid> below 44 bytes, and isolate it with mkdtemp's private 0700 directory.
834
- const runtimeTemp = await fsPromises.mkdtemp('/tmp/jini-');
812
+ // Read denies also block create/unlink, so Claude's cwd files cannot live under /tmp.
813
+ // /var/tmp exists on macOS/Linux; its realpath plus /claude-<uid> must fit 44 bytes
814
+ // or the policy refuses launch instead of letting Bash fall back to shared scratch.
815
+ // mkdtemp creates a private 0700 directory; register cleanup before resolving aliases.
816
+ const runtimeTemp = await fsPromises.mkdtemp('/var/tmp/jini-');
835
817
  filesystemTemps.get(input.runId).push(runtimeTemp);
836
- input.filesystemRuntimeTempBase = runtimeTemp;
837
- spawnEnv.CLAUDE_CODE_TMPDIR = runtimeTemp;
818
+ input.filesystemRuntimeTempBase = await fsPromises.realpath(runtimeTemp);
819
+ spawnEnv.CLAUDE_CODE_TMPDIR = input.filesystemRuntimeTempBase;
838
820
  }
839
821
  if (supported === false) {
840
822
  input.filesystemNotice = 'files and shell are not folder-limited for this agent';
@@ -854,7 +836,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
854
836
  spawnEnv.CLAUDE_CODE_DISABLE_AUTO_MEMORY = '1';
855
837
  }
856
838
  catch {
857
- return failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_UNSUPPORTED', message: 'Folder-limited launch refused: invalid host settings or private runtime directory.' });
839
+ await failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_UNSUPPORTED', message: 'Folder-limited launch refused: invalid host settings or private runtime directory.' });
858
840
  }
859
841
  }
860
842
  function resolveRunOverlay({ input, def, imageDelivery }, _options = {}) {
@@ -1055,10 +1037,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1055
1037
  }
1056
1038
  /**
1057
1039
  * `AgentExecutor.run()` — see that interface method's own doc for the
1058
- * public contract. Implementation note on shape: every guard below
1059
- * returns `failBeforeSpawn(...)` directly (a `Promise<never>`, valid
1060
- * wherever `Promise<void>` is expected) rather than `await`-then-`return`,
1061
- * so each failure path reads as a single, obviously-terminal statement.
1040
+ * public contract. Implementation note on shape: every failure path below
1041
+ * awaits `failBeforeSpawn(...)` (a `Promise<never>`, so the await always
1042
+ * throws) and then returns, so each one reads as obviously terminal.
1062
1043
  * @param input - `{runId, agentId, prompt, cwd, model?, reasoning?, imagePaths?, extraAllowedDirs?, uploadRoot?, env?}` — `runId` must already be `lifecycle.start()`-ed.
1063
1044
  * @throws {@link AgentExecutorError} — see index.ts module doc's Invariant; never a bare `Error`.
1064
1045
  * @complexity O(1) setup (registry lookup, launch resolution, one spawn call); steady-state cost thereafter belongs to {@link wireChildLifecycle}.
@@ -1255,7 +1236,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1255
1236
  const spawnResult = spawnAgentChildProcess({ input: { cwd: input.cwd, childEnv, invocation }, deps: { spawn: spawnFn } });
1256
1237
  if (spawnResult.kind === 'error') {
1257
1238
  await releaseStagedResources();
1258
- return failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}` });
1239
+ await failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}` });
1240
+ return;
1259
1241
  }
1260
1242
  const child = spawnResult.child;
1261
1243
  // Registered before the spawn-confirmation await below, for the same reason
@@ -1282,10 +1264,12 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1282
1264
  // worse than releasing early (see `RuntimeLockHold.waitForHandoff`'s own doc).
1283
1265
  armHandoffWatcher({ runtimeLockHold: runtimeLockHold, handoffInput: { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, release: releaseRuntimeLock });
1284
1266
  if (streamFormat === 'acp-json-rpc') {
1285
- return dispatchAcpPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, mcpBridge, overlayDelivery }, {});
1267
+ await dispatchAcpPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, mcpBridge, overlayDelivery }, {});
1268
+ return;
1286
1269
  }
1287
1270
  if (streamFormat === 'pi-rpc') {
1288
- return dispatchPiPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, overlayDelivery }, {});
1271
+ await dispatchPiPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, overlayDelivery }, {});
1272
+ return;
1289
1273
  }
1290
1274
  dispatchStdinPrompt({ input, def, child, overlayDelivery, imageDelivery, stdinHandle }, {});
1291
1275
  }
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from './values.js';
2
+ import { omitKeys } from '../core/omit-keys.js';
2
3
  /** Parse only records; malformed stdout remains the stream parser's responsibility. */
3
4
  function frameRecord(line) {
4
5
  try {
@@ -39,8 +40,7 @@ export function createLiveSessionState({ emit }, _options = {}) {
39
40
  let backgroundTasks = 0;
40
41
  function publishTask(task) {
41
42
  tasks.set(task.id, task);
42
- const { local: _local, ...payload } = task;
43
- emit(payload);
43
+ emit(omitKeys(task, ['local']));
44
44
  }
45
45
  function taskDetails(frame, previous) {
46
46
  const patch = isRecord(frame.patch) ? frame.patch : frame;
@@ -276,6 +276,11 @@ export function buildMcpBridgeDelivery(input) {
276
276
  return { kind: 'codex-toml', serverEntry };
277
277
  case 'env-passthrough':
278
278
  return { kind: 'env-passthrough', serverEntry };
279
+ default: {
280
+ // Unreachable: every bridge strategy has a case above, and a new one fails typecheck here.
281
+ const unknownStrategy = strategy;
282
+ throw new TypeError(`Unknown MCP bridge strategy: ${String(unknownStrategy)}`);
283
+ }
279
284
  }
280
285
  }
281
286
  function bridgeCredentialOptions(input) {
@@ -26,7 +26,8 @@ export function createMessageRelay({ emit }, { maxChars = 32_000 } = {}) {
26
26
  enqueue({ id, text, hasAttachments }) {
27
27
  if (closed)
28
28
  return 'not-running';
29
- pending.push({ id, text, hasAttachments, seq: ++seq });
29
+ seq += 1;
30
+ pending.push({ id, text, hasAttachments, seq });
30
31
  emit({ type: 'user_message', id, text, deliveryState: 'queued' });
31
32
  return 'delivered';
32
33
  },
@@ -36,11 +37,9 @@ export function createMessageRelay({ emit }, { maxChars = 32_000 } = {}) {
36
37
  let count = 0;
37
38
  let chars = 0;
38
39
  for (const message of pending) {
39
- if (message.hasAttachments)
40
- break;
41
- const size = JSON.stringify({ id: message.id, seq: message.seq, text: message.text }).length;
42
- // Whole messages only; a single oversized message is delivered alone.
43
- if (count > 0 && chars + size > maxChars)
40
+ // Whole messages only, stopping at an attachment; a single oversized message is delivered alone.
41
+ const size = message.hasAttachments ? 0 : JSON.stringify({ id: message.id, seq: message.seq, text: message.text }).length;
42
+ if (message.hasAttachments || (count > 0 && chars + size > maxChars))
44
43
  break;
45
44
  chars += size;
46
45
  count++;
@@ -1,7 +1,12 @@
1
1
  import { redactSecrets } from '@jini-ai/core';
2
2
  // Only a terminal-prefixed failure overrides real output; ordinary assistant explanations
3
3
  // can mention empty output or headless permissions without being failed runs themselves.
4
- const DIAGNOSTIC_LINE = /^(?:jetski|agy|error|fatal):\s*(?:no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)|(?:error:\s*)?permission denied\b.*headless|headless\b.*auto-denied)/i;
4
+ // One pattern per diagnostic; a line is a diagnostic when any of them matches.
5
+ const DIAGNOSTIC_LINES = [
6
+ /^(?:jetski|agy|error|fatal):\s*no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)/i,
7
+ /^(?:jetski|agy|error|fatal):\s*(?:error:\s*)?permission denied\b.*headless/i,
8
+ /^(?:jetski|agy|error|fatal):\s*headless\b.*auto-denied/i,
9
+ ];
5
10
  /** Records whether the event is assistant text, tool activity or a run error. */
6
11
  function noteActivity(seen, event) {
7
12
  if (event.kind === 'error')
@@ -23,7 +28,7 @@ function noteDiagnostic({ seen, tails, event, plainStdout }) {
23
28
  return;
24
29
  const channel = event.kind === 'stderr' ? 'stderr' : 'stdout';
25
30
  tails[channel] = (tails[channel] + event.payload.chunk).slice(-8192);
26
- const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINE.test(value.trim()));
31
+ const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINES.some(pattern => pattern.test(value.trim())));
27
32
  if (line)
28
33
  seen.diagnostic = line;
29
34
  }
@@ -30,6 +30,11 @@ export function classifyRunCloseStatus(requiredArgs, optionalArgs = {}) {
30
30
  }
31
31
  return 'failed';
32
32
  }
33
+ /** A set environment value parsed as a positive, finite millisecond count, else undefined. */
34
+ function positiveTimeout(raw) {
35
+ const value = raw !== undefined ? Number(raw) : undefined;
36
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
37
+ }
33
38
  /**
34
39
  * Resolves a timeout in milliseconds from an env-var override, an
35
40
  * agent-supplied default, and a kernel default, in that precedence order,
@@ -48,8 +53,7 @@ export function resolveTimeoutMs(requiredArgs, optionalArgs = {}) {
48
53
  const input = { ...requiredArgs, ...optionalArgs };
49
54
  const env = input.env ?? (typeof process !== 'undefined' ? process.env : {});
50
55
  const rawFromEnv = input.envVar ? env[input.envVar] : undefined;
51
- const fromEnv = rawFromEnv !== undefined ? Number(rawFromEnv) : undefined;
52
- const candidate = fromEnv !== undefined && Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : (input.agentDefaultMs ?? input.defaultMs);
56
+ const candidate = positiveTimeout(rawFromEnv) ?? input.agentDefaultMs ?? input.defaultMs;
53
57
  return Math.min(candidate, input.maxMs);
54
58
  }
55
59
  /**
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
3
+ * without binding a throwaway name for every dropped key.
4
+ */
5
+ /** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
6
+ export declare function omitKeys<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Omit<T, K>;
7
+ //# sourceMappingURL=omit-keys.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
3
+ * without binding a throwaway name for every dropped key.
4
+ */
5
+ /** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
6
+ export function omitKeys(source, keys) {
7
+ const copy = { ...source };
8
+ for (const key of keys)
9
+ delete copy[key];
10
+ return copy;
11
+ }
12
+ //# sourceMappingURL=omit-keys.js.map