@jini-ai/daemon 0.6.17 → 0.6.22

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 (78) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent-executor/claude/mcp-config.d.ts +17 -0
  3. package/dist/agent-executor/claude/mcp-config.js +29 -0
  4. package/dist/agent-executor/codex/home-config.d.ts +7 -6
  5. package/dist/agent-executor/codex/home-config.js +15 -3
  6. package/dist/agent-executor/codex/home-staging.d.ts +4 -2
  7. package/dist/agent-executor/codex/home-staging.js +5 -3
  8. package/dist/agent-executor/contracts.d.ts +15 -0
  9. package/dist/agent-executor/event-translation.js +3 -0
  10. package/dist/agent-executor/events.d.ts +6 -4
  11. package/dist/agent-executor/events.js +29 -7
  12. package/dist/agent-executor/gemini/tool-calls.d.ts +7 -0
  13. package/dist/agent-executor/gemini/tool-calls.js +9 -0
  14. package/dist/agent-executor/http-mcp.d.ts +23 -0
  15. package/dist/agent-executor/http-mcp.js +74 -0
  16. package/dist/agent-executor/launch.js +43 -11
  17. package/dist/agent-executor/mcp-bridge.d.ts +10 -2
  18. package/dist/agent-executor/mcp-bridge.js +2 -2
  19. package/dist/agent-executor/opencode/mcp-config.d.ts +10 -0
  20. package/dist/agent-executor/opencode/mcp-config.js +11 -0
  21. package/dist/agent-executor/opencode/tool-calls.d.ts +7 -0
  22. package/dist/agent-executor/opencode/tool-calls.js +12 -0
  23. package/dist/agent-executor/sanitized-line-stream.d.ts +24 -0
  24. package/dist/agent-executor/sanitized-line-stream.js +45 -0
  25. package/dist/agent-executor/tool-call-receipts.d.ts +7 -0
  26. package/dist/agent-executor/tool-call-receipts.js +9 -0
  27. package/dist/core/read-only-tools.d.ts +2 -0
  28. package/dist/core/read-only-tools.js +19 -3
  29. package/dist/delegated-tool-bridge.js +6 -1
  30. package/dist/http/attachments.d.ts +2 -101
  31. package/dist/http/attachments.js +13 -1
  32. package/dist/http/connectors.js +3 -1
  33. package/dist/http/db-ops.js +3 -1
  34. package/dist/http/delegated-tools.d.ts +13 -1
  35. package/dist/http/delegated-tools.js +23 -6
  36. package/dist/http/frontend-control.d.ts +0 -36
  37. package/dist/http/frontend-control.js +39 -1
  38. package/dist/http/frontend-sessions.js +4 -2
  39. package/dist/http/index.d.ts +2 -1
  40. package/dist/http/index.js +1 -0
  41. package/dist/http/media.js +3 -1
  42. package/dist/http/model-proxy.js +4 -2
  43. package/dist/http/remote-run-events.js +3 -1
  44. package/dist/http/research.js +3 -2
  45. package/dist/http/runs.js +3 -1
  46. package/dist/http/surface-restoration.d.ts +17 -0
  47. package/dist/http/surface-restoration.js +50 -0
  48. package/dist/http/terminals.js +3 -1
  49. package/dist/http/tool-catalog.d.ts +15 -1
  50. package/dist/http/tool-execution-routes.d.ts +3 -1
  51. package/dist/http/tool-execution-routes.js +4 -3
  52. package/dist/http/xai.js +3 -2
  53. package/dist/idle-monitor.d.ts +29 -0
  54. package/dist/idle-monitor.js +64 -0
  55. package/dist/image-prompt-delivery.d.ts +1 -1
  56. package/dist/image-prompt-delivery.js +4 -3
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +1 -0
  59. package/dist/legacy-data-migration.js +3 -1
  60. package/dist/remote-attachment-lookup.d.ts +3 -1
  61. package/dist/remote-attachment-lookup.js +2 -2
  62. package/dist/routines/scheduler.js +6 -4
  63. package/dist/routing-tool-executor.d.ts +6 -1
  64. package/dist/routing-tool-executor.js +15 -2
  65. package/dist/run-lifecycle.d.ts +0 -16
  66. package/dist/run-lifecycle.js +24 -6
  67. package/dist/surface-exchanges.d.ts +5 -1
  68. package/dist/surface-exchanges.js +41 -7
  69. package/dist/surface-restoration.d.ts +38 -0
  70. package/dist/surface-restoration.js +152 -0
  71. package/dist/tool-recovery/index.d.ts +1 -0
  72. package/dist/tool-recovery/index.js +1 -0
  73. package/dist/tool-recovery/redaction.d.ts +1 -0
  74. package/dist/tool-recovery/redaction.js +4 -1
  75. package/dist/tool-recovery/stack.js +2 -1
  76. package/dist/tool-recovery/surface-lifetime.d.ts +11 -0
  77. package/dist/tool-recovery/surface-lifetime.js +41 -0
  78. package/package.json +14 -14
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.6.22
2
+
3
+ - Child stdout can be framed and sanitized line by line before parsing. A timed-out or cancelled delegated tool can report host-loaded recovery facts through `recoverInterruptedTool`, with no retry.
4
+
5
+ ## 0.6.19
6
+
7
+ - `DelegatedToolsHttpDeps.toolRegistry` takes a list-only registry view (`Pick<ToolRegistry, 'list'>`): the route only reads descriptors, and hosts pass descriptor-only facades.
8
+ - Redact exception logs with the shared core redactor while preserving correlation IDs and error codes.
9
+ - Redact host-supplied paths and payload names in the default migration logger; preserve bind-error callback object assertions.
10
+
1
11
  ## 0.6.17
2
12
 
3
13
  ### Patch Changes
@@ -0,0 +1,17 @@
1
+ import type { McpHttpEndpoint, McpJsonInjectionOptions } from '../contracts.js';
2
+ /** Stage Claude's HTTP capability outside the project; only this private path enters argv.
3
+ * @returns A private config path and removal of only its owned directory.
4
+ * @throws Filesystem staging errors after removing partial output.
5
+ * @complexity O(config bytes), one project read and one private config write.
6
+ */
7
+ export declare function prepareClaudeHttpMcpConfig({ cwd, endpoint }: {
8
+ cwd: string;
9
+ endpoint: Pick<McpHttpEndpoint, 'url' | 'token'>;
10
+ }, { inheritUserMcp, readFile }?: {
11
+ inheritUserMcp?: boolean;
12
+ readFile?: McpJsonInjectionOptions['readFile'];
13
+ }): Promise<{
14
+ path: string;
15
+ cleanup: () => Promise<void>;
16
+ }>;
17
+ //# sourceMappingURL=mcp-config.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { defaultReadMcpJsonFile, mergeMcpJsonContent } from '../mcp-bridge.js';
5
+ /** Stage Claude's HTTP capability outside the project; only this private path enters argv.
6
+ * @returns A private config path and removal of only its owned directory.
7
+ * @throws Filesystem staging errors after removing partial output.
8
+ * @complexity O(config bytes), one project read and one private config write.
9
+ */
10
+ export async function prepareClaudeHttpMcpConfig({ cwd, endpoint }, { inheritUserMcp = true, readFile = defaultReadMcpJsonFile } = {}) {
11
+ const dir = await fs.mkdtemp(join(tmpdir(), 'jini-claude-mcp-'));
12
+ const cleanup = () => fs.rm(dir, { recursive: true, force: true });
13
+ try {
14
+ let existingRaw;
15
+ if (inheritUserMcp)
16
+ existingRaw = await readFile({ path: join(cwd, '.mcp.json') }).catch(() => undefined);
17
+ const content = mergeMcpJsonContent({ existingRaw, serverEntry: {
18
+ type: 'http', url: endpoint.url, headers: { Authorization: `Bearer ${endpoint.token}` },
19
+ } });
20
+ const path = join(dir, 'mcp.json');
21
+ await fs.writeFile(path, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
22
+ return { path, cleanup };
23
+ }
24
+ catch (error) {
25
+ await cleanup();
26
+ throw error;
27
+ }
28
+ }
29
+ //# sourceMappingURL=mcp-config.js.map
@@ -1,23 +1,24 @@
1
- import type { McpJsonServerEntry } from '../contracts.js';
1
+ import type { McpHttpEndpoint, McpJsonServerEntry } from '../contracts.js';
2
2
  /** A TOML key: bare when TOML allows it, quoted otherwise — host-supplied bridge env names are not guaranteed bare-safe. */
3
3
  export declare function tomlKey(key: string): string;
4
4
  /**
5
5
  * Mechanism 5 of 5 — `'codex-toml'`'s serialization step. Builds the `[mcp_servers.jini]` TOML
6
6
  * table (plus, when the entry carries any env vars, a separate `[mcp_servers.jini.env]` table)
7
- * Codex's own config schema expects.
7
+ * Codex's own config schema expects. An explicit HTTP endpoint instead writes `url` and
8
+ * literal `http_headers` through the same table replacement and escaping owners.
8
9
  *
9
10
  * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
10
11
  * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
11
12
  * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
12
13
  * table, env vars in a nested `.env` table) — see `archived provenance ledger` for the transcript.
13
- * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
14
+ * @param entry - A stdio bridge entry or an explicit run HTTP endpoint.
14
15
  * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
15
16
  * @complexity O(n) in the number of argv/env entries.
16
17
  * @overallScore 100/100
17
18
  */
18
19
  export declare function buildCodexMcpServerToml({ entry }: {
19
- readonly entry: McpJsonServerEntry;
20
- }): string;
20
+ readonly entry: McpJsonServerEntry | Pick<McpHttpEndpoint, 'url' | 'token'>;
21
+ }, _options?: {}): string;
21
22
  /**
22
23
  * Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
23
24
  * config, with any pre-existing `[mcp_servers.jini]` table removed (see
@@ -43,7 +44,7 @@ export declare function buildCodexMcpServerToml({ entry }: {
43
44
  */
44
45
  export declare function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings }: {
45
46
  readonly existingRaw: string | undefined;
46
- readonly entry: McpJsonServerEntry;
47
+ readonly entry: McpJsonServerEntry | Pick<McpHttpEndpoint, 'url' | 'token'>;
47
48
  readonly inheritUserMcp?: boolean;
48
49
  readonly inheritUserSettings?: boolean;
49
50
  }, _optional?: Record<string, never>): string;
@@ -44,18 +44,21 @@ const CODEX_TOOL_TIMEOUT_SEC = 6 * 60 + 40;
44
44
  /**
45
45
  * Mechanism 5 of 5 — `'codex-toml'`'s serialization step. Builds the `[mcp_servers.jini]` TOML
46
46
  * table (plus, when the entry carries any env vars, a separate `[mcp_servers.jini.env]` table)
47
- * Codex's own config schema expects.
47
+ * Codex's own config schema expects. An explicit HTTP endpoint instead writes `url` and
48
+ * literal `http_headers` through the same table replacement and escaping owners.
48
49
  *
49
50
  * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
50
51
  * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
51
52
  * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
52
53
  * table, env vars in a nested `.env` table) — see `archived provenance ledger` for the transcript.
53
- * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
54
+ * @param entry - A stdio bridge entry or an explicit run HTTP endpoint.
54
55
  * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
55
56
  * @complexity O(n) in the number of argv/env entries.
56
57
  * @overallScore 100/100
57
58
  */
58
- export function buildCodexMcpServerToml({ entry }) {
59
+ export function buildCodexMcpServerToml({ entry }, _options = {}) {
60
+ if ('url' in entry)
61
+ return buildCodexHttpMcpServerToml({ endpoint: entry });
59
62
  const argsLiteral = entry.args.map(tomlString).join(', ');
60
63
  const serverTable = `[mcp_servers.${JINI_MCP_SERVER_KEY}]\ncommand = ${tomlString(entry.command)}\nargs = [${argsLiteral}]\n` +
61
64
  `tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n`;
@@ -66,6 +69,15 @@ export function buildCodexMcpServerToml({ entry }) {
66
69
  return serverTable;
67
70
  return `${serverTable}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.env]\n${envLines.join('\n')}\n`;
68
71
  }
72
+ /** Literal headers keep the run token out of argv and bearer environment variables.
73
+ * @returns One HTTP table using the existing timeout and TOML escaping owner.
74
+ * @complexity O(endpoint bytes), without I/O.
75
+ */
76
+ function buildCodexHttpMcpServerToml({ endpoint }, _options = {}) {
77
+ return `[mcp_servers.${JINI_MCP_SERVER_KEY}]\nurl = ${tomlString(endpoint.url)}\n` +
78
+ `tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n\n[mcp_servers.${JINI_MCP_SERVER_KEY}.http_headers]\n` +
79
+ `Authorization = ${tomlString(`Bearer ${endpoint.token}`)}\n`;
80
+ }
69
81
  /**
70
82
  * Splits a TOML table header's dotted key into its individual segments, honoring quoted parts
71
83
  * (`"basic"` or `'literal'`) that may themselves contain a literal `.` — a bare `.` only
@@ -1,5 +1,5 @@
1
1
  import type { RuntimeAgentDef } from '@jini-ai/agent-runtime';
2
- import type { McpJsonInjectionOptions, McpBridgeDelivery, PreparedCodexHome, FailBeforeSpawn } from '../contracts.js';
2
+ import type { McpJsonInjectionOptions, McpHttpEndpoint, McpBridgeDelivery, PreparedCodexHome, FailBeforeSpawn } from '../contracts.js';
3
3
  /**
4
4
  * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
5
5
  * never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
@@ -37,7 +37,8 @@ export declare function codexInheritance(input: {
37
37
  * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
38
38
  * mechanism, or for an unconfigured host — matching {@link writeMcpJsonIfNeeded}'s identical gate).
39
39
  * @param input.def - Only used for its `id`, in the failure message.
40
- * @param input.mcpBridge - This run's resolved bridge delivery — a no-op unless its `kind` is `'codex-toml'`.
40
+ * @param input.mcpBridge - Existing stdio delivery; HTTP uses the optional `httpEndpoint` instead.
41
+ * @param input.httpEndpoint - Explicit run listener, using the same isolation, state links and cleanup as stdio.
41
42
  * @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceCodexHomeDir} rather than read from a module-level `process.env` so this phase stays testable with an injected env.
42
43
  * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
43
44
  * @overallScore 100/100
@@ -49,6 +50,7 @@ export declare function prepareCodexHomeIfNeeded({ input, deps }: {
49
50
  readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
50
51
  readonly def: RuntimeAgentDef;
51
52
  readonly mcpBridge: McpBridgeDelivery | null;
53
+ readonly httpEndpoint?: McpHttpEndpoint;
52
54
  };
53
55
  readonly deps: {
54
56
  readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
@@ -165,13 +165,15 @@ export function codexInheritance(input) {
165
165
  * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
166
166
  * mechanism, or for an unconfigured host — matching {@link writeMcpJsonIfNeeded}'s identical gate).
167
167
  * @param input.def - Only used for its `id`, in the failure message.
168
- * @param input.mcpBridge - This run's resolved bridge delivery — a no-op unless its `kind` is `'codex-toml'`.
168
+ * @param input.mcpBridge - Existing stdio delivery; HTTP uses the optional `httpEndpoint` instead.
169
+ * @param input.httpEndpoint - Explicit run listener, using the same isolation, state links and cleanup as stdio.
169
170
  * @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceCodexHomeDir} rather than read from a module-level `process.env` so this phase stays testable with an injected env.
170
171
  * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
171
172
  * @overallScore 100/100
172
173
  */
173
174
  export async function prepareCodexHomeIfNeeded({ input, deps }) {
174
- if (input.mcpBridge?.kind !== 'codex-toml' || deps.mcpJsonInjection === undefined) {
175
+ const entry = input.httpEndpoint ?? (input.mcpBridge?.kind === 'codex-toml' ? input.mcpBridge.serverEntry : undefined);
176
+ if (entry === undefined || deps.mcpJsonInjection === undefined) {
175
177
  if (codexTurnsOffPersonalConfig(input)) {
176
178
  await deps.releaseStagedResources();
177
179
  return deps.failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED',
@@ -181,7 +183,7 @@ export async function prepareCodexHomeIfNeeded({ input, deps }) {
181
183
  }
182
184
  const inherit = codexInheritance(input);
183
185
  try {
184
- return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection), inherit.settings, inherit.mcp);
186
+ return await prepareCodexHomeForRun(input.runId, entry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection), inherit.settings, inherit.mcp);
185
187
  }
186
188
  catch (err) {
187
189
  await deps.releaseStagedResources();
@@ -344,7 +344,22 @@ export interface ContinuationOptions {
344
344
  * loopback URL this package could assume on a caller's behalf (matching every other seam on this
345
345
  * interface that defaults to *nothing* rather than a real implementation).
346
346
  */
347
+ /** A private per-run listener; its token authorizes this endpoint, never daemon callbacks. */
348
+ export interface McpHttpEndpoint {
349
+ readonly url: string;
350
+ readonly token: string;
351
+ close(required: Record<string, never>, optional?: Record<string, never>): Promise<void>;
352
+ }
347
353
  export interface McpJsonInjectionOptions {
354
+ /** Optional transport port. Reject before CLI launch when HTTP cannot be prepared.
355
+ * The host keeps callback credentials in memory; only this endpoint capability reaches the CLI. */
356
+ readonly prepareHttp?: (required: {
357
+ readonly runId: string;
358
+ }, optional?: {
359
+ readonly messageRelayNonce?: string;
360
+ readonly startupToolIds?: readonly string[];
361
+ readonly diagnosticToolIds?: readonly string[];
362
+ }) => Promise<McpHttpEndpoint>;
348
363
  /** Read current host-registered tool IDs at launch, outside model-controlled input. */
349
364
  readonly diagnosticToolIds?: () => readonly string[];
350
365
  /**
@@ -86,6 +86,9 @@ function translateToolUseEvent(rawEvent) {
86
86
  id: asString(rawEvent.id),
87
87
  name: asString(rawEvent.name),
88
88
  input: rawEvent.input ?? null,
89
+ ...(isRecord(rawEvent.bridgeCall) && typeof rawEvent.bridgeCall.name === 'string'
90
+ ? { bridgeCall: { name: rawEvent.bridgeCall.name, input: rawEvent.bridgeCall.input,
91
+ ...(rawEvent.bridgeCall.buffered === true ? { buffered: true } : {}) } } : {}),
89
92
  },
90
93
  };
91
94
  }
@@ -11,7 +11,7 @@ import type { ChildDrivenStreamFormat, TerminateChildTreeDeps, AgentCleanupFailu
11
11
  /** Default sink when a host does not supply `onCleanupFailure`: still observable, never silent. Redacted per SEC-007 (a spawn/permission error can embed paths/host detail). */
12
12
  export declare function defaultCleanupFailureSink(context: AgentCleanupFailureContext): void;
13
13
  /**
14
- * Default ceiling on the `'until-close'` stdout accumulator (see `RuntimeStdoutPolicy` in
14
+ * Default ceiling on the `'until-close'` or per-line stdout accumulator (see `RuntimeStdoutPolicy` in
15
15
  * `@jini-ai/agent-runtime`), in bytes of received UTF-8.
16
16
  *
17
17
  * A buffered def holds its child's entire stdout in one in-memory string until the process closes,
@@ -20,8 +20,8 @@ export declare function defaultCleanupFailureSink(context: AgentCleanupFailureCo
20
20
  * nothing obliges it to ever close or to stop emitting. Without a ceiling one run could exhaust the
21
21
  * daemon's heap and take every unrelated run in the process down with it.
22
22
  *
23
- * 8 MiB is chosen to sit far above any real buffered-agent transcript (antigravity's print-mode
24
- * output — the only `'until-close'` def — is a few KiB of auth prompt and result text) while staying
23
+ * 8 MiB was chosen to sit far above the original buffered print-mode auth prompt and result text
24
+ * (a few KiB) while staying
25
25
  * small enough that a hostile child cannot meaningfully pressure the heap. A host that genuinely
26
26
  * needs more passes `CreateAgentExecutorOptions.bufferedStdoutMaxBytes`.
27
27
  */
@@ -101,13 +101,15 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
101
101
  *
102
102
  * *When* those chunks leave is the def's call, via `def.stdoutPolicy`:
103
103
  *
104
- * - `'live'` (the default, and every def but antigravity) — emit per
104
+ * - `'live'` (the default) — emit per
105
105
  * chunk, as it arrives.
106
106
  * - `'until-close'` — accumulate, and emit the whole thing exactly once
107
107
  * from the `close` handler, after `def.stdoutPolicy.sanitize`. For an
108
108
  * adapter that can print a secret to stdout and still exit 0, no
109
109
  * per-chunk decision is safe: the pattern to redact can straddle two
110
110
  * `'data'` events.
111
+ * - `'line'` — sanitize each bounded complete wire line before both raw delivery and parsing;
112
+ * retain only its partial tail. Secrets split across chunks stay private without waiting for exit.
111
113
  *
112
114
  * The buffered path holds back the raw `'stdout'` echo too, not just the
113
115
  * `'agent'`/`text_delta`, and sanitizes both. Emitting an unsanitized raw
@@ -1,6 +1,7 @@
1
1
  import { createToolResultDiagnosticProjection, projectDiagnosticFrame } from '../tool-result-diagnostics.js';
2
2
  import { appendSerialTask } from "@jini-ai/core/primitives";
3
3
  import { createLiveSessionState } from './live-session-state.js';
4
+ import { createSanitizedLineStream } from './sanitized-line-stream.js';
4
5
  import { acceptsMidRunUserMessages, createMessageRelay } from './message-relay.js';
5
6
  import { randomUUID } from 'node:crypto';
6
7
  import { createModelReceiptTracker } from './model-receipts.js';
@@ -13,6 +14,7 @@ import { classifyRunCloseStatus, } from '../close-status.js';
13
14
  import { resolveContinuationTransport, } from '../continuation/continuation-transport.js';
14
15
  import { resultContent, } from '../delegated-tool-bridge.js';
15
16
  import { projectToolResultEvent, } from '../tool-result-media.js';
17
+ import { createCliToolCallReceiver } from './tool-call-receipts.js';
16
18
  import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
17
19
  import { errorMessage, } from './values.js';
18
20
  import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
@@ -45,7 +47,7 @@ function createStreamHandlerForDef(def, streamFormat, onEvent) {
45
47
  case 'qoder-stream-json':
46
48
  return createQoderStreamHandler({ onEvent: onEvent });
47
49
  case 'json-event-stream':
48
- return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: onEvent });
50
+ return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: createCliToolCallReceiver({ kind: def.eventParser ?? '', onEvent }) });
49
51
  default: {
50
52
  // Unreachable: every stream format has a case above, and a new one fails typecheck here.
51
53
  const unknownFormat = streamFormat;
@@ -185,7 +187,7 @@ function receivedJournalEntry(channel, content) {
185
187
  return { content, provenance: { source: 'agent', channel }, trust: 'untrusted' };
186
188
  }
187
189
  /**
188
- * Default ceiling on the `'until-close'` stdout accumulator (see `RuntimeStdoutPolicy` in
190
+ * Default ceiling on the `'until-close'` or per-line stdout accumulator (see `RuntimeStdoutPolicy` in
189
191
  * `@jini-ai/agent-runtime`), in bytes of received UTF-8.
190
192
  *
191
193
  * A buffered def holds its child's entire stdout in one in-memory string until the process closes,
@@ -194,8 +196,8 @@ function receivedJournalEntry(channel, content) {
194
196
  * nothing obliges it to ever close or to stop emitting. Without a ceiling one run could exhaust the
195
197
  * daemon's heap and take every unrelated run in the process down with it.
196
198
  *
197
- * 8 MiB is chosen to sit far above any real buffered-agent transcript (antigravity's print-mode
198
- * output — the only `'until-close'` def — is a few KiB of auth prompt and result text) while staying
199
+ * 8 MiB was chosen to sit far above the original buffered print-mode auth prompt and result text
200
+ * (a few KiB) while staying
199
201
  * small enough that a hostile child cannot meaningfully pressure the heap. A host that genuinely
200
202
  * needs more passes `CreateAgentExecutorOptions.bufferedStdoutMaxBytes`.
201
203
  */
@@ -282,13 +284,15 @@ function createStdoutDiagnostics({ ctx, enqueueEmit }, _options = {}) {
282
284
  *
283
285
  * *When* those chunks leave is the def's call, via `def.stdoutPolicy`:
284
286
  *
285
- * - `'live'` (the default, and every def but antigravity) — emit per
287
+ * - `'live'` (the default) — emit per
286
288
  * chunk, as it arrives.
287
289
  * - `'until-close'` — accumulate, and emit the whole thing exactly once
288
290
  * from the `close` handler, after `def.stdoutPolicy.sanitize`. For an
289
291
  * adapter that can print a secret to stdout and still exit 0, no
290
292
  * per-chunk decision is safe: the pattern to redact can straddle two
291
293
  * `'data'` events.
294
+ * - `'line'` — sanitize each bounded complete wire line before both raw delivery and parsing;
295
+ * retain only its partial tail. Secrets split across chunks stay private without waiting for exit.
292
296
  *
293
297
  * The buffered path holds back the raw `'stdout'` echo too, not just the
294
298
  * `'agent'`/`text_delta`, and sanitizes both. Emitting an unsanitized raw
@@ -363,12 +367,25 @@ export function wireChildLifecycle(ctx) {
363
367
  let bridgeChecked = !ctx.expectsJiniBridge;
364
368
  let bridgeUnavailable = false;
365
369
  // `def.stdoutPolicy` read once, up front, so the per-chunk handler below is a single boolean
366
- // test rather than a repeated union narrowing. `undefined` (every def but antigravity) means
370
+ // test rather than a repeated union narrowing. `undefined` means
367
371
  // live — see this function's own doc.
368
372
  const stdoutPolicy = def.stdoutPolicy;
369
373
  const bufferStdoutUntilClose = stdoutPolicy?.buffering === 'until-close';
370
374
  const sanitizeBufferedStdout = stdoutPolicy?.buffering === 'until-close' ? stdoutPolicy.sanitize : undefined;
371
- // Accumulator for the `'until-close'` path. Stays `''` for every live def, and the flush below
375
+ const lineStdout = stdoutPolicy?.buffering === 'line' ? createSanitizedLineStream({
376
+ maxBytes: ctx.bufferedStdoutMaxBytes, sanitize: stdoutPolicy.sanitize,
377
+ emit: ({ text }) => {
378
+ stdoutDiagnostics.feed({ text });
379
+ if (streamFormat === 'plain')
380
+ enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
381
+ else
382
+ streamHandler?.feed({ chunk: text });
383
+ },
384
+ onOverflow: ({ bytes }) => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'error', data: {
385
+ message: bufferedStdoutTruncationNotice(bytes, ctx.bufferedStdoutMaxBytes),
386
+ } } })),
387
+ }) : undefined;
388
+ // Accumulator for the `'until-close'` path. Stays `''` for live and line policies, and the flush below
372
389
  // is then a no-op that emits nothing. Bounded by `ctx.bufferedStdoutMaxBytes` — see
373
390
  // {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES} for why an unbounded accumulator was a
374
391
  // denial-of-service surface rather than merely untidy.
@@ -646,6 +663,10 @@ export function wireChildLifecycle(ctx) {
646
663
  child.stdout?.on('data', (chunk) => {
647
664
  const text = chunk.toString('utf8');
648
665
  stdoutDiagnostics.record({ chunk, text });
666
+ if (lineStdout) {
667
+ lineStdout.feed({ chunk: text });
668
+ return;
669
+ }
649
670
  if (streamFormat === 'plain' && text.length > 0)
650
671
  userVisibleOutputSeen = true;
651
672
  if (bufferStdoutUntilClose) {
@@ -715,6 +736,7 @@ export function wireChildLifecycle(ctx) {
715
736
  controlScanBuffer = '';
716
737
  }
717
738
  flushBufferedStdout();
739
+ lineStdout?.flush({});
718
740
  streamHandler?.flush();
719
741
  session.endSession();
720
742
  stdoutDiagnostics.flush();
@@ -0,0 +1,7 @@
1
+ /** Gemini's legacy stream reports bare MCP names, indistinguishable from native tool names.
2
+ * Correlation is only a candidate: the shared fold requires a real bridge row with equal input.
3
+ * Keep native names/arguments intact for presentation and auditing. Pure, O(1). */
4
+ export declare function geminiToolCallReceipt({ event }: {
5
+ event: Record<string, unknown>;
6
+ }, _options?: {}): Record<string, unknown>;
7
+ //# sourceMappingURL=tool-calls.d.ts.map
@@ -0,0 +1,9 @@
1
+ /** Gemini's legacy stream reports bare MCP names, indistinguishable from native tool names.
2
+ * Correlation is only a candidate: the shared fold requires a real bridge row with equal input.
3
+ * Keep native names/arguments intact for presentation and auditing. Pure, O(1). */
4
+ export function geminiToolCallReceipt({ event }, _options = {}) {
5
+ if (event.type !== 'tool_use' || typeof event.name !== 'string' || event.name.startsWith('mcp__'))
6
+ return event;
7
+ return { ...event, bridgeCall: { name: event.name, input: event.input } };
8
+ }
9
+ //# sourceMappingURL=tool-calls.js.map
@@ -0,0 +1,23 @@
1
+ import type { RuntimeAgentDef } from '@jini-ai/agent-runtime';
2
+ import type { RunLifecycle } from '../run-lifecycle.js';
3
+ import type { McpHttpEndpoint, McpJsonInjectionOptions } from './contracts.js';
4
+ /** Owned transport resources; cleanup is idempotent and also armed on cancellation/termination. */
5
+ export interface PreparedHttpMcp {
6
+ readonly endpoint: McpHttpEndpoint;
7
+ readonly mcpJsonPath?: string;
8
+ cleanup(): Promise<void>;
9
+ }
10
+ /** Prepare HTTP before launch; only missing support or listener failure selects stdio.
11
+ * @returns Null for stdio, otherwise a run-owned endpoint and optional private Claude config.
12
+ * @throws Configuration staging errors after closing the endpoint; never retries dispatched work.
13
+ * @complexity O(config bytes) for Claude; O(1) otherwise, plus the injected host preparation.
14
+ */
15
+ export declare function prepareHttpMcpForRun({ runId, cwd, def, lifecycle, injection, inheritUserMcp }: {
16
+ runId: string;
17
+ cwd: string;
18
+ def: RuntimeAgentDef;
19
+ lifecycle: RunLifecycle;
20
+ injection: McpJsonInjectionOptions | undefined;
21
+ inheritUserMcp?: boolean;
22
+ }, _options?: {}): Promise<PreparedHttpMcp | null>;
23
+ //# sourceMappingURL=http-mcp.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { prepareClaudeHttpMcpConfig } from './claude/mcp-config.js';
3
+ import { supportsMessageRelay } from './message-relay.js';
4
+ /** Close admission on cancellation/terminal transition without waiting for the CLI pipes.
5
+ * The shared close promise also handles explicit pre-spawn cleanup exactly once.
6
+ * @complexity O(1), one cancellable terminal waiter and one cancel subscription per live listener.
7
+ */
8
+ function watchEndpoint({ endpoint, lifecycle, runId, cleanupConfig }) {
9
+ const waiting = new AbortController();
10
+ let closing;
11
+ let unsubscribe = () => { };
12
+ const close = () => {
13
+ waiting.abort();
14
+ unsubscribe();
15
+ closing ??= endpoint.close({}).finally(cleanupConfig);
16
+ return closing;
17
+ };
18
+ const closeSafely = () => { void close().catch(() => console.warn('[agent-executor] MCP HTTP cleanup failed')); };
19
+ unsubscribe = lifecycle.onCancelRequested({ runId, listener: closeSafely });
20
+ // onCancelRequested replays a prior cancellation before returning its unsubscribe handle.
21
+ if (closing)
22
+ unsubscribe();
23
+ void lifecycle.waitForTerminal({ runId }, { signal: waiting.signal }).then(closeSafely, () => { });
24
+ return close;
25
+ }
26
+ /** Snapshot host-selected metadata once at launch; it is never learned from model arguments. */
27
+ function httpLaunchOptions({ def, runId, injection }) {
28
+ return {
29
+ ...(supportsMessageRelay({ def }) ? { messageRelayNonce: randomBytes(24).toString('hex') } : {}),
30
+ ...(injection.startupToolIds ? { startupToolIds: injection.startupToolIds({ runId }) } : {}),
31
+ ...(injection.diagnosticToolIds ? { diagnosticToolIds: injection.diagnosticToolIds() } : {}),
32
+ };
33
+ }
34
+ /** Claude's path is staged before argv; other vendors use their existing env/home carriers. */
35
+ async function stageHttpConfig({ cwd, def, injection, endpoint, inheritUserMcp }) {
36
+ if (def.externalMcpInjection !== 'claude-mcp-json')
37
+ return {};
38
+ const config = await prepareClaudeHttpMcpConfig({ cwd, endpoint }, { inheritUserMcp,
39
+ ...(injection.readFile ? { readFile: injection.readFile } : {}),
40
+ });
41
+ return { mcpJsonPath: config.path, cleanupConfig: config.cleanup };
42
+ }
43
+ /** Prepare HTTP before launch; only missing support or listener failure selects stdio.
44
+ * @returns Null for stdio, otherwise a run-owned endpoint and optional private Claude config.
45
+ * @throws Configuration staging errors after closing the endpoint; never retries dispatched work.
46
+ * @complexity O(config bytes) for Claude; O(1) otherwise, plus the injected host preparation.
47
+ */
48
+ export async function prepareHttpMcpForRun({ runId, cwd, def, lifecycle, injection, inheritUserMcp = true }, _options = {}) {
49
+ if (!injection?.prepareHttp || !def.externalMcpInjection)
50
+ return null;
51
+ if (!('mcpHttp' in def) || def.mcpHttp !== true) {
52
+ console.warn('[agent-executor] MCP HTTP unsupported for this agent; using stdio');
53
+ return null;
54
+ }
55
+ const launchOptions = httpLaunchOptions({ def, runId, injection });
56
+ let endpoint;
57
+ try {
58
+ endpoint = await injection.prepareHttp({ runId }, launchOptions);
59
+ }
60
+ catch {
61
+ console.warn('[agent-executor] MCP HTTP listener unavailable; using stdio');
62
+ return null;
63
+ }
64
+ try {
65
+ const config = await stageHttpConfig({ cwd, def, injection, endpoint, inheritUserMcp });
66
+ const cleanup = watchEndpoint({ endpoint, lifecycle, runId, ...config });
67
+ return { endpoint, ...(config.mcpJsonPath ? { mcpJsonPath: config.mcpJsonPath } : {}), cleanup };
68
+ }
69
+ catch (error) {
70
+ await endpoint.close({});
71
+ throw error;
72
+ }
73
+ }
74
+ //# sourceMappingURL=http-mcp.js.map