@jini-ai/daemon 0.6.5 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.6.7 — 2026-10-15
2
+
3
+ - An unknown tool id is a `400 BAD_REQUEST`, not a redacted `500 INTERNAL_ERROR` (b4a3abad): `ToolExecutor` throws `ToolInputError` naming up to eight registered ids the unknown id prefixes (e.g. `content_read` → `content_read.menu`), and the delegated-tools route returns that message.
4
+
5
+ ## 0.6.6 — 2026-10-14
6
+
7
+ - A failed jini MCP bridge now says why (8bc0f07d): the run still stops at once, but a disposable credential-free copy of the bridge is spawned, sent MCP `initialize`, and its exit code + stderr tail (e.g. `ERR_MODULE_NOT_FOUND`) go into the `MCP_BRIDGE_UNAVAILABLE` message and `error.details`, emitted before the run's end event. `probeMcpBridge` is exported so hosts can run the same check at boot; `mcpJsonInjection.probeOnFailure: false` opts out. The baseline agent env moves to `agent-env.ts` so probe and launch share one allowlist.
8
+
1
9
  ## 0.6.5 — 2026-10-14
2
10
 
3
11
  - Express 5 accepted: the optional `express` peer is now `^4.21.0 || ^5.0.0` (1442d875), so an Express 5 host no longer hits ERESOLVE.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Deny-by-default agent subprocess environment: `BASELINE_AGENT_ENV_KEYS` resolved from
3
+ * `hostEnv`, plus this run's explicitly-delegated credential(s) — never a passthrough of
4
+ * `hostEnv` itself. Run launch reaches this when the caller omits `AgentExecutorRunInput.env`;
5
+ * supplying `env` bypasses this function for that launch (see its call site). Diagnostic bridge
6
+ * probes reuse this same baseline without adding delegated credentials.
7
+ */
8
+ export declare function buildAgentEnv({ hostEnv }: {
9
+ hostEnv: NodeJS.ProcessEnv;
10
+ }, { credentialEnv }?: {
11
+ credentialEnv?: Record<string, string> | undefined;
12
+ }): Record<string, string>;
13
+ //# sourceMappingURL=agent-env.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Fixed baseline of host environment variables every spawned agent subprocess may see by
3
+ * default — resolved from the host's env one name at a time, never derived programmatically
4
+ * from `process.env` as a bag, so this can't silently widen. SEC-001's deny-by-default fix; see
5
+ * `AgentExecutorRunInput.env`'s doc for the full threat model.
6
+ */
7
+ const BASELINE_AGENT_ENV_KEYS = [
8
+ 'PATH', 'HOME', 'USERPROFILE', 'TMPDIR', 'TEMP', 'TMP', 'SHELL',
9
+ 'LANG', 'LC_ALL', 'LC_CTYPE',
10
+ // `USER` is required for a spawned `claude` CLI to find its own login/credential state — with
11
+ // it omitted (even though `HOME` is present), `claude` fails fast with "Not logged in · Please
12
+ // run /login" despite real credentials existing on disk/keychain. Confirmed by bisection against
13
+ // a real authenticated `claude` install: `BASELINE_AGENT_ENV_KEYS` alone fails, adding back every
14
+ // `CLAUDE_CODE_*`/`CLAUDECODE` var still fails, `LOGNAME`/`SSH_AUTH_SOCK` alone still fail, but
15
+ // `USER` alone flips it to success. Login lookup requires the user identity
16
+ // as well as the home directory.
17
+ 'USER',
18
+ 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT', // Windows-only; harmless no-ops elsewhere
19
+ ];
20
+ /**
21
+ * Deny-by-default agent subprocess environment: `BASELINE_AGENT_ENV_KEYS` resolved from
22
+ * `hostEnv`, plus this run's explicitly-delegated credential(s) — never a passthrough of
23
+ * `hostEnv` itself. Run launch reaches this when the caller omits `AgentExecutorRunInput.env`;
24
+ * supplying `env` bypasses this function for that launch (see its call site). Diagnostic bridge
25
+ * probes reuse this same baseline without adding delegated credentials.
26
+ */
27
+ export function buildAgentEnv({ hostEnv }, { credentialEnv } = {}) {
28
+ const result = {};
29
+ for (const key of BASELINE_AGENT_ENV_KEYS) {
30
+ const value = hostEnv[key];
31
+ if (typeof value === 'string')
32
+ result[key] = value;
33
+ }
34
+ for (const [key, value] of Object.entries(credentialEnv ?? {})) {
35
+ result[key] = value;
36
+ }
37
+ return result;
38
+ }
39
+ //# sourceMappingURL=agent-env.js.map
@@ -1,4 +1,5 @@
1
1
  import type { MessageAttachmentImage } from '../attachment-content.js';
2
+ import type { probeMcpBridge } from './mcp-bridge-probe.js';
2
3
  import type { spawn as nodeSpawn, ChildProcess } from 'node:child_process';
3
4
  import type { Principal } from '@jini-ai/core';
4
5
  import type { RunAgentPayload, RunErrorPayload } from '@jini-ai/protocol';
@@ -312,6 +313,8 @@ export interface ContinuationOptions {
312
313
  * interface that defaults to *nothing* rather than a real implementation).
313
314
  */
314
315
  export interface McpJsonInjectionOptions {
316
+ /** Probe a reported bridge failure before emitting its error. @default true when injection is configured. */
317
+ readonly probeOnFailure?: boolean;
315
318
  /** Absolute path (or PATH-resolvable name) to the `jini-mcp` bin entry (`packages/mcp/src/bin/serve.ts`) this driver tells the spawned CLI to launch as its own MCP server subprocess. */
316
319
  readonly command: string;
317
320
  /** Extra argv for `command`. @default [] */
@@ -594,6 +597,8 @@ export interface CreateAgentExecutorOptions {
594
597
  readonly createCommandInvocation?: typeof createCommandInvocation;
595
598
  /** @default `node:child_process`'s `spawn` */
596
599
  readonly spawn?: typeof nodeSpawn;
600
+ /** Diagnostic bridge probe; independent of the agent spawn seam. @default the real MCP initialize probe. */
601
+ readonly probeMcpBridge?: typeof probeMcpBridge;
597
602
  /** @default the real `@jini-ai/agent-runtime` ACP session transport */
598
603
  readonly attachAcpSession?: typeof attachAcpSession;
599
604
  /**
@@ -1,3 +1,4 @@
1
+ import type { probeMcpBridge } from './mcp-bridge-probe.js';
1
2
  import type { ModelCatalogSnapshot } from '@jini-ai/agent-runtime';
2
3
  import type { ChildProcess } from 'node:child_process';
3
4
  import { attachAcpSession, attachPiRpcSession, type AcpPermissionHandler, type RuntimeAgentDef } from '@jini-ai/agent-runtime';
@@ -56,6 +57,8 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
56
57
  * {@link MCP_BRIDGE_UNAVAILABLE} instead of letting it continue without the host's tools.
57
58
  */
58
59
  readonly expectsJiniBridge: boolean;
60
+ readonly bridgeProbeEntry: Parameters<typeof probeMcpBridge>[0]['entry'] | undefined;
61
+ readonly probeMcpBridge: typeof probeMcpBridge;
59
62
  }
60
63
  /**
61
64
  * Wires one spawned child's full observable lifecycle: raw stdout/stderr
@@ -561,9 +561,25 @@ export function wireChildLifecycle(ctx) {
561
561
  */
562
562
  function stopForUnavailableBridge(initStatus, bridgeStatus) {
563
563
  bridgeUnavailable = true;
564
- const message = bridgeUnavailableMessage(bridgeStatus);
565
564
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: initStatus } }));
566
- enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: { message, error: { code: MCP_BRIDGE_UNAVAILABLE, message } } } }));
565
+ enqueueEmit(async () => {
566
+ let probe;
567
+ if (ctx.bridgeProbeEntry) {
568
+ try {
569
+ probe = await ctx.probeMcpBridge({ entry: ctx.bridgeProbeEntry });
570
+ }
571
+ catch {
572
+ // A diagnostic failure must not suppress the original bridge error or the terminal event.
573
+ }
574
+ }
575
+ const message = bridgeUnavailableMessage(bridgeStatus, probe);
576
+ const details = probe ? {
577
+ exitCode: probe.exitCode, signal: probe.signal, stderrTail: probe.stderrTail, elapsedMs: probe.elapsedMs,
578
+ } : undefined;
579
+ await lifecycle.emit({ runId, input: { event: 'error', data: {
580
+ message, error: { code: MCP_BRIDGE_UNAVAILABLE, message, ...(details ? { details } : {}) },
581
+ } } });
582
+ });
567
583
  void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
568
584
  }
569
585
  /**
@@ -93,6 +93,7 @@ export { type SupportedStreamFormat, type AgentExecutorCompatibility, type Agent
93
93
  export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
94
94
  export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
95
95
  export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
96
+ export { probeMcpBridge, type McpBridgeProbeResult } from './mcp-bridge-probe.js';
96
97
  export { DEFAULT_BUFFERED_STDOUT_MAX_BYTES, runAcpDispatch, runPiRpcDispatch, } from './events.js';
97
98
  export { armHandoffWatcher, } from './handoff.js';
98
99
  //# sourceMappingURL=index.d.ts.map
@@ -93,6 +93,7 @@ export { AgentExecutorError, } from './contracts.js';
93
93
  export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
94
94
  export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
95
95
  export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
96
+ export { probeMcpBridge } from './mcp-bridge-probe.js';
96
97
  export { DEFAULT_BUFFERED_STDOUT_MAX_BYTES, runAcpDispatch, runPiRpcDispatch, } from './events.js';
97
98
  export { armHandoffWatcher, } from './handoff.js';
98
99
  //# sourceMappingURL=index.js.map
@@ -379,6 +379,6 @@ export declare function confirmChildSpawned({ input, deps }: {
379
379
  * @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
380
380
  * @overallScore 100/100
381
381
  */
382
- export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "codexDisablePlugins" | "piGracefulShutdownMs" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
382
+ export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "codexDisablePlugins" | "piGracefulShutdownMs" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "probeMcpBridge" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
383
383
  export {};
384
384
  //# sourceMappingURL=launch.d.ts.map
@@ -1,3 +1,5 @@
1
+ import { buildAgentEnv } from './agent-env.js';
2
+ import { probeMcpBridge } from './mcp-bridge-probe.js';
1
3
  import { spawn as nodeSpawn, } from 'node:child_process';
2
4
  import { promises as fsPromises, } from 'node:fs';
3
5
  import { tmpdir, } from 'node:os';
@@ -95,47 +97,10 @@ function toStringEnvRecord(env) {
95
97
  }
96
98
  return result;
97
99
  }
98
- /**
99
- * Fixed baseline of host environment variables every spawned agent subprocess may see by
100
- * default — resolved from the host's env one name at a time, never derived programmatically
101
- * from `process.env` as a bag, so this can't silently widen. SEC-001's deny-by-default fix; see
102
- * `AgentExecutorRunInput.env`'s doc for the full threat model.
103
- */
104
- const BASELINE_AGENT_ENV_KEYS = [
105
- 'PATH', 'HOME', 'USERPROFILE', 'TMPDIR', 'TEMP', 'TMP', 'SHELL',
106
- 'LANG', 'LC_ALL', 'LC_CTYPE',
107
- // `USER` is required for a spawned `claude` CLI to find its own login/credential state — with
108
- // it omitted (even though `HOME` is present), `claude` fails fast with "Not logged in · Please
109
- // run /login" despite real credentials existing on disk/keychain. Confirmed by bisection against
110
- // a real authenticated `claude` install: `BASELINE_AGENT_ENV_KEYS` alone fails, adding back every
111
- // `CLAUDE_CODE_*`/`CLAUDECODE` var still fails, `LOGNAME`/`SSH_AUTH_SOCK` alone still fail, but
112
- // `USER` alone flips it to success. Login lookup requires the user identity
113
- // as well as the home directory.
114
- 'USER',
115
- 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT', // Windows-only; harmless no-ops elsewhere
116
- ];
117
- /**
118
- * Deny-by-default agent subprocess environment: `BASELINE_AGENT_ENV_KEYS` resolved from
119
- * `hostEnv`, plus this run's explicitly-delegated credential(s) — never a passthrough of
120
- * `hostEnv` itself. Only reached when the caller omits `AgentExecutorRunInput.env`; supplying
121
- * `env` bypasses this function entirely (see its call site).
122
- */
123
- function buildAgentEnv(hostEnv, credentialEnv) {
124
- const result = {};
125
- for (const key of BASELINE_AGENT_ENV_KEYS) {
126
- const value = hostEnv[key];
127
- if (typeof value === 'string')
128
- result[key] = value;
129
- }
130
- for (const [key, value] of Object.entries(credentialEnv ?? {})) {
131
- result[key] = value;
132
- }
133
- return result;
134
- }
135
100
  /** Snapshot only host configuration used by spawn and config staging; credentials stay per-run.
136
101
  * Returns an independent allowlisted record. @complexity O(1), fixed key list. */
137
102
  export function selectAgentExecutorHostEnv({ env }, { runtimeState } = {}) {
138
- const selected = buildAgentEnv(env, undefined);
103
+ const selected = buildAgentEnv({ hostEnv: env });
139
104
  for (const key of ['CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'VELA_OPENCODE_BIN', 'AGENT_RUNTIME_HOME', 'AGENT_RUNTIME_RESOURCE_ROOT', 'VP_HOME', 'NPM_CONFIG_PREFIX', 'npm_config_prefix', 'APPDATA', 'LOCALAPPDATA', 'MISE_DATA_DIR', 'FNM_DIR']) {
140
105
  if (typeof env[key] === 'string')
141
106
  selected[key] = env[key];
@@ -239,7 +204,7 @@ export async function resolveImageDeliveryAndArgvBudget({ input, deps }) {
239
204
  * Pure.
240
205
  */
241
206
  export function resolveRunEnv({ input, hostEnv }) {
242
- return input.env !== undefined ? toStringEnvRecord(input.env) : buildAgentEnv(hostEnv, input.credentialEnv);
207
+ return input.env !== undefined ? toStringEnvRecord(input.env) : buildAgentEnv({ hostEnv }, { credentialEnv: input.credentialEnv });
243
208
  }
244
209
  /** Phase 3b: launch-path resolution + binary-not-resolved guard. */
245
210
  export async function resolveLaunch({ input, deps }) {
@@ -979,6 +944,11 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
979
944
  classifyFailure,
980
945
  bufferedStdoutMaxBytes,
981
946
  expectsJiniBridge: writtenMcpJsonPath !== undefined,
947
+ // probeMcpBridge itself strips the run credential and swaps in a diagnostic run id.
948
+ bridgeProbeEntry: mcpBridge?.kind === 'claude-mcp-json' && mcpJsonInjection?.probeOnFailure !== false
949
+ ? mcpBridge.serverEntry
950
+ : undefined,
951
+ probeMcpBridge: options.probeMcpBridge ?? probeMcpBridge,
982
952
  })
983
953
  : null;
984
954
  // Registered with the lifecycle wiring above, before the spawn-confirmation await, so a child
@@ -0,0 +1,25 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ export interface McpBridgeProbeResult {
3
+ readonly ok: boolean;
4
+ readonly exitCode: number | null;
5
+ readonly signal: string | null;
6
+ readonly stderrTail: string;
7
+ readonly elapsedMs: number;
8
+ }
9
+ /** Initialize a disposable bridge without a run credential; always reap it after the bounded probe.
10
+ * Spawn/errors/exits/timeouts return diagnostics rather than rejecting. Stderr retains 2000 bytes.
11
+ * @complexity O(n) in received bytes; stderr storage is bounded independently of child output.
12
+ */
13
+ export declare function probeMcpBridge({ entry }: {
14
+ readonly entry: {
15
+ readonly command: string;
16
+ readonly args: readonly string[];
17
+ readonly env?: Readonly<Record<string, string>>;
18
+ };
19
+ }, { spawn, baseEnv, timeoutMs, now }?: {
20
+ readonly spawn?: typeof nodeSpawn;
21
+ readonly baseEnv?: Readonly<Record<string, string>>;
22
+ readonly timeoutMs?: number;
23
+ readonly now?: () => number;
24
+ }): Promise<McpBridgeProbeResult>;
25
+ //# sourceMappingURL=mcp-bridge-probe.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ import { redactSecrets } from '@jini-ai/core';
4
+ import { buildAgentEnv } from './agent-env.js';
5
+ import { errorMessage, isRecord } from './values.js';
6
+ /** Initialize a disposable bridge without a run credential; always reap it after the bounded probe.
7
+ * Spawn/errors/exits/timeouts return diagnostics rather than rejecting. Stderr retains 2000 bytes.
8
+ * @complexity O(n) in received bytes; stderr storage is bounded independently of child output.
9
+ */
10
+ export async function probeMcpBridge({ entry }, { spawn = nodeSpawn, baseEnv = buildAgentEnv({ hostEnv: process.env }), timeoutMs = 15_000, now = Date.now } = {}) {
11
+ const startedAt = now();
12
+ const env = { ...baseEnv, ...entry.env, JINI_RUN_ID: 'bridge-probe' };
13
+ // Initialize needs no callback: never delegate a real run's authority to this diagnostic child.
14
+ delete env.JINI_DAEMON_TOKEN;
15
+ let child;
16
+ let lines;
17
+ let timer;
18
+ let stderr = Buffer.alloc(0);
19
+ const appendStderr = (chunk) => {
20
+ stderr = Buffer.concat([stderr, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]).subarray(-2000);
21
+ };
22
+ const result = (ok, exitCode = null, signal = null) => ({
23
+ ok, exitCode, signal,
24
+ stderrTail: redactSecrets({ input: stderr.toString('utf8') }, {
25
+ extraPatterns: [{ name: 'daemon_token', regex: /JINI_DAEMON_TOKEN\s*=\s*\S+/gi }],
26
+ }).trim(),
27
+ elapsedMs: now() - startedAt,
28
+ });
29
+ try {
30
+ child = spawn(entry.command, [...entry.args], { env, stdio: 'pipe' });
31
+ const bridge = child;
32
+ lines = bridge.stdout ? createInterface({ input: bridge.stdout, crlfDelay: Infinity }) : undefined;
33
+ return await new Promise((resolve) => {
34
+ const fail = (error) => {
35
+ appendStderr(`\n${errorMessage(error)}`);
36
+ resolve(result(false));
37
+ };
38
+ timer = setTimeout(() => resolve(result(false)), timeoutMs);
39
+ bridge.once('error', fail);
40
+ bridge.once('exit', (code, signal) => resolve(result(false, code, signal)));
41
+ bridge.once('close', (code, signal) => resolve(result(false, code, signal)));
42
+ bridge.stderr?.on('data', appendStderr);
43
+ bridge.stdin?.once('error', fail);
44
+ if (lines) {
45
+ lines.once('error', fail);
46
+ lines.on('line', (line) => {
47
+ try {
48
+ const reply = JSON.parse(line);
49
+ if (isRecord(reply) && reply.id === 1 && Object.hasOwn(reply, 'result'))
50
+ resolve(result(true));
51
+ }
52
+ catch {
53
+ // Startup chatter is not an initialize response; keep waiting for the matching reply.
54
+ }
55
+ });
56
+ }
57
+ bridge.stdin?.end(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {
58
+ protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'jini-bridge-probe', version: '0' },
59
+ } })}\n`);
60
+ });
61
+ }
62
+ catch (error) {
63
+ appendStderr(`\n${errorMessage(error)}`);
64
+ return result(false);
65
+ }
66
+ finally {
67
+ clearTimeout(timer);
68
+ lines?.close();
69
+ try {
70
+ child?.kill('SIGKILL');
71
+ }
72
+ catch {
73
+ // A process already gone or refusing cleanup must not discard the original probe diagnostic.
74
+ }
75
+ }
76
+ }
77
+ //# sourceMappingURL=mcp-bridge-probe.js.map
@@ -1,3 +1,4 @@
1
+ import type { McpBridgeProbeResult } from './mcp-bridge-probe.js';
1
2
  import type { AcpMcpServerInput, RuntimeAgentDef } from '@jini-ai/agent-runtime';
2
3
  import type { McpJsonInjectionOptions, McpJsonServerEntry, McpBridgeDelivery, PreparedCodexHome, PreparedClaudeConfigDir, ClaudeConfigDirIsolationOptions, FailBeforeSpawn } from './contracts.js';
3
4
  /** The run-error code a run gets when its CLI reports the injected `jini` bridge as anything but connected. */
@@ -16,7 +17,7 @@ export declare function unavailableJiniBridgeStatus({ rawEvent }: {
16
17
  readonly rawEvent: unknown;
17
18
  }): string | undefined;
18
19
  /** The user-readable reason a run stopped because its bridge did not connect. */
19
- export declare function bridgeUnavailableMessage(status: string): string;
20
+ export declare function bridgeUnavailableMessage(status: string, probe?: McpBridgeProbeResult): string;
20
21
  /**
21
22
  * Builds this run's `mcpServers.jini` entry — pure and synchronous, so every field mapping is
22
23
  * directly assertable without touching the filesystem. The credential arrives already resolved:
@@ -25,8 +25,13 @@ export function unavailableJiniBridgeStatus({ rawEvent }) {
25
25
  return status === 'connected' ? undefined : status;
26
26
  }
27
27
  /** The user-readable reason a run stopped because its bridge did not connect. */
28
- export function bridgeUnavailableMessage(status) {
29
- return `The assistant's tools did not load (MCP bridge "${JINI_MCP_SERVER_KEY}" status: ${status}). The run was stopped instead of continuing without them.`;
28
+ export function bridgeUnavailableMessage(status, probe) {
29
+ const message = `The assistant's tools did not load (MCP bridge "${JINI_MCP_SERVER_KEY}" status: ${status}). The run was stopped instead of continuing without them.`;
30
+ if (!probe || probe.ok)
31
+ return message;
32
+ const lines = probe.stderrTail.trim().split(/\r?\n/);
33
+ const reason = lines.find((line) => line.includes('Error')) ?? lines.at(-1);
34
+ return `${message} Bridge exited with code ${probe.exitCode}: ${reason}`;
30
35
  }
31
36
  /**
32
37
  * Builds this run's `mcpServers.jini` entry — pure and synchronous, so every field mapping is
@@ -6,5 +6,5 @@
6
6
  * wrappers or a second implementation would let tested and shipped behavior drift.
7
7
  * See agent-executor/index.ts for the driver contracts and invariants.
8
8
  */
9
- export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
9
+ export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, probeMcpBridge, type McpBridgeProbeResult, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
10
10
  //# sourceMappingURL=agent-executor.d.ts.map
@@ -6,5 +6,5 @@
6
6
  * wrappers or a second implementation would let tested and shipped behavior drift.
7
7
  * See agent-executor/index.ts for the driver contracts and invariants.
8
8
  */
9
- export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, AgentExecutorError, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
9
+ export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, AgentExecutorError, MCP_BRIDGE_UNAVAILABLE, probeMcpBridge, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
10
10
  //# sourceMappingURL=agent-executor.js.map
@@ -33,8 +33,8 @@ export interface DelegatedToolInvocation {
33
33
  export interface DelegatedToolBridge {
34
34
  /**
35
35
  * Emits a canonical `tool_use`, executes through `ToolExecutor`, then emits
36
- * exactly one matching `tool_result`. Unknown tool ids remain programmer or
37
- * routing errors from `ToolExecutor` and are rethrown after their error
36
+ * exactly one matching `tool_result`. Unknown tool ids are input errors
37
+ * from `ToolExecutor` and are rethrown after their error
38
38
  * result is recorded.
39
39
  */
40
40
  execute(requiredArgs: Pick<DelegatedToolInvocation, "runId" | "toolUseId" | "toolId" | "principal" | "input">, optionalArgs?: Pick<DelegatedToolInvocation, "signal">): Promise<ToolExecutionResult>;
@@ -1,5 +1,5 @@
1
1
  import type { Express } from 'express';
2
- import type { Principal, ToolRegistry } from '@jini-ai/core';
2
+ import { type Principal, type ToolRegistry } from '@jini-ai/core';
3
3
  import { type RunLifecycle, type ToolExecutionResult, type ToolExecutor } from '../index.js';
4
4
  import { type AdapterContext } from '@jini-ai/http-kit';
5
5
  export interface DelegatedToolExecuteRequest {
@@ -81,7 +81,7 @@ export interface DelegatedToolsHttpDeps {
81
81
  /**
82
82
  * Host opt-in: the model-safe text to send in place of `'an internal error occurred'` for every
83
83
  * failure this route would otherwise answer with the SEC-005-redacted `500 INTERNAL_ERROR` — a
84
- * thrown executor (e.g. an unknown tool id), a throwing `resolvePrincipal`, `timed-out`,
84
+ * thrown executor infrastructure failure, a throwing `resolvePrincipal`, `timed-out`,
85
85
  * `cancelled`, and a `failed` result `isModelSafeToolFailure` did not vouch for. Without it, those
86
86
  * reach the model as a bare "INTERNAL_ERROR" it can neither act on nor report.
87
87
  *
@@ -98,8 +98,8 @@ export interface DelegatedToolsHttpDeps {
98
98
  * `POST /api/delegated-tool-calls` — executes one delegated tool call against an already-started
99
99
  * run. Maps `ToolExecutionResult.status` to the HTTP response via `toolExecutionResultToApiResult`
100
100
  * above — mirroring `db-ops.ts`'s precedent for the identical status union rather than treating
101
- * every business outcome as a `200`. A genuinely unexpected throw (an unregistered `toolId` — a
102
- * routing/programming error per `ToolExecutor.execute`'s own contract — or a race where the run
101
+ * every business outcome as a `200`. An unregistered `toolId` throws `ToolInputError` and answers
102
+ * `400 BAD_REQUEST`. A genuinely unexpected throw (such as a race where the run
103
103
  * became terminal between this route's existence check and the bridge's first emitted event)
104
104
  * reaches the same SEC-005 redaction path.
105
105
  */
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { randomUUID } from 'node:crypto';
29
29
  import { createApiError } from '@jini-ai/protocol';
30
+ import { ToolInputError } from '@jini-ai/core';
30
31
  import { createDelegatedToolBridge } from '../index.js';
31
32
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
32
33
  import { validationError } from '@jini-ai/http-kit';
@@ -159,8 +160,8 @@ function toolExecutionResultToApiResult(deps, runId, toolId, result) {
159
160
  * `POST /api/delegated-tool-calls` — executes one delegated tool call against an already-started
160
161
  * run. Maps `ToolExecutionResult.status` to the HTTP response via `toolExecutionResultToApiResult`
161
162
  * above — mirroring `db-ops.ts`'s precedent for the identical status union rather than treating
162
- * every business outcome as a `200`. A genuinely unexpected throw (an unregistered `toolId` — a
163
- * routing/programming error per `ToolExecutor.execute`'s own contract — or a race where the run
163
+ * every business outcome as a `200`. An unregistered `toolId` throws `ToolInputError` and answers
164
+ * `400 BAD_REQUEST`. A genuinely unexpected throw (such as a race where the run
164
165
  * became terminal between this route's existence check and the bridge's first emitted event)
165
166
  * reaches the same SEC-005 redaction path.
166
167
  */
@@ -219,6 +220,9 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
219
220
  return toolExecutionResultToApiResult(deps, input.runId, input.toolId, result);
220
221
  }
221
222
  catch (error) {
223
+ if (error instanceof ToolInputError) {
224
+ return err({ error: createApiError({ code: 'BAD_REQUEST', message: error.message }) });
225
+ }
222
226
  return err({ error: reportInternalError(deps, 'delegated-tool-execute', error, input.runId, input.toolId) });
223
227
  }
224
228
  }
@@ -83,7 +83,7 @@ export interface ToolExecutor {
83
83
  * @param emitSurface - Transport-supplied seam letting the handler push a human-only surface into
84
84
  * the run's event stream *before* this call settles. Omit for a caller with no run event stream;
85
85
  * see `@jini-ai/core`'s `SurfaceEmitter` for why a handler that waits on a human needs it.
86
- * @throws If `toolId` isn't registered — a routing/programming error, distinct from the denial/confirmation-denial states `ToolExecutionResult.status` covers.
86
+ * @throws {ToolInputError} If `toolId` isn't registered, with up to eight registered prefix matches.
87
87
  */
88
88
  execute(args: {
89
89
  readonly principal: Principal;
@@ -125,7 +125,7 @@ export interface CreateToolExecutorOptions {
125
125
  * — the one and only caller of that internal export.
126
126
  * @param options.delegate - Transport-supplied authorize/confirm UI seam;
127
127
  * omit for a headless caller whose tools never need interactive gating.
128
- * @complexity `execute` is O(1) plus the handler's own cost; `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
128
+ * @complexity Known-id `execute` is O(1) plus the handler's own cost; unknown ids scan O(n) descriptors for hints. `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
129
129
  * @overallScore 100/100
130
130
  */
131
131
  export declare function createToolExecutor(requiredArgs: Pick<CreateToolExecutorOptions, "registry">, optionalArgs?: Pick<CreateToolExecutorOptions, "delegate" | "now">): ToolExecutor;
@@ -81,7 +81,7 @@ function truncateOutput(output, maxOutputBytes) {
81
81
  * — the one and only caller of that internal export.
82
82
  * @param options.delegate - Transport-supplied authorize/confirm UI seam;
83
83
  * omit for a headless caller whose tools never need interactive gating.
84
- * @complexity `execute` is O(1) plus the handler's own cost; `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
84
+ * @complexity Known-id `execute` is O(1) plus the handler's own cost; unknown ids scan O(n) descriptors for hints. `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
85
85
  * @overallScore 100/100
86
86
  */
87
87
  export function createToolExecutor(requiredArgs, optionalArgs = {}) {
@@ -182,7 +182,9 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
182
182
  // existence, never returns a handler, so this doesn't reopen the leak authorizeToolInvocation
183
183
  // closed — that gate still runs, still unconditionally, inside the call below.
184
184
  if (!registry.has({ toolId })) {
185
- throw new Error(`ToolExecutor: unknown tool "${toolId}"`);
185
+ const candidates = registry.list({}).map((tool) => tool.id).filter((id) => id.startsWith(toolId)).slice(0, 8);
186
+ const hint = candidates.length > 0 ? `; did you mean ${candidates.join(', ')}?` : '';
187
+ throw new ToolInputError({ message: `ToolExecutor: unknown tool "${toolId}"${hint}` });
186
188
  }
187
189
  const executionId = openAudit(principal, run, toolId);
188
190
  // Observes `signal` for this execution's ENTIRE remaining lifetime, not just the handler-run
@@ -216,7 +218,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
216
218
  // Recorded terminal and returned as `failed` with a fixed message. `failed` is the right state:
217
219
  // this execution ended, and it ended for an operational reason rather than a decision. The real
218
220
  // exception stays server-side. Deliberately narrower than the `unknown tool` throw above, which
219
- // stays a throw because it is a routing/programming error, not an execution outcome.
221
+ // stays a typed input-error throw because no tool execution exists to settle.
220
222
  let resolved;
221
223
  try {
222
224
  resolved = await authorize(principal, run, toolId, input);
@@ -1,8 +1,8 @@
1
1
  // Local federation forks moved to @jini-ai/mcp/federation (+ /stdio, /approvals); see development/DELETED-CODE.md.
2
2
  /**
3
3
  * `ToolExecutor.execute`'s own contract for an unregistered id (`@jini-ai/daemon`'s
4
- * `tool-executor.ts`): a thrown `Error` reading `unknown tool "<id>"`, with no code or subclass to
5
- * match on instead. Duplicated from `tool-executor-audit.ts`'s identical `isUnknownToolError` rather
4
+ * `tool-executor.ts`): a thrown `ToolInputError` (from `@jini-ai/core`) reading `unknown tool "<id>"`.
5
+ * This still matches on the message text, not the class. Duplicated from `tool-executor-audit.ts`'s identical `isUnknownToolError` rather
6
6
  * than imported: that file is a sibling one layer up with no dependency on `mcp-federation`, and
7
7
  * importing across that line for an 18-character regex is not worth the coupling. Both copies carry
8
8
  * the same MEDIUM message-text-coupling note that file's own doc records — if `@jini-ai/daemon`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -139,17 +139,17 @@
139
139
  "@types/pg": "8.23.1",
140
140
  "express": "^4.21.0",
141
141
  "@types/express": "^4.17.21",
142
- "@jini-ai/http-kit": "0.4.2",
143
- "@jini-ai/chat": "0.6.2",
144
- "@jini-ai/core": "^0.4.6",
145
142
  "@jini-ai/agent-runtime": "^0.4.11",
146
- "@jini-ai/platform": "^0.5.7",
143
+ "@jini-ai/chat": "0.6.5",
147
144
  "@jini-ai/protocol": "^0.4.4",
145
+ "@jini-ai/core": "^0.4.6",
146
+ "@jini-ai/platform": "^0.5.8",
147
+ "@jini-ai/http-kit": "0.4.2",
148
148
  "@jini-ai/db": "0.3.4"
149
149
  },
150
150
  "dependencies": {
151
- "@jini-ai/core": "^0.4.6",
152
- "@jini-ai/protocol": "^0.4.4"
151
+ "@jini-ai/protocol": "^0.4.4",
152
+ "@jini-ai/core": "^0.4.6"
153
153
  },
154
154
  "peerDependencies": {
155
155
  "node-pty": "^1.1.0",