@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
@@ -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,24 @@
1
+ /** Frame and sanitize stdout before either parser or raw-event delivery.
2
+ * @param required - Host byte cap, pure sanitizer and delivery/error sinks.
3
+ * @returns A per-process stream; flush delivers a final unterminated line once.
4
+ * @complexity O(n) time for n wire characters, O(maxBytes) retained text plus incoming chunk slices.
5
+ * @example const stream = createSanitizedLineStream({ maxBytes: 4096, sanitize, emit, onOverflow });
6
+ */
7
+ export declare function createSanitizedLineStream({ maxBytes, sanitize, emit, onOverflow }: {
8
+ maxBytes: number;
9
+ sanitize: (required: {
10
+ fullText: string;
11
+ }) => string;
12
+ emit: (required: {
13
+ text: string;
14
+ }, optional?: Record<string, never>) => void;
15
+ onOverflow: (required: {
16
+ bytes: number;
17
+ }, optional?: Record<string, never>) => void;
18
+ }, _options?: Record<string, never>): {
19
+ feed(required: {
20
+ chunk: string;
21
+ }, optional?: Record<string, never>): void;
22
+ flush(required: Record<string, never>, optional?: Record<string, never>): void;
23
+ };
24
+ //# sourceMappingURL=sanitized-line-stream.d.ts.map
@@ -0,0 +1,45 @@
1
+ /** Frame and sanitize stdout before either parser or raw-event delivery.
2
+ * @param required - Host byte cap, pure sanitizer and delivery/error sinks.
3
+ * @returns A per-process stream; flush delivers a final unterminated line once.
4
+ * @complexity O(n) time for n wire characters, O(maxBytes) retained text plus incoming chunk slices.
5
+ * @example const stream = createSanitizedLineStream({ maxBytes: 4096, sanitize, emit, onOverflow });
6
+ */
7
+ export function createSanitizedLineStream({ maxBytes, sanitize, emit, onOverflow }, _options = {}) {
8
+ let parts = [];
9
+ let bytes = 0;
10
+ let overflow = false;
11
+ /** One line's fragments retain at most maxBytes; an oversized line is discarded through its end. */
12
+ function append(text) {
13
+ if (!text)
14
+ return;
15
+ bytes += Buffer.byteLength(text, 'utf8');
16
+ if (bytes > maxBytes) {
17
+ parts = [];
18
+ overflow = true;
19
+ }
20
+ if (!overflow)
21
+ parts.push(text);
22
+ }
23
+ /** Delivery happens once per completed line, or once for the final unterminated tail. */
24
+ function flush(newline) {
25
+ if (overflow)
26
+ onOverflow({ bytes });
27
+ else if (bytes > 0 || newline)
28
+ emit({ text: sanitize({ fullText: parts.join('') + newline }) });
29
+ parts = [];
30
+ bytes = 0;
31
+ overflow = false;
32
+ }
33
+ return {
34
+ feed({ chunk }, _optional = {}) {
35
+ const lines = chunk.split('\n');
36
+ lines.forEach((line, index) => {
37
+ append(line);
38
+ if (index < lines.length - 1)
39
+ flush('\n');
40
+ });
41
+ },
42
+ flush(_required, _optional = {}) { flush(''); },
43
+ };
44
+ }
45
+ //# sourceMappingURL=sanitized-line-stream.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 });
@@ -31,6 +31,8 @@ export function principalIsReadOnlyConstrained({ principal }, _optional = {}) {
31
31
  /**
32
32
  * Hide writes and unclassified tools from a constrained caller's search and schema discovery.
33
33
  * Uses the same classification as dispatch; discovery never grants execution authority.
34
+ * Guidance entries (`targetToolIds`) are shown only when every target is allowed; the catalog is
35
+ * told the allowed ids so an overview can list just those.
34
36
  * @param required - Per-call identity, live descriptors, and the existing catalog owner.
35
37
  * @returns A filtered catalog, or the original catalog for an unconstrained caller.
36
38
  * @example filterReadOnlyToolCatalog({ principal, registry, catalog });
@@ -49,13 +51,27 @@ export function filterReadOnlyToolCatalog(required, _optional = {}) {
49
51
  .filter(hit => allowed.has(hit.id)).slice(0, limit)
50
52
  .map(hit => ({ ...hit, ...toolDiscoveryMetadata({ descriptor: allowed.get(hit.id) }) }));
51
53
  },
52
- describe(input) {
53
- const descriptor = allowedTools().get(input.id);
54
- const entry = descriptor ? required.catalog.describe(input) : null;
54
+ describe(input, options = {}) {
55
+ const allowed = allowedTools();
56
+ const descriptor = allowed.get(input.id);
57
+ // A caller may further narrow discovery; read-only attenuation must never widen that set.
58
+ const visible = new Map([...allowed].filter(([id]) => !options.visibleToolIds || options.visibleToolIds.has(id)));
59
+ if (!descriptor)
60
+ return readOnlyGuidance({ entry: required.catalog.describe(input, { visibleToolIds: new Set(visible.keys()) }), allowed: visible });
61
+ const entry = required.catalog.describe(input, options);
55
62
  return entry ? { ...entry, ...toolDiscoveryMetadata({ descriptor }) } : null;
56
63
  },
57
64
  };
58
65
  }
66
+ // An id that is not a registered read-only tool can still name guidance (a retired alias, a
67
+ // namespace overview). Guidance is judged by what it points at, never by its own id; guidance
68
+ // without declared targets cannot be judged, so it stays hidden (fail closed).
69
+ function readOnlyGuidance({ entry, allowed }) {
70
+ const targets = entry?.targetToolIds;
71
+ if (!targets || targets.length === 0)
72
+ return null;
73
+ return targets.every(id => allowed.has(id)) ? entry : null;
74
+ }
59
75
  /** @returns Required host wording for a registered non-read-only tool. */
60
76
  export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {}) {
61
77
  return messages.toolRefusalMessage({ toolId });
@@ -64,6 +64,10 @@ export { serializeDelegatedToolOutput, resultContent } from './tool-result-media
64
64
  function linkCancellation({ lifecycle, runId }, { signal }) {
65
65
  const controller = new AbortController();
66
66
  const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => controller.abort() });
67
+ const terminalWatch = new AbortController();
68
+ // A CLI can time out/exit without requesting cancellation. Its unanswered cards must stop
69
+ // holding the daemon busy, while the exchange owner retains their safe restoration handle.
70
+ void lifecycle.waitForTerminal({ runId }, { signal: terminalWatch.signal }).then(() => controller.abort(), () => { });
67
71
  const abortFromTransport = () => controller.abort();
68
72
  if (signal?.aborted)
69
73
  controller.abort();
@@ -73,6 +77,7 @@ function linkCancellation({ lifecycle, runId }, { signal }) {
73
77
  controller,
74
78
  dispose: () => {
75
79
  unsubscribeCancel();
80
+ terminalWatch.abort();
76
81
  signal?.removeEventListener('abort', abortFromTransport);
77
82
  },
78
83
  };
@@ -209,7 +214,7 @@ export function createDelegatedToolBridge(requiredArgs, optionalArgs = {}) {
209
214
  const call = { lifecycle, runId, toolUseId, ...(options.delegatedResultCorrelation ? { delegatedResultCorrelation: options.delegatedResultCorrelation } : {}) };
210
215
  await lifecycle.emit({ runId: runId, input: {
211
216
  event: 'agent',
212
- data: { type: 'tool_use', id: toolUseId, name: toolId, input },
217
+ data: { type: 'tool_use', id: toolUseId, name: toolId, input, ...{ toolOrigin: 'bridge' } },
213
218
  } });
214
219
  const cancellation = linkCancellation(call, invocation);
215
220
  const seam = createSurfaceSeam(call);
@@ -1,104 +1,3 @@
1
- /**
2
- * @module attachments
3
- *
4
- * `POST /api/attachments`, `DELETE /api/attachments` — an HTTP route pack for user file/image
5
- * uploads staged for one agent run, over a narrow, transport-owned `AttachmentStore` port plus a
6
- * batteries-included disk implementation (`createDiskAttachmentStore`).
7
- *
8
- * **The capability this closes.** A chat composer that accepts drag-and-drop needs somewhere to put
9
- * the bytes, a way to hand the agent a path it is actually allowed to read, and a guarantee that
10
- * those bytes disappear when the run ends. That is three separate concerns (an upload endpoint, a
11
- * trusted-metadata registry, and a run-scoped lifetime) which every host would otherwise hand-roll.
12
- * This module owns all three; `@jini-ai/chat-react`'s `createDaemonAttachmentUploader` is the
13
- * matching client half.
14
- *
15
- * **Trust model.** A renderer supplies bytes, a filename, and a batch id. None of that is trusted:
16
- * - the filename is reduced to a basename over a conservative character allowlist
17
- * (`sanitizeAttachmentName`) and is only ever used as *display* text plus a short extension
18
- * suffix — never as the stored filename, which is a fresh `randomUUID()`;
19
- * - the `kind` (`'image' | 'file'`) is sniffed from the leading bytes (`detectAttachmentKind`),
20
- * never taken from a renderer-controlled MIME type or file extension;
21
- * - the byte count is measured while streaming, never read from a client-supplied length;
22
- * - the returned `path` is an opaque `attachment:<uuid>` capability id, not a filesystem path, so a
23
- * renderer never learns where the upload root is and cannot name a file it did not upload.
24
- * `claim()` is what exchanges those ids for real paths, server-side.
25
- *
26
- * **What `claim()` guarantees.** Exactly once per registered attachment, and only if the file is
27
- * still the same file: `lstat` must report a regular non-symlink whose `dev`/`ino`/`size` and
28
- * recorded modification/change timestamps match, and `realpath` must equal the recorded path
29
- * (so neither the file nor any parent directory was swapped for a symlink between upload and run
30
- * start). Legacy sidecars establish their timestamp baseline on first verified read. Every
31
- * claimed attachment must belong to one batch, so the single `batchDirectory` a host grants the
32
- * agent read access to cannot be widened by mixing batches.
33
- *
34
- * **Ownership is opt-in and host-attached, not self-asserted.** `register()`'s optional `ownerId`
35
- * (and `listPendingForOwner`, the discovery method it enables) is never read from anything a
36
- * renderer sends — a host wires `AttachmentsHttpDeps.resolveOwnerId` to pull it from a channel IT
37
- * already trusts (its own session-verified principal header, say), never from `req` unauthenticated.
38
- * A store with no `resolveOwnerId` wired registers every attachment ownerless, and
39
- * `listPendingForOwner` never returns an ownerless record for any caller — see that method's own doc
40
- * for why an absent `ownerId` must never become a wildcard match.
41
- *
42
- * **Storage lifetime is daemon-lifetime by default, and opt-in restart-surviving.** With
43
- * `retainAcrossRestarts` left off (the default, and this pack's only behavior before that option
44
- * existed), `createDiskAttachmentStore` empties its upload directory on construction: files left
45
- * behind by an interrupted previous process cannot be authenticated against an in-memory registry
46
- * that no longer exists, so they are removed rather than adopted. Unclaimed uploads also expire by
47
- * TTL (`pruneExpired`), and a run's claimed files are deleted by `cleanupRun`.
48
- *
49
- * That default is wrong for any host whose process restarts while a person is still using the
50
- * composer. A file-watching dev server restarting the daemon on an unrelated source edit is not a
51
- * crash, but the construction wipe cannot tell the two apart, so it destroys uploads the user is
52
- * about to send — real data loss, not a tidy-up. `retainAcrossRestarts: true` replaces "wipe
53
- * everything, always" with "adopt exactly what can still be authenticated": see
54
- * {@link prepareAttachmentStorage} for the whole mechanism and {@link loadPersistedAttachments} for
55
- * why an adopted record is no less trusted than one this process registered itself.
56
- *
57
- * **This pack does not auto-wire itself into a run's lifecycle**, because no generic hook for that
58
- * exists — the same deliberate choice `@jini-ai/daemon`'s `createRunScopedContextStore` makes. A
59
- * host claims in its own `onRunStarted` and cleans up in a `finally`, roughly:
60
- *
61
- * ```ts
62
- * const store = await createDiskAttachmentStore({ uploadDirectory });
63
- * // ... httpExtensions: [(app, { adapter }) => registerAttachmentRoutes({ app, deps: { store }, adapter })]
64
- * onRunStarted: (context) => {
65
- * void (async () => {
66
- * try {
67
- * const claimed = await store.claim({ attachments: attachmentRefsFrom(context.request), runId: context.run.id });
68
- * await executor.run({
69
- * runId: context.run.id,
70
- * // ... prompt, cwd, agentId
71
- * }, {
72
- * ...(claimed.batchDirectory === undefined ? {} : {
73
- * imagePaths: claimed.attachments.filter((a) => a.kind === 'image').map((a) => a.path),
74
- * extraAllowedDirs: [claimed.batchDirectory],
75
- * uploadRoot: claimed.batchDirectory,
76
- * }),
77
- * });
78
- * } finally {
79
- * await store.cleanupRun({ runId: context.run.id });
80
- * }
81
- * })();
82
- * }
83
- * ```
84
- *
85
- * `imagePaths`/`extraAllowedDirs`/`uploadRoot` are pre-existing `AgentExecutor.run()` options; this
86
- * module only produces real values for them.
87
- *
88
- * **Body-parser ordering (`POST` reads the raw request stream).** The upload route streams
89
- * `request` straight to disk, so any body-parsing middleware that has already consumed the stream
90
- * leaves nothing to write. The classic way to hit this is a JSON body parser mounted app-wide: a
91
- * user drops a `.json` file, the browser sets `content-type: application/json`, and
92
- * `express.json()` eats the body — which without a guard shows up as the deeply unhelpful
93
- * "attachment is empty". `registerAttachmentRoutes` detects an already-consumed stream up front and
94
- * reports it as a host misconfiguration (`'attachment-body-consumed'`) instead of failing silently.
95
- * Mount this pack before any global body parser, or scope the parser so it skips this path.
96
- *
97
- * **SEC-005 redaction**: a store failure can carry filesystem paths a caller must not see, so
98
- * anything that is not an explicitly-classified `AttachmentRejectedError` becomes a
99
- * correlation-id-bearing generic `INTERNAL_ERROR`, with the real error reaching a host-owned sink —
100
- * the same `reportInternalError` shape `media.ts`/`delegated-tools.ts` use.
101
- */
102
1
  import { type FileHandle } from 'node:fs/promises';
103
2
  import type { Express, Request, Response } from 'express';
104
3
  import type { AdapterContext } from '@jini-ai/http-kit';
@@ -688,6 +587,8 @@ export interface AttachmentsInternalErrorContext {
688
587
  }
689
588
  export interface AttachmentsHttpDeps {
690
589
  readonly store: AttachmentStore;
590
+ /** Host busy lease follows handler completion, including cleanup after an aborted response. */
591
+ readonly acquireActivity?: () => () => void;
691
592
  /**
692
593
  * Uploads accepted at once before further requests are rate-limited. Bounds concurrent
693
594
  * filesystem work, which matters because each in-flight upload holds an open handle and may be
@@ -99,6 +99,8 @@
99
99
  * correlation-id-bearing generic `INTERNAL_ERROR`, with the real error reaching a host-owned sink —
100
100
  * the same `reportInternalError` shape `media.ts`/`delegated-tools.ts` use.
101
101
  */
102
+ import { inspect } from 'node:util';
103
+ import { redactSecrets } from '@jini-ai/core';
102
104
  import { chmod, copyFile, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, rmdir } from 'node:fs/promises';
103
105
  import { randomUUID } from 'node:crypto';
104
106
  import { basename, dirname, extname, resolve } from 'node:path';
@@ -971,7 +973,7 @@ export async function createDiskAttachmentStore(requiredArgs, optionalArgs = {})
971
973
  export const ATTACHMENTS_ROUTE_PATH = '/api/attachments';
972
974
  function defaultInternalErrorSink(context) {
973
975
  // eslint-disable-next-line no-console
974
- console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
976
+ console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
975
977
  }
976
978
  function reportInternalError(deps, source, error, batchId) {
977
979
  const correlationId = randomUUID();
@@ -1135,7 +1137,9 @@ export function registerAttachmentRoutes({ app, deps, adapter }, _optional = {})
1135
1137
  // second host) must not share one upload budget.
1136
1138
  const state = { activeUploads: 0 };
1137
1139
  app.post(ATTACHMENTS_ROUTE_PATH, async (req, res) => {
1140
+ let release;
1138
1141
  try {
1142
+ release = deps.acquireActivity?.();
1139
1143
  if (!passesOriginGuard(req, res, deps, adapter))
1140
1144
  return;
1141
1145
  await handleAttachmentUpload({ req, res, deps, state });
@@ -1151,9 +1155,14 @@ export function registerAttachmentRoutes({ app, deps, adapter }, _optional = {})
1151
1155
  if (!res.headersSent)
1152
1156
  respondToUploadFailure(res, deps, error, null, 'attachment-upload');
1153
1157
  }
1158
+ finally {
1159
+ release?.();
1160
+ }
1154
1161
  });
1155
1162
  app.delete(ATTACHMENTS_ROUTE_PATH, async (req, res) => {
1163
+ let release;
1156
1164
  try {
1165
+ release = deps.acquireActivity?.();
1157
1166
  if (!passesOriginGuard(req, res, deps, adapter))
1158
1167
  return;
1159
1168
  await handleAttachmentCleanup({ req, res, deps });
@@ -1162,6 +1171,9 @@ export function registerAttachmentRoutes({ app, deps, adapter }, _optional = {})
1162
1171
  if (!res.headersSent)
1163
1172
  sendApiError({ res, status: 500, error: reportInternalError(deps, 'attachment-cleanup', error, null) });
1164
1173
  }
1174
+ finally {
1175
+ release?.();
1176
+ }
1165
1177
  });
1166
1178
  }
1167
1179
  //# sourceMappingURL=attachments.js.map
@@ -44,6 +44,8 @@
44
44
  * to the provider.
45
45
  * - `RealtimeProvider.subscribe` has no route here — see the realtime section below for why.
46
46
  */
47
+ import { inspect } from 'node:util';
48
+ import { redactSecrets } from '@jini-ai/core';
47
49
  import { randomUUID } from 'node:crypto';
48
50
  import { createApiError } from '@jini-ai/protocol';
49
51
  import { ClientFacingError, defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
@@ -53,7 +55,7 @@ import { err, ok } from '@jini-ai/http-kit';
53
55
  const BODY_NOT_OBJECT_MESSAGE = 'body must be a JSON object';
54
56
  function defaultInternalErrorSink(context) {
55
57
  // eslint-disable-next-line no-console
56
- console.error(`[@jini-ai/http-kit] internal error (connectors/${context.source}.${context.operation}, correlationId=${context.correlationId})`, context.error);
58
+ console.error(`[@jini-ai/http-kit] internal error (connectors/${context.source}.${context.operation}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
57
59
  }
58
60
  function notConfiguredError(capability) {
59
61
  return createApiError({ code: 'NOT_CONFIGURED', message: `${capability} provider not configured` });
@@ -36,6 +36,8 @@
36
36
  * boundary — see {@link denyAllDaemonDbPolicy}'s doc for why the default
37
37
  * policy denies every call rather than allowing one.
38
38
  */
39
+ import { inspect } from 'node:util';
40
+ import { redactSecrets } from '@jini-ai/core';
39
41
  import { randomUUID } from 'node:crypto';
40
42
  import { createApiError } from '@jini-ai/protocol';
41
43
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
@@ -116,7 +118,7 @@ export function createDaemonDbToolRegistrations(requiredArgs, optionalArgs = {})
116
118
  }
117
119
  function defaultDaemonDbInternalErrorSink(context) {
118
120
  // eslint-disable-next-line no-console
119
- console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
121
+ console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
120
122
  }
121
123
  /** Every DB-ops call gets its own opaque, single-use `RunRef` — these are one-off operations requests, not steps of a longer-lived agent run, so there is no real run to attach the audit trail to. */
122
124
  function freshRun() {
@@ -37,6 +37,8 @@ export interface DelegatedToolExecuteResponse {
37
37
  */
38
38
  export interface DelegatedToolsInternalErrorContext {
39
39
  readonly source: 'delegated-tool-execute' | 'resolve-principal';
40
+ /** Optional host-loaded recovery facts; the host describer must redact these as usual. */
41
+ readonly recoveryMessage?: string;
40
42
  readonly runId: string;
41
43
  readonly toolId: string;
42
44
  readonly correlationId: string;
@@ -50,6 +52,16 @@ export interface DelegatedToolsInternalErrorContext {
50
52
  readonly status?: 'timed-out' | 'cancelled' | 'failed';
51
53
  }
52
54
  export interface DelegatedToolsHttpDeps {
55
+ /** One bounded read after an interrupted call, for hosts whose jobs live in another process.
56
+ * Must authorize with the supplied principal, never execute a write/retry, and impose its own
57
+ * deadline. A throw is contained; absent facts remain unknown. No calls on the success path.
58
+ */
59
+ readonly recoverInterruptedTool?: (required: {
60
+ principal: Principal;
61
+ runId: string;
62
+ toolId: string;
63
+ result: ToolExecutionResult;
64
+ }) => Promise<string | undefined>;
53
65
  /** The same negotiated digest store supplied to the run lifecycle and agent executor. */
54
66
  readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
55
67
  readonly lifecycle: RunLifecycle;
@@ -70,7 +82,7 @@ export interface DelegatedToolsHttpDeps {
70
82
  * call it cannot verify is refused outright (the configured refusal message), never
71
83
  * waived. Unconstrained calls behave identically with or without it.
72
84
  */
73
- readonly toolRegistry?: ToolRegistry;
85
+ readonly toolRegistry?: Pick<ToolRegistry, 'list'>;
74
86
  /**
75
87
  * Host opt-in: returns `true` for a `failed` result whose `error` text the HOST has already made
76
88
  * safe to show the model — secret values blanked, an error id minted — so this route answers it