@jini-ai/daemon 0.6.16 → 0.6.19

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 (73) hide show
  1. package/CHANGELOG.md +12 -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.js +2 -1
  11. package/dist/agent-executor/gemini/tool-calls.d.ts +7 -0
  12. package/dist/agent-executor/gemini/tool-calls.js +9 -0
  13. package/dist/agent-executor/http-mcp.d.ts +23 -0
  14. package/dist/agent-executor/http-mcp.js +74 -0
  15. package/dist/agent-executor/launch.js +43 -11
  16. package/dist/agent-executor/mcp-bridge.d.ts +10 -2
  17. package/dist/agent-executor/mcp-bridge.js +2 -2
  18. package/dist/agent-executor/opencode/mcp-config.d.ts +10 -0
  19. package/dist/agent-executor/opencode/mcp-config.js +11 -0
  20. package/dist/agent-executor/opencode/tool-calls.d.ts +7 -0
  21. package/dist/agent-executor/opencode/tool-calls.js +12 -0
  22. package/dist/agent-executor/tool-call-receipts.d.ts +7 -0
  23. package/dist/agent-executor/tool-call-receipts.js +9 -0
  24. package/dist/core/read-only-tools.d.ts +2 -0
  25. package/dist/core/read-only-tools.js +19 -3
  26. package/dist/delegated-tool-bridge.js +6 -1
  27. package/dist/http/attachments.d.ts +2 -101
  28. package/dist/http/attachments.js +13 -1
  29. package/dist/http/connectors.js +3 -1
  30. package/dist/http/db-ops.js +3 -1
  31. package/dist/http/delegated-tools.d.ts +1 -1
  32. package/dist/http/delegated-tools.js +5 -3
  33. package/dist/http/frontend-control.d.ts +0 -36
  34. package/dist/http/frontend-control.js +39 -1
  35. package/dist/http/frontend-sessions.js +4 -2
  36. package/dist/http/index.d.ts +2 -1
  37. package/dist/http/index.js +1 -0
  38. package/dist/http/media.js +3 -1
  39. package/dist/http/model-proxy.js +4 -2
  40. package/dist/http/remote-run-events.js +3 -1
  41. package/dist/http/research.js +3 -2
  42. package/dist/http/runs.js +3 -1
  43. package/dist/http/surface-restoration.d.ts +17 -0
  44. package/dist/http/surface-restoration.js +50 -0
  45. package/dist/http/terminals.js +3 -1
  46. package/dist/http/tool-catalog.d.ts +15 -1
  47. package/dist/http/tool-execution-routes.d.ts +3 -1
  48. package/dist/http/tool-execution-routes.js +4 -3
  49. package/dist/http/xai.js +3 -2
  50. package/dist/idle-monitor.d.ts +29 -0
  51. package/dist/idle-monitor.js +64 -0
  52. package/dist/image-prompt-delivery.d.ts +1 -1
  53. package/dist/image-prompt-delivery.js +4 -3
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +1 -0
  56. package/dist/legacy-data-migration.js +3 -1
  57. package/dist/remote-attachment-lookup.d.ts +3 -1
  58. package/dist/remote-attachment-lookup.js +2 -2
  59. package/dist/routines/scheduler.js +6 -4
  60. package/dist/routing-tool-executor.d.ts +6 -1
  61. package/dist/routing-tool-executor.js +15 -2
  62. package/dist/run-lifecycle.d.ts +0 -16
  63. package/dist/run-lifecycle.js +24 -6
  64. package/dist/surface-exchanges.d.ts +5 -1
  65. package/dist/surface-exchanges.js +41 -7
  66. package/dist/surface-restoration.d.ts +38 -0
  67. package/dist/surface-restoration.js +152 -0
  68. package/dist/tool-recovery/index.d.ts +1 -0
  69. package/dist/tool-recovery/index.js +1 -0
  70. package/dist/tool-recovery/stack.js +2 -1
  71. package/dist/tool-recovery/surface-lifetime.d.ts +11 -0
  72. package/dist/tool-recovery/surface-lifetime.js +41 -0
  73. package/package.json +14 -14
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.6.19
2
+
3
+ - `DelegatedToolsHttpDeps.toolRegistry` takes a list-only registry view (`Pick<ToolRegistry, 'list'>`): the route only reads descriptors, and hosts pass descriptor-only facades.
4
+ - Redact exception logs with the shared core redactor while preserving correlation IDs and error codes.
5
+ - Redact host-supplied paths and payload names in the default migration logger; preserve bind-error callback object assertions.
6
+
7
+ ## 0.6.17
8
+
9
+ ### Patch Changes
10
+
11
+ - Handle npm 12 pack JSON through the shared parser in packed agent-session fixtures.
12
+
1
13
  ## 0.6.16
2
14
 
3
15
  ### Minor 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
  }
@@ -13,6 +13,7 @@ import { classifyRunCloseStatus, } from '../close-status.js';
13
13
  import { resolveContinuationTransport, } from '../continuation/continuation-transport.js';
14
14
  import { resultContent, } from '../delegated-tool-bridge.js';
15
15
  import { projectToolResultEvent, } from '../tool-result-media.js';
16
+ import { createCliToolCallReceiver } from './tool-call-receipts.js';
16
17
  import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
17
18
  import { errorMessage, } from './values.js';
18
19
  import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
@@ -45,7 +46,7 @@ function createStreamHandlerForDef(def, streamFormat, onEvent) {
45
46
  case 'qoder-stream-json':
46
47
  return createQoderStreamHandler({ onEvent: onEvent });
47
48
  case 'json-event-stream':
48
- return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: onEvent });
49
+ return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: createCliToolCallReceiver({ kind: def.eventParser ?? '', onEvent }) });
49
50
  default: {
50
51
  // Unreachable: every stream format has a case above, and a new one fails typecheck here.
51
52
  const unknownFormat = streamFormat;
@@ -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
@@ -1,3 +1,5 @@
1
+ import { prepareHttpMcpForRun } from './http-mcp.js';
2
+ import { mergeOpenCodeHttpMcpConfig } from './opencode/mcp-config.js';
1
3
  import { buildAgentEnv } from './agent-env.js';
2
4
  import { randomUUID } from 'node:crypto';
3
5
  import { supportsMessageRelay } from './message-relay.js';
@@ -363,8 +365,8 @@ function settingsBuildOptions({ input }, _options = {}) {
363
365
  };
364
366
  }
365
367
  /** A relay needs both a delivered bridge and a runtime that understands it. Pure, O(1). */
366
- function canRegisterRunMessageRelay({ def, mcpBridge }, _options = {}) {
367
- return mcpBridge !== null && supportsMessageRelay({ def });
368
+ function canRegisterRunMessageRelay({ def, mcpBridge, httpMcp }, _options = {}) {
369
+ return (mcpBridge !== null || httpMcp === true) && supportsMessageRelay({ def });
368
370
  }
369
371
  /** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
370
372
  * that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
@@ -619,6 +621,16 @@ export function spawnAgentChildProcess({ input, deps }) {
619
621
  return { kind: 'error', error };
620
622
  }
621
623
  }
624
+ /** Command preparation is part of pre-spawn staging, so failures must release all owned resources. */
625
+ async function prepareRunCommand({ command, args, env, runId }, { createInvocation, release, failBeforeSpawn, }) {
626
+ try {
627
+ return createInvocation({ command, args, env });
628
+ }
629
+ catch {
630
+ await release();
631
+ return failBeforeSpawn({ runId, code: 'AGENT_SPAWN_FAILED', message: 'AgentExecutor: could not prepare agent command' });
632
+ }
633
+ }
622
634
  /** Named predicate replacing an inline `streamFormat === 'acp-json-rpc' || streamFormat === 'pi-rpc'` check — the two formats that own their own prompt/event protocol and skip `wireChildLifecycle`. */
623
635
  export function isStdinDrivenFormat(args) {
624
636
  const { streamFormat } = args;
@@ -913,11 +925,11 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
913
925
  ? mcpBridge.serverEntry
914
926
  : undefined;
915
927
  }
916
- function wireRunInput({ input, def, streamFormat, child, modelCatalog, cleanupStagedFiles, writtenMcpJsonPath, mcpBridge }, _options = {}) {
928
+ function wireRunInput({ input, def, streamFormat, child, modelCatalog, cleanupStagedFiles, writtenMcpJsonPath, mcpBridge, httpMcp }, _options = {}) {
917
929
  const formatForStdin = { streamFormat };
918
930
  const stdinHandle = isStdinDrivenFormat(formatForStdin)
919
931
  ? wireChildLifecycle({
920
- ...(canRegisterRunMessageRelay({ def, mcpBridge }) ? { registerMessageRelay: (relay) => liveRunRelays.set(input.runId, relay) } : {}),
932
+ ...(canRegisterRunMessageRelay({ def, mcpBridge, ...(httpMcp === undefined ? {} : { httpMcp }) }) ? { registerMessageRelay: (relay) => liveRunRelays.set(input.runId, relay) } : {}),
921
933
  ...childLifecycleOptions({ input, modelCatalog }),
922
934
  runId: input.runId,
923
935
  def,
@@ -933,7 +945,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
933
945
  continuation,
934
946
  classifyFailure,
935
947
  bufferedStdoutMaxBytes,
936
- expectsJiniBridge: writtenMcpJsonPath !== undefined,
948
+ expectsJiniBridge: writtenMcpJsonPath !== undefined || httpMcp === true,
937
949
  bridgeProbeEntry: bridgeProbeEntryForRun({ mcpBridge }, {}),
938
950
  probeMcpBridge: options.probeMcpBridge ?? probeMcpBridge,
939
951
  })
@@ -1095,6 +1107,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1095
1107
  * `'claude-mcp-json'` mechanism stages a file at all — `'acp-merge'` and `'env-content'` leave
1096
1108
  * nothing on disk, so this stays `undefined` for those.
1097
1109
  */
1110
+ let preparedHttpMcp = null;
1098
1111
  let writtenMcpJsonPath;
1099
1112
  const removeMcpJsonFileFn = mcpJsonInjection?.removeFile ?? defaultRemoveMcpJsonFile;
1100
1113
  /**
@@ -1120,6 +1133,11 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1120
1133
  */
1121
1134
  let preparedSystemPromptOverlayFile = null;
1122
1135
  const cleanupStagedFiles = async () => {
1136
+ if (preparedHttpMcp) {
1137
+ const http = preparedHttpMcp;
1138
+ preparedHttpMcp = null;
1139
+ await http.cleanup();
1140
+ }
1123
1141
  await cleanupFilesystemTemp({ runId: input.runId }, {});
1124
1142
  if (preparedPromptFile)
1125
1143
  await preparedPromptFile.cleanup();
@@ -1151,8 +1169,18 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1151
1169
  // resolving here means the per-run bearer credential is minted exactly once no matter which of
1152
1170
  // the five mechanisms ends up carrying it. `null` for an unconfigured host or a def declaring
1153
1171
  // no strategy — see `buildMcpBridgeDelivery`'s doc.
1154
- const mcpBridge = await resolveMcpBridgeForRun({ input: { runId: input.runId, cwd: input.cwd, def }, deps: { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn } });
1155
- const runtimeContext = computeRuntimeContext({ preparedPromptFile: preparedPromptFile, preparedLogFile: preparedLogFile, mcpBridge: mcpBridge }, sessionRuntimeContext(input, {}));
1172
+ try {
1173
+ preparedHttpMcp = await prepareHttpMcpForRun({ runId: input.runId, cwd: input.cwd, def, lifecycle,
1174
+ injection: mcpJsonInjection, inheritUserMcp: input.nativeToolAccess !== false && input.nativeCapabilities?.userMcp !== false });
1175
+ }
1176
+ catch {
1177
+ await cleanupStagedFiles();
1178
+ return failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: 'AgentExecutor: could not stage private HTTP MCP configuration' });
1179
+ }
1180
+ const mcpBridge = preparedHttpMcp ? null : await resolveMcpBridgeForRun({ input: { runId: input.runId, cwd: input.cwd, def }, deps: { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn } });
1181
+ const baseRuntimeContext = computeRuntimeContext({ preparedPromptFile, preparedLogFile, mcpBridge }, sessionRuntimeContext(input, {}));
1182
+ const runtimeContext = preparedHttpMcp?.mcpJsonPath
1183
+ ? { ...baseRuntimeContext, mcpJsonPath: preparedHttpMcp.mcpJsonPath } : baseRuntimeContext;
1156
1184
  // A `runtimeLock` def's buildArgs mutates process-global state its own CLI reads back at
1157
1185
  // startup, so the mutex must be held from before buildArgs until the spawned child has
1158
1186
  // demonstrably consumed it — see `RuntimeLock`'s own doc for the concrete race. Undefined for
@@ -1195,7 +1223,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1195
1223
  // `codex.ts`'s `buildArgs` needs no argv change for this (CODEX_HOME is an env var, not a flag),
1196
1224
  // so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
1197
1225
  // constraint of its own; it is placed here only to keep the two staging steps adjacent.
1198
- preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, nativeToolAccess: input.nativeToolAccess, ...nativeCapabilityInput({ input }, {}), def, mcpBridge }, deps: { mcpJsonInjection, hostEnv: input.cliParity ? spawnEnv : hostEnv, releaseStagedResources, failBeforeSpawn } });
1226
+ preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, nativeToolAccess: input.nativeToolAccess, ...nativeCapabilityInput({ input }, {}), def, mcpBridge, ...(def.id === 'codex' && preparedHttpMcp ? { httpEndpoint: preparedHttpMcp.endpoint } : {}) }, deps: { mcpJsonInjection, hostEnv: input.cliParity ? spawnEnv : hostEnv, releaseStagedResources, failBeforeSpawn } });
1199
1227
  // Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stage this run's scratch
1200
1228
  // `CLAUDE_CONFIG_DIR` directory. Unconditional for a `claude`-id run (unlike CODEX_HOME above,
1201
1229
  // this does not depend on `mcpJsonInjection` being configured at all — see
@@ -1221,7 +1249,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1221
1249
  // `McpBridgeDelivery`'s `'codex-toml'` variant doc for why it cannot be pre-computed the way
1222
1250
  // `'claude-mcp-json'`'s deterministic path is). Nothing between the old, earlier call site and
1223
1251
  // here ever read `childEnv`, so moving the call cost nothing.
1224
- const childEnv = computeChildEnv({ spawnEnv, mcpBridge }, stagedChildEnvOptions({
1252
+ const httpSpawnEnv = def.id === 'opencode' && preparedHttpMcp
1253
+ ? { ...spawnEnv, OPENCODE_CONFIG_CONTENT: mergeOpenCodeHttpMcpConfig({ existingRaw: spawnEnv.OPENCODE_CONFIG_CONTENT, endpoint: preparedHttpMcp.endpoint }) } : spawnEnv;
1254
+ const childEnv = computeChildEnv({ spawnEnv: httpSpawnEnv, mcpBridge }, stagedChildEnvOptions({
1225
1255
  def, preparedCodexHome, preparedClaudeConfigDir, preparedSystemPromptOverlayFile, systemPromptEnvOverrides,
1226
1256
  }, {}));
1227
1257
  // Post-buildArgs guard for argv-bound defs whose resolved binary is a
@@ -1229,7 +1259,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1229
1259
  // budget can still expand past CreateProcess's command-line cap once
1230
1260
  // quote-escaped. Both are no-ops off-Windows / for non-argv-bound defs.
1231
1261
  await guardWindowsCommandLineBudget({ input: { runId: input.runId, def, launchPath: launch.launchPath, args }, deps: { releaseStagedResources, failBeforeSpawn } });
1232
- const invocation = createCommandInvocationFn({ command: launch.launchPath, args, env: childEnv });
1262
+ const invocation = await prepareRunCommand({ command: launch.launchPath, args, env: childEnv, runId: input.runId }, {
1263
+ createInvocation: createCommandInvocationFn, release: releaseStagedResources, failBeforeSpawn,
1264
+ });
1233
1265
  // Kept a synchronous call (no `await`) on purpose — see `spawnAgentChildProcess`'s own doc for
1234
1266
  // the microtask-timing race this avoids. The error branch's own cleanup/failBeforeSpawn calls are
1235
1267
  // async, which is fine: no child exists yet on that path, so nothing is racing a listener.
@@ -1247,7 +1279,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1247
1279
  // grandchild inheriting them can delay arbitrarily). A spawn that never produced a process at
1248
1280
  // all emits no `'exit'`, and is covered instead by `releaseStagedResources` on the reject path.
1249
1281
  child.once('exit', releaseRuntimeLock);
1250
- const stdinHandle = wireRunInput({ input, def, streamFormat, child, modelCatalog, cleanupStagedFiles, writtenMcpJsonPath, mcpBridge }, {});
1282
+ const stdinHandle = wireRunInput({ input, def, streamFormat, child, modelCatalog, cleanupStagedFiles, writtenMcpJsonPath, mcpBridge, httpMcp: preparedHttpMcp !== null }, {});
1251
1283
  const runId = input.runId;
1252
1284
  try {
1253
1285
  await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
@@ -52,7 +52,11 @@ export declare function buildMcpJsonServerEntry({ runId, options }: {
52
52
  */
53
53
  export declare function mergeMcpJsonContent({ existingRaw, serverEntry }: {
54
54
  readonly existingRaw: string | undefined;
55
- readonly serverEntry: McpJsonServerEntry;
55
+ readonly serverEntry: McpJsonServerEntry | {
56
+ readonly type: 'http';
57
+ readonly url: string;
58
+ readonly headers: Readonly<Record<string, string>>;
59
+ };
56
60
  }): string;
57
61
  /**
58
62
  * Mechanism 2 of 5 — `'acp-merge'`. Re-shapes the same bridge entry into the `mcpServers` element
@@ -107,7 +111,11 @@ export declare function buildAcpMcpBridgeServers({ entry }: {
107
111
  */
108
112
  export declare function mergeEnvContentMcpConfig({ existingRaw, entry }: {
109
113
  readonly existingRaw: string | undefined;
110
- readonly entry: McpJsonServerEntry;
114
+ readonly entry: McpJsonServerEntry | {
115
+ readonly type: 'remote';
116
+ readonly url: string;
117
+ readonly headers: Readonly<Record<string, string>>;
118
+ };
111
119
  }): string;
112
120
  /**
113
121
  * Merges a staged system-prompt overlay file's path into the `instructions` array of the same
@@ -175,12 +175,12 @@ export function mergeEnvContentMcpConfig({ existingRaw, entry }) {
175
175
  const existingMcp = isRecord(doc.mcp) ? doc.mcp : {};
176
176
  const mcp = {
177
177
  ...existingMcp,
178
- [JINI_MCP_SERVER_KEY]: {
178
+ [JINI_MCP_SERVER_KEY]: 'command' in entry ? {
179
179
  type: 'local',
180
180
  command: [entry.command, ...entry.args],
181
181
  environment: { ...entry.env },
182
182
  enabled: true,
183
- },
183
+ } : entry,
184
184
  };
185
185
  return JSON.stringify({ ...doc, mcp });
186
186
  }
@@ -0,0 +1,10 @@
1
+ import type { McpHttpEndpoint } from '../contracts.js';
2
+ /** Keep OpenCode's run-local inline carrier and its existing settings/instruction merge owner.
3
+ * @returns Remote entry replacing stale stdio fields; only the endpoint token is delivered.
4
+ * @complexity O(config bytes) time/space, without I/O.
5
+ */
6
+ export declare function mergeOpenCodeHttpMcpConfig({ existingRaw, endpoint }: {
7
+ existingRaw: string | undefined;
8
+ endpoint: Pick<McpHttpEndpoint, 'url' | 'token'>;
9
+ }, _options?: {}): string;
10
+ //# sourceMappingURL=mcp-config.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { mergeEnvContentMcpConfig } from '../mcp-bridge.js';
2
+ /** Keep OpenCode's run-local inline carrier and its existing settings/instruction merge owner.
3
+ * @returns Remote entry replacing stale stdio fields; only the endpoint token is delivered.
4
+ * @complexity O(config bytes) time/space, without I/O.
5
+ */
6
+ export function mergeOpenCodeHttpMcpConfig({ existingRaw, endpoint }, _options = {}) {
7
+ return mergeEnvContentMcpConfig({ existingRaw, entry: {
8
+ type: 'remote', url: endpoint.url, headers: { Authorization: `Bearer ${endpoint.token}` },
9
+ } });
10
+ }
11
+ //# sourceMappingURL=mcp-config.js.map
@@ -0,0 +1,7 @@
1
+ /** OpenCode joins server and tool with an underscore and can emit a completed call after its
2
+ * bridge result. Preserve the receipt; only its correlation candidate uses the remote name.
3
+ * A native snake-case tool cannot fold without a matching canonical bridge row. Pure, O(n). */
4
+ export declare function openCodeToolCallReceipt({ event }: {
5
+ event: Record<string, unknown>;
6
+ }, _options?: {}): Record<string, unknown>;
7
+ //# sourceMappingURL=tool-calls.d.ts.map
@@ -0,0 +1,12 @@
1
+ /** OpenCode joins server and tool with an underscore and can emit a completed call after its
2
+ * bridge result. Preserve the receipt; only its correlation candidate uses the remote name.
3
+ * A native snake-case tool cannot fold without a matching canonical bridge row. Pure, O(n). */
4
+ export function openCodeToolCallReceipt({ event }, _options = {}) {
5
+ if (event.type !== 'tool_use' || typeof event.name !== 'string' || event.name.startsWith('mcp__'))
6
+ return event;
7
+ const separator = event.name.indexOf('_');
8
+ if (separator <= 0 || separator === event.name.length - 1)
9
+ return event;
10
+ return { ...event, bridgeCall: { name: event.name.slice(separator + 1), input: event.input, buffered: true } };
11
+ }
12
+ //# sourceMappingURL=tool-calls.js.map
@@ -0,0 +1,7 @@
1
+ /** Select the CLI's receipt dialect once; raw events and tools from other CLIs pass unchanged.
2
+ * Folding stays in chat's existing shared rule. O(1) construction, O(name length) per receipt. */
3
+ export declare function createCliToolCallReceiver({ kind, onEvent }: {
4
+ kind: string;
5
+ onEvent: (event: Record<string, unknown>) => void;
6
+ }, _options?: {}): (event: Record<string, unknown>) => void;
7
+ //# sourceMappingURL=tool-call-receipts.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { geminiToolCallReceipt } from './gemini/tool-calls.js';
2
+ import { openCodeToolCallReceipt } from './opencode/tool-calls.js';
3
+ /** Select the CLI's receipt dialect once; raw events and tools from other CLIs pass unchanged.
4
+ * Folding stays in chat's existing shared rule. O(1) construction, O(name length) per receipt. */
5
+ export function createCliToolCallReceiver({ kind, onEvent }, _options = {}) {
6
+ const adapt = kind === 'legacy-google-stream-json' ? geminiToolCallReceipt : kind === 'opencode' ? openCodeToolCallReceipt : undefined;
7
+ return event => onEvent(adapt ? adapt({ event }, {}) : event);
8
+ }
9
+ //# sourceMappingURL=tool-call-receipts.js.map
@@ -77,6 +77,8 @@ export declare function principalIsReadOnlyConstrained({ principal }: {
77
77
  /**
78
78
  * Hide writes and unclassified tools from a constrained caller's search and schema discovery.
79
79
  * Uses the same classification as dispatch; discovery never grants execution authority.
80
+ * Guidance entries (`targetToolIds`) are shown only when every target is allowed; the catalog is
81
+ * told the allowed ids so an overview can list just those.
80
82
  * @param required - Per-call identity, live descriptors, and the existing catalog owner.
81
83
  * @returns A filtered catalog, or the original catalog for an unconstrained caller.
82
84
  * @example filterReadOnlyToolCatalog({ principal, registry, catalog });