@jini-ai/daemon 0.6.8 → 0.6.10

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 (79) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
  3. package/dist/agent-executor/antigravity-permissions.js +64 -0
  4. package/dist/agent-executor/contracts.d.ts +37 -6
  5. package/dist/agent-executor/event-translation.d.ts +6 -1
  6. package/dist/agent-executor/event-translation.js +3 -1
  7. package/dist/agent-executor/events.d.ts +5 -0
  8. package/dist/agent-executor/events.js +143 -87
  9. package/dist/agent-executor/launch.d.ts +5 -3
  10. package/dist/agent-executor/launch.js +315 -217
  11. package/dist/agent-executor/mcp-bridge-probe.js +51 -35
  12. package/dist/agent-executor/mcp-bridge.d.ts +18 -8
  13. package/dist/agent-executor/mcp-bridge.js +105 -65
  14. package/dist/agent-executor/run-output.d.ts +20 -0
  15. package/dist/agent-executor/run-output.js +80 -0
  16. package/dist/attachment-content.js +18 -15
  17. package/dist/core/read-only-tools.d.ts +15 -0
  18. package/dist/core/read-only-tools.js +29 -1
  19. package/dist/core/tool-audit.d.ts +3 -1
  20. package/dist/core/tool-audit.js +35 -22
  21. package/dist/core/tool-gaps.d.ts +60 -0
  22. package/dist/core/tool-gaps.js +199 -0
  23. package/dist/delegated-result-correlation.d.ts +26 -0
  24. package/dist/delegated-result-correlation.js +68 -0
  25. package/dist/delegated-tool-bridge.d.ts +6 -14
  26. package/dist/delegated-tool-bridge.js +155 -155
  27. package/dist/event-log.d.ts +45 -2
  28. package/dist/event-log.js +83 -4
  29. package/dist/http/attachments.d.ts +26 -1
  30. package/dist/http/attachments.js +129 -65
  31. package/dist/http/connectors.d.ts +1 -32
  32. package/dist/http/connectors.js +13 -68
  33. package/dist/http/delegated-tools.d.ts +8 -1
  34. package/dist/http/delegated-tools.js +30 -30
  35. package/dist/http/host-tools.d.ts +1 -0
  36. package/dist/http/host-tools.js +64 -59
  37. package/dist/http/index.d.ts +2 -2
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/media.js +48 -69
  40. package/dist/http/memory.js +45 -36
  41. package/dist/http/model-proxy-fields.d.ts +23 -0
  42. package/dist/http/model-proxy-fields.js +29 -0
  43. package/dist/http/model-proxy.js +193 -395
  44. package/dist/http/research-tavily.adapter.js +72 -49
  45. package/dist/http/research.js +33 -23
  46. package/dist/http/resource-request.d.ts +37 -0
  47. package/dist/http/resource-request.js +59 -0
  48. package/dist/http/routines.js +49 -90
  49. package/dist/http/runs.d.ts +7 -5
  50. package/dist/http/runs.js +21 -7
  51. package/dist/http/terminals.js +23 -48
  52. package/dist/http/tool-catalog.d.ts +5 -0
  53. package/dist/http/xai.d.ts +11 -1
  54. package/dist/http/xai.js +125 -124
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +1 -0
  57. package/dist/legacy-data-migration.js +41 -24
  58. package/dist/run/core/retry.js +51 -35
  59. package/dist/run/diagnostics/diagnostics.js +5 -17
  60. package/dist/run-credentials.js +49 -27
  61. package/dist/run-lifecycle.d.ts +31 -2
  62. package/dist/run-lifecycle.js +49 -42
  63. package/dist/store/agent-sessions/sqlite.d.ts +6 -0
  64. package/dist/store/agent-sessions/sqlite.js +6 -4
  65. package/dist/surface-exchanges.js +2 -1
  66. package/dist/terminal-session.d.ts +1 -1
  67. package/dist/terminal-session.js +26 -33
  68. package/dist/tool-audit.d.ts +1 -0
  69. package/dist/tool-audit.js +1 -0
  70. package/dist/tool-executor.d.ts +24 -3
  71. package/dist/tool-executor.js +276 -139
  72. package/dist/tool-recovery/recovery.js +64 -35
  73. package/dist/tool-recovery/stack.d.ts +2 -1
  74. package/dist/tool-recovery/stack.js +1 -1
  75. package/dist/tool-result-diagnostics.d.ts +27 -0
  76. package/dist/tool-result-diagnostics.js +77 -0
  77. package/dist/tool-result-media.d.ts +27 -14
  78. package/dist/tool-result-media.js +51 -38
  79. package/package.json +14 -14
@@ -1,4 +1,15 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ /** MIME types a runtime with image access receives as native image input; any other type is a file. */
3
+ const NATIVE_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
4
+ /** The notice for an attachment that is not delivered as a native image. */
5
+ function fileNotice({ source, fileAccess }) {
6
+ const video = source.mimeType.startsWith('video/');
7
+ if (!fileAccess || !source.path)
8
+ return `${source.name}: ${video ? 'video' : 'file'} attachments are unsupported by this runtime.`;
9
+ return video
10
+ ? `${source.name}: video attachments are unsupported by this runtime; the file is available at ${source.path}.`
11
+ : `${source.name}: read the attached file at ${source.path}.`;
12
+ }
2
13
  /** One byte-bearing message path for CLI and API consumers. The host reader owns authorization,
3
14
  * integrity and MIME sniffing; this module never opens a caller-selected path itself. Unsupported
4
15
  * files are explicit so the model cannot mistake a filename for content it has received. */
@@ -8,24 +19,16 @@ export async function prepareMessageAttachments({ refs, read }, { fileAccess = f
8
19
  const imagePaths = [];
9
20
  for (const ref of refs) {
10
21
  const source = await read({ ref }, {});
11
- if (['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(source.mimeType)) {
12
- if (!imageAccess) {
13
- notices.push(`${source.name}: image attachments are unsupported by this runtime.`);
14
- continue;
15
- }
16
- if (source.path)
17
- imagePaths.push(source.path);
18
- images.push({ mimeType: source.mimeType, data: Buffer.from(source.bytes).toString('base64') });
19
- continue;
22
+ if (!NATIVE_IMAGE_MIME_TYPES.has(source.mimeType)) {
23
+ notices.push(fileNotice({ source, fileAccess }));
20
24
  }
21
- const video = source.mimeType.startsWith('video/');
22
- if (fileAccess && source.path) {
23
- notices.push(video
24
- ? `${source.name}: video attachments are unsupported by this runtime; the file is available at ${source.path}.`
25
- : `${source.name}: read the attached file at ${source.path}.`);
25
+ else if (!imageAccess) {
26
+ notices.push(`${source.name}: image attachments are unsupported by this runtime.`);
26
27
  }
27
28
  else {
28
- notices.push(`${source.name}: ${video ? 'video' : 'file'} attachments are unsupported by this runtime.`);
29
+ if (source.path)
30
+ imagePaths.push(source.path);
31
+ images.push({ mimeType: source.mimeType, data: Buffer.from(source.bytes).toString('base64') });
29
32
  }
30
33
  }
31
34
  return { images, imagePaths, notice: notices.length ? `\n\nAttachments to this message:\n${notices.join('\n')}` : '' };
@@ -1,6 +1,7 @@
1
1
  import { type Principal, type ToolRegistry } from '@jini-ai/core';
2
2
  import type { ToolExecutor } from '../tool-executor.js';
3
3
  import type { IdGenerator } from '@jini-ai/core/primitives';
4
+ import type { ToolCatalogQuery } from '../http/tool-catalog.js';
4
5
  /**
5
6
  * A `Principal` narrowed to tools that only read.
6
7
  *
@@ -71,6 +72,20 @@ export declare function constrainPrincipalToReadOnlyTools({ principal }: {
71
72
  export declare function principalIsReadOnlyConstrained({ principal }: {
72
73
  principal: Principal;
73
74
  }, _optional?: Record<string, never>): boolean;
75
+ /**
76
+ * Hide writes and unclassified tools from a constrained caller's search and schema discovery.
77
+ * Uses the same classification as dispatch; discovery never grants execution authority.
78
+ * @param required - Per-call identity, live descriptors, and the existing catalog owner.
79
+ * @returns A filtered catalog, or the original catalog for an unconstrained caller.
80
+ * @example filterReadOnlyToolCatalog({ principal, registry, catalog });
81
+ * @complexity O(n + h) per search for n live registrations and h ranked hits. Overfetch is bounded
82
+ * by registered tool count so withheld writes cannot displace permitted reads at a small limit.
83
+ */
84
+ export declare function filterReadOnlyToolCatalog(required: {
85
+ principal: Principal;
86
+ registry: Pick<ToolRegistry, 'list'>;
87
+ catalog: ToolCatalogQuery;
88
+ }, _optional?: Record<string, never>): ToolCatalogQuery;
74
89
  /** @returns Required host wording for a registered non-read-only tool. */
75
90
  export declare function readOnlyToolRefusalMessage({ toolId, messages }: {
76
91
  toolId: string;
@@ -1,4 +1,4 @@
1
- import { isReadOnlyTool } from '@jini-ai/core';
1
+ import { isReadOnlyTool, toolDiscoveryMetadata } from '@jini-ai/core';
2
2
  import { toolCandidates } from './tool-candidates.js';
3
3
  /**
4
4
  * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes both
@@ -22,6 +22,34 @@ export function constrainPrincipalToReadOnlyTools({ principal }, _optional = {})
22
22
  export function principalIsReadOnlyConstrained({ principal }, _optional = {}) {
23
23
  return 'toolAccess' in principal && principal.toolAccess === 'read-only';
24
24
  }
25
+ /**
26
+ * Hide writes and unclassified tools from a constrained caller's search and schema discovery.
27
+ * Uses the same classification as dispatch; discovery never grants execution authority.
28
+ * @param required - Per-call identity, live descriptors, and the existing catalog owner.
29
+ * @returns A filtered catalog, or the original catalog for an unconstrained caller.
30
+ * @example filterReadOnlyToolCatalog({ principal, registry, catalog });
31
+ * @complexity O(n + h) per search for n live registrations and h ranked hits. Overfetch is bounded
32
+ * by registered tool count so withheld writes cannot displace permitted reads at a small limit.
33
+ */
34
+ export function filterReadOnlyToolCatalog(required, _optional = {}) {
35
+ if (!principalIsReadOnlyConstrained({ principal: required.principal }))
36
+ return required.catalog;
37
+ const allowedTools = () => new Map(required.registry.list({})
38
+ .filter(descriptor => isReadOnlyTool({ descriptor })).map(descriptor => [descriptor.id, descriptor]));
39
+ return {
40
+ search(input, { limit = 10 } = {}) {
41
+ const allowed = allowedTools();
42
+ return required.catalog.search(input, { limit: Math.max(limit, required.registry.list({}).length) })
43
+ .filter(hit => allowed.has(hit.id)).slice(0, limit)
44
+ .map(hit => ({ ...hit, ...toolDiscoveryMetadata({ descriptor: allowed.get(hit.id) }) }));
45
+ },
46
+ describe(input) {
47
+ const descriptor = allowedTools().get(input.id);
48
+ const entry = descriptor ? required.catalog.describe(input) : null;
49
+ return entry ? { ...entry, ...toolDiscoveryMetadata({ descriptor }) } : null;
50
+ },
51
+ };
52
+ }
25
53
  /** @returns Required host wording for a registered non-read-only tool. */
26
54
  export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {}) {
27
55
  return messages.toolRefusalMessage({ toolId });
@@ -13,7 +13,9 @@ export interface ToolAttemptEvent {
13
13
  readonly principalId: string;
14
14
  readonly phase: ToolAttemptPhase;
15
15
  readonly at: string;
16
- /** Value-free metadata; never raw input, query, output or error text. */
16
+ /** Value-free metadata; never raw input, query, output or error text.
17
+ * Reserved native_tool_gap rows instead carry redacted command/path metadata (ToolGapEntry).
18
+ * They never carry file bodies, tool output or full edit patches. */
17
19
  readonly detail?: string | null;
18
20
  }
19
21
  export interface ToolAttemptAuditSink {
@@ -33,6 +33,35 @@ export function describeInput({ input }, _optional = {}) {
33
33
  return 'input: an unreadable object';
34
34
  }
35
35
  }
36
+ /** Forward only the channels the caller supplied; an explicit undefined key is never forwarded. */
37
+ function forwardedOptions({ signal, emitSurface }, _optional = {}) {
38
+ return {
39
+ ...(signal === undefined ? {} : { signal }),
40
+ ...(emitSurface === undefined ? {} : { emitSurface }),
41
+ };
42
+ }
43
+ /** Label a thrown delegation by phase and value-free detail. */
44
+ function thrownAttempt({ error }, _optional = {}) {
45
+ if (!(error instanceof Error))
46
+ return { phase: 'failed', detail: typeof error };
47
+ // The boundary exposes no typed unknown-tool error. Wording drift loses only label
48
+ // precision, never the row or execution behavior; retain the error class, not its message.
49
+ return { phase: /unknown tool/i.test(error.message) ? 'unknown-tool' : 'failed', detail: error.name };
50
+ }
51
+ /** Settled details retain truncation or an errorId link, never error text or operator content. */
52
+ function settledDetail({ result, readErrorId }, options) {
53
+ if (result.truncated)
54
+ return 'output truncated';
55
+ // Host error-ID metadata is optional observation, never part of execution success.
56
+ try {
57
+ const id = readErrorId({ result });
58
+ return id === undefined ? null : `errorId=${id}`;
59
+ }
60
+ catch (error) {
61
+ reportSinkError(error, options);
62
+ return null;
63
+ }
64
+ }
36
65
  /** Append requested BEFORE delegation: unknown tools and throwing authorization have no execution
37
66
  * record yet, and wrapping a handler would miss denied/confirmed attempts. The executor's own
38
67
  * in-memory audit remains authoritative for getAuditRecord; an injected sink can retain history.
@@ -47,36 +76,20 @@ export function withToolAttemptAudit(deps, options = {}) {
47
76
  await appendSafely({ ...base, executionId: null, phase: 'requested', at: deps.now(), detail: describeInput({ input }) }, deps, options);
48
77
  let result;
49
78
  try {
50
- result = await deps.inner.execute({ principal, run, toolId, input }, {
51
- ...(signal === undefined ? {} : { signal }),
52
- ...(emitSurface === undefined ? {} : { emitSurface }),
53
- });
79
+ result = await deps.inner.execute({ principal, run, toolId, input }, forwardedOptions({ signal, emitSurface }));
54
80
  }
55
81
  catch (error) {
56
- // The boundary exposes no typed unknown-tool error. Wording drift loses only label
57
- // precision, never the row or execution behavior; retain the error class, not its message.
58
- const phase = error instanceof Error && /unknown tool/i.test(error.message) ? 'unknown-tool' : 'failed';
59
- await appendSafely({ ...base, executionId: null, phase, at: deps.now(), detail: error instanceof Error ? error.name : typeof error }, deps, options);
82
+ const { phase, detail } = thrownAttempt({ error });
83
+ await appendSafely({ ...base, executionId: null, phase, at: deps.now(), detail }, deps, options);
60
84
  throw error;
61
85
  }
62
- let detail = result.truncated ? 'output truncated' : null;
63
- // Host error-ID metadata is optional observation, never part of execution success.
64
- if (detail === null) {
65
- try {
66
- const id = deps.readErrorId({ result });
67
- if (id !== undefined)
68
- detail = `errorId=${id}`;
69
- }
70
- catch (error) {
71
- reportSinkError(error, options);
72
- }
73
- }
86
+ const detail = settledDetail({ result, readErrorId: deps.readErrorId }, options);
74
87
  await appendSafely({ ...base, executionId: result.executionId, phase: result.status, at: deps.now(), detail }, deps, options);
75
88
  return result;
76
89
  },
77
- resumeConfirmation: ({ executionId, decision }) => deps.inner.resumeConfirmation({ executionId: executionId, decision: decision }),
90
+ resumeConfirmation: ({ executionId, decision }) => deps.inner.resumeConfirmation({ executionId, decision }),
78
91
  cancel: ({ executionId }) => deps.inner.cancel({ executionId }),
79
- getAuditRecord: ({ executionId }) => deps.inner.getAuditRecord({ executionId: executionId }),
92
+ getAuditRecord: ({ executionId }) => deps.inner.getAuditRecord({ executionId }),
80
93
  };
81
94
  }
82
95
  /** Keep ids in returned rank order and record queryLength/limit, never raw query text, scores
@@ -0,0 +1,60 @@
1
+ import type { RunProtocolEvent } from '@jini-ai/protocol';
2
+ import type { ToolAttemptAuditSink, ToolAttemptEvent } from './tool-audit.js';
3
+ /** Reserved audit marker; detail is a redacted ToolGapEntry, never tool output or file content. */
4
+ export declare const TOOL_GAP_AUDIT_ID = "native_tool_gap";
5
+ export interface ToolGapEntry {
6
+ type: 'tool_gap' | 'native_tool_used';
7
+ id: string;
8
+ at: string;
9
+ site: string;
10
+ chatId: string;
11
+ runId: string;
12
+ provider: string;
13
+ toolUsed: string;
14
+ summary: string;
15
+ gapLine: string | null;
16
+ }
17
+ /** Decode only the public metadata shape; corrupt/older audit rows are skipped. Pure, O(n) JSON. */
18
+ export declare function decodeToolGapEntry(required: {
19
+ json: string | null;
20
+ }, _optional?: {}): ToolGapEntry | null;
21
+ /** Project the in-memory audit adapter with the SQL reader's same latest-revision semantics.
22
+ * Pure, O(n) over retained audit events; bounded output, no provider or database coupling. */
23
+ export declare function toolGapsFromAuditEvents(required: {
24
+ events: readonly ToolAttemptEvent[];
25
+ workspaceId: string;
26
+ principalId: string;
27
+ }, optional?: {
28
+ limit?: number;
29
+ }): readonly ToolGapEntry[];
30
+ /** Extract command/path metadata only. File bodies, patches and tool output never enter audit.
31
+ * Pure, O(n) in the tool input size. Redaction must precede truncation at the collector boundary. */
32
+ export declare function nativeFallbackSummary(required: {
33
+ name: string;
34
+ input: unknown;
35
+ }, _optional?: {}): string | null;
36
+ /** Observe normalized CLI events, independent of model cooperation. Writes immediately at tool use;
37
+ * a later explanation appends a revision with the same id. One collector per live run.
38
+ * Sink/redactor are injected; O(n) text work, O(g) identity state for g fallbacks in the run.
39
+ * @example const audit = createToolGapCollector({ ...identity, sink, redact }, {});
40
+ */
41
+ export declare function createToolGapCollector(required: {
42
+ sink: ToolAttemptAuditSink;
43
+ workspaceId: string;
44
+ principalId: string;
45
+ site: string;
46
+ chatId: string;
47
+ runId: string;
48
+ provider: string;
49
+ redact: (text: string) => string;
50
+ }, optional?: {
51
+ onError?: (error: unknown) => void;
52
+ }): {
53
+ /** Called synchronously from the run's existing subscription; no recursive run events. */
54
+ observe({ event }: {
55
+ event: RunProtocolEvent;
56
+ }, _options?: {}): void;
57
+ /** Await outstanding audit writes during graceful shutdown/tests. */
58
+ flush: () => Promise<void>;
59
+ };
60
+ //# sourceMappingURL=tool-gaps.d.ts.map
@@ -0,0 +1,199 @@
1
+ /** Reserved audit marker; detail is a redacted ToolGapEntry, never tool output or file content. */
2
+ export const TOOL_GAP_AUDIT_ID = 'native_tool_gap';
3
+ const GAP_ENTRY_STRING_KEYS = ['id', 'at', 'site', 'chatId', 'runId', 'provider', 'toolUsed', 'summary'];
4
+ /** True when a parsed row carries every public metadata field; older rows may omit `type`. */
5
+ function hasToolGapShape(entry) {
6
+ if (!GAP_ENTRY_STRING_KEYS.every(key => typeof entry[key] === 'string'))
7
+ return false;
8
+ if (entry.gapLine !== null && typeof entry.gapLine !== 'string')
9
+ return false;
10
+ return entry.type === undefined || entry.type === 'tool_gap' || entry.type === 'native_tool_used';
11
+ }
12
+ /** Decode only the public metadata shape; corrupt/older audit rows are skipped. Pure, O(n) JSON. */
13
+ export function decodeToolGapEntry(required, _optional = {}) {
14
+ try {
15
+ const entry = JSON.parse(required.json ?? 'null');
16
+ if (!entry || typeof entry !== 'object' || !hasToolGapShape(entry))
17
+ return null;
18
+ return { ...entry, type: entry.type ?? 'tool_gap' };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /** Default 100, at most 200, at least 1; zero/NaN/fractions below 1 fall back to the default. */
25
+ function clampGapLimit(limit) {
26
+ return Math.max(1, Math.min(200, Math.trunc(limit ?? 100) || 100));
27
+ }
28
+ /** A reserved gap row owned by the requested workspace and principal. */
29
+ function isGapRowFor(row, owner) {
30
+ return row.toolId === TOOL_GAP_AUDIT_ID && row.workspaceId === owner.workspaceId && row.principalId === owner.principalId;
31
+ }
32
+ /** Project the in-memory audit adapter with the SQL reader's same latest-revision semantics.
33
+ * Pure, O(n) over retained audit events; bounded output, no provider or database coupling. */
34
+ export function toolGapsFromAuditEvents(required, optional = {}) {
35
+ const limit = clampGapLimit(optional.limit);
36
+ const entries = new Map();
37
+ for (let index = required.events.length - 1; index >= 0 && entries.size < limit; index--) {
38
+ const row = required.events[index];
39
+ if (!isGapRowFor(row, required))
40
+ continue;
41
+ const entry = decodeToolGapEntry({ json: row.detail ?? null }, {});
42
+ if (entry && !entries.has(entry.id))
43
+ entries.set(entry.id, entry);
44
+ }
45
+ return [...entries.values()];
46
+ }
47
+ const CATALOG_BRIDGE_TOOLS = new Set(['search_tools', 'describe_tool', 'execute_delegated_tool', 'execute_readonly_delegated_tool']);
48
+ const FALLBACK_NAMES = new Set([
49
+ 'Bash', 'BashOutput', 'KillShell', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit',
50
+ 'shell', 'shell_command', 'exec_command', 'write_stdin', 'run_shell_command',
51
+ 'apply_patch', 'write_file', 'replace', 'edit_file', 'file_change',
52
+ ]);
53
+ /** Extract command/path metadata only. File bodies, patches and tool output never enter audit.
54
+ * Pure, O(n) in the tool input size. Redaction must precede truncation at the collector boundary. */
55
+ export function nativeFallbackSummary(required, _optional = {}) {
56
+ const name = required.name.replace(/^functions\./, '');
57
+ if (!FALLBACK_NAMES.has(name))
58
+ return null;
59
+ if (typeof required.input === 'string')
60
+ return summarizeTextInput(name, required.input);
61
+ return summarizeStructuredInput(required.input);
62
+ }
63
+ const NO_COMMAND_OR_PATH = '[no command/path reported]';
64
+ /** Raw text input is omitted, except patch file headers. */
65
+ function summarizeTextInput(name, text) {
66
+ if (name === 'apply_patch')
67
+ return text.split('\n').filter(line => /^\*\*\* (?:Add|Update|Delete) File:/.test(line)).join('; ');
68
+ return '[native tool input omitted]';
69
+ }
70
+ /** Prefer a command, then the first reported path field. */
71
+ function summarizeStructuredInput(value) {
72
+ const input = value;
73
+ if (!input || typeof input !== 'object')
74
+ return NO_COMMAND_OR_PATH;
75
+ const command = input.command ?? input.cmd;
76
+ if (typeof command === 'string')
77
+ return command;
78
+ return summarizePaths(input.file_path ?? input.path ?? input.notebook_path ?? input.paths);
79
+ }
80
+ function summarizePaths(paths) {
81
+ if (typeof paths === 'string')
82
+ return paths;
83
+ if (Array.isArray(paths))
84
+ return paths.filter(path => typeof path === 'string').join('; ');
85
+ return NO_COMMAND_OR_PATH;
86
+ }
87
+ /** Observe normalized CLI events, independent of model cooperation. Writes immediately at tool use;
88
+ * a later explanation appends a revision with the same id. One collector per live run.
89
+ * Sink/redactor are injected; O(n) text work, O(g) identity state for g fallbacks in the run.
90
+ * @example const audit = createToolGapCollector({ ...identity, sink, redact }, {});
91
+ */
92
+ export function createToolGapCollector(required, optional = {}) {
93
+ const seen = new Set();
94
+ let tail = '';
95
+ let discardingLine = false;
96
+ let explanation = null;
97
+ let lastGap = null;
98
+ let pending = Promise.resolve();
99
+ const safe = (text) => required.redact(text).slice(0, 500);
100
+ const append = (entry) => {
101
+ pending = pending.then(() => required.sink.append({
102
+ attemptId: entry.id, executionId: null, workspaceId: required.workspaceId,
103
+ principalId: required.principalId, runId: required.runId, toolId: TOOL_GAP_AUDIT_ID,
104
+ phase: 'requested', at: entry.at,
105
+ }, { detail: JSON.stringify(entry) })).catch(error => {
106
+ // Observation must not strand later writes if a host's error reporter itself throws.
107
+ try {
108
+ optional.onError?.(error);
109
+ }
110
+ catch { /* host reporter failed */ }
111
+ });
112
+ };
113
+ const readLine = (line) => {
114
+ const match = /(?:^|\s)Tool gap:\s*(.+)/.exec(line);
115
+ if (!match)
116
+ return;
117
+ // Prefer the next fallback: a missing explanation on an earlier tool must not steal the
118
+ // line announcing a later one. A final explanation is attached on the terminal event.
119
+ explanation = safe(`Tool gap: ${match[1]}`);
120
+ };
121
+ /** Terminal event: read an unfinished line, then attach a final explanation as a revision. */
122
+ const finishRun = () => {
123
+ if (!discardingLine)
124
+ readLine(tail);
125
+ if (lastGap && explanation && lastGap.gapLine === null) {
126
+ lastGap = { ...lastGap, gapLine: explanation };
127
+ append(lastGap);
128
+ }
129
+ tail = '';
130
+ explanation = null;
131
+ };
132
+ const readDelta = (delta) => {
133
+ const lines = (tail + delta).split('\n');
134
+ // String.prototype.split always returns at least one element, so the last piece exists.
135
+ tail = lines.pop();
136
+ for (const line of lines) {
137
+ if (!discardingLine)
138
+ readLine(line);
139
+ discardingLine = false;
140
+ }
141
+ // Discard whole oversized lines rather than cutting a secret token before redaction.
142
+ if (tail.length > 2048)
143
+ discardingLine = true;
144
+ if (discardingLine)
145
+ tail = '';
146
+ };
147
+ const recordNativeTool = (tool, ts) => {
148
+ // Never store native/MCP inputs: they may contain prompts, credentials or document bodies.
149
+ append({ id: `${required.runId}:native:${tool.id}`, type: 'native_tool_used',
150
+ at: new Date(ts).toISOString(), site: required.site, chatId: required.chatId,
151
+ runId: required.runId, provider: required.provider, toolUsed: safe(tool.name),
152
+ summary: 'Native tool used', gapLine: null });
153
+ };
154
+ const recordGap = (tool, summary, ts) => {
155
+ if (!discardingLine)
156
+ readLine(tail);
157
+ tail = '';
158
+ discardingLine = false;
159
+ lastGap = { id: `${required.runId}:gap:${tool.id}`, type: 'tool_gap', at: new Date(ts).toISOString(),
160
+ site: required.site, chatId: required.chatId, runId: required.runId,
161
+ provider: required.provider, toolUsed: safe(tool.name), summary: safe(summary), gapLine: explanation };
162
+ explanation = null;
163
+ append(lastGap);
164
+ };
165
+ const recordToolUse = (tool, ts) => {
166
+ if (seen.has(tool.id))
167
+ return;
168
+ // Catalog bridge calls are already audited by the catalog itself and are never marked seen.
169
+ if (CATALOG_BRIDGE_TOOLS.has(tool.name.replace(/^(?:mcp__jini__|functions\.)/, '')))
170
+ return;
171
+ seen.add(tool.id);
172
+ const summary = nativeFallbackSummary({ name: tool.name, input: tool.input }, {});
173
+ if (summary === null)
174
+ recordNativeTool(tool, ts);
175
+ else
176
+ recordGap(tool, summary, ts);
177
+ };
178
+ return {
179
+ /** Called synchronously from the run's existing subscription; no recursive run events. */
180
+ observe({ event }, _options = {}) {
181
+ if (event.runId !== required.runId)
182
+ return;
183
+ if (event.kind === 'end') {
184
+ finishRun();
185
+ return;
186
+ }
187
+ if (event.kind !== 'agent')
188
+ return;
189
+ const payload = event.payload;
190
+ if (payload.type === 'text_delta')
191
+ readDelta(payload.delta);
192
+ else if (payload.type === 'tool_use')
193
+ recordToolUse(payload, event.ts);
194
+ },
195
+ /** Await outstanding audit writes during graceful shutdown/tests. */
196
+ flush: () => pending,
197
+ };
198
+ }
199
+ //# sourceMappingURL=tool-gaps.js.map
@@ -0,0 +1,26 @@
1
+ import type { CredentialCryptoPort } from './run-credentials.js';
2
+ interface ResultIdentity {
3
+ readonly runId: string;
4
+ readonly toolUseId: string;
5
+ readonly content: readonly unknown[];
6
+ readonly isError: boolean;
7
+ }
8
+ /** Run-scoped proof of a saved bridge result. Implementations retain digests, never pixels. */
9
+ export interface DelegatedResultCorrelation {
10
+ record(input: ResultIdentity, options?: Record<string, never>): void;
11
+ matches(input: ResultIdentity, options?: Record<string, never>): boolean;
12
+ clearRun(input: {
13
+ runId: string;
14
+ }, options?: Record<string, never>): void;
15
+ }
16
+ /** Bounded digest store. Eviction only disables summarization, so legacy output remains safe.
17
+ * Share this instance with bridge/executor/lifecycle; lifecycle eviction clears each run.
18
+ * @complexity O(n) hashing plus JSON key sorting; O(r*c) retained fixed-size digests. */
19
+ export declare function createDelegatedResultCorrelation({ crypto }: {
20
+ crypto: Pick<CredentialCryptoPort, 'digest'>;
21
+ }, { maxRuns, maxCallsPerRun }?: {
22
+ maxRuns?: number;
23
+ maxCallsPerRun?: number;
24
+ }): DelegatedResultCorrelation;
25
+ export {};
26
+ //# sourceMappingURL=delegated-result-correlation.d.ts.map
@@ -0,0 +1,68 @@
1
+ /** Accept only the complete base64 source shape; extra fields must remain part of the digest. */
2
+ function base64ImageSource({ source }, _options = {}) {
3
+ if (typeof source !== 'object' || source === null)
4
+ return undefined;
5
+ const image = source;
6
+ if (image.type !== 'base64' || typeof image.media_type !== 'string' || typeof image.data !== 'string' || Object.keys(image).length !== 3)
7
+ return undefined;
8
+ return { data: image.data, media_type: image.media_type };
9
+ }
10
+ /** Convert only source-only images; dual representations and extra source fields stay in the proof. */
11
+ function normalizeSourceImage({ record }, _options = {}) {
12
+ if (record.type !== 'image' || 'data' in record || 'mimeType' in record)
13
+ return record;
14
+ const image = base64ImageSource({ source: record.source });
15
+ if (!image)
16
+ return record;
17
+ const { source: _source, ...rest } = record;
18
+ return { ...rest, data: image.data, mimeType: image.media_type };
19
+ }
20
+ /** Normalize unambiguous CLI images and JSON object key order.
21
+ * @complexity O(n + k log k) for n bytes and k keys per object; inputs are wire JSON. */
22
+ function normalizedJson(_key, value) {
23
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
24
+ return value;
25
+ const record = normalizeSourceImage({ record: value });
26
+ return Object.fromEntries(Object.keys(record).sort().map(key => [key, record[key]]));
27
+ }
28
+ /** Bounded digest store. Eviction only disables summarization, so legacy output remains safe.
29
+ * Share this instance with bridge/executor/lifecycle; lifecycle eviction clears each run.
30
+ * @complexity O(n) hashing plus JSON key sorting; O(r*c) retained fixed-size digests. */
31
+ export function createDelegatedResultCorrelation({ crypto }, { maxRuns = 256, maxCallsPerRun = 2048 } = {}) {
32
+ if (!Number.isSafeInteger(maxRuns) || maxRuns < 1 || !Number.isSafeInteger(maxCallsPerRun) || maxCallsPerRun < 1)
33
+ throw new Error('correlation limits must be positive integers');
34
+ const runs = new Map();
35
+ // Optional correlation must not turn an otherwise serializable legacy fallback into a failure
36
+ // (for example, a cyclic in-process tool result handled by serializeDelegatedToolOutput).
37
+ const digest = (input) => {
38
+ try {
39
+ return crypto.digest({ token: JSON.stringify({ isError: input.isError, content: input.content }, normalizedJson) });
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ };
45
+ return {
46
+ record(input) {
47
+ const fingerprint = digest(input);
48
+ if (fingerprint === undefined)
49
+ return;
50
+ let calls = runs.get(input.runId);
51
+ if (!calls) {
52
+ if (runs.size >= maxRuns)
53
+ runs.delete(runs.keys().next().value);
54
+ calls = new Map();
55
+ runs.set(input.runId, calls);
56
+ }
57
+ if (calls.size >= maxCallsPerRun)
58
+ calls.delete(calls.keys().next().value);
59
+ calls.set(input.toolUseId, fingerprint);
60
+ },
61
+ matches(input) {
62
+ const expected = runs.get(input.runId)?.get(input.toolUseId);
63
+ return expected !== undefined && expected === digest(input);
64
+ },
65
+ clearRun({ runId }) { runs.delete(runId); },
66
+ };
67
+ }
68
+ //# sourceMappingURL=delegated-result-correlation.js.map
@@ -6,8 +6,9 @@
6
6
  * enforces Jini's registry policy, confirmation, timeout, cancellation, and
7
7
  * audit trail before any registered handler runs.
8
8
  */
9
- import type { Principal } from '@jini-ai/core';
9
+ import { type Principal } from '@jini-ai/core';
10
10
  import type { RunLifecycle } from './run-lifecycle.js';
11
+ import type { DelegatedResultCorrelation } from './delegated-result-correlation.js';
11
12
  import type { ToolExecutionResult, ToolExecutor } from './tool-executor.js';
12
13
  /**
13
14
  * Run-protocol `type` for a human-only surface withheld from a tool result.
@@ -42,24 +43,15 @@ export interface DelegatedToolBridge {
42
43
  export interface CreateDelegatedToolBridgeOptions {
43
44
  readonly lifecycle: RunLifecycle;
44
45
  readonly toolExecutor: ToolExecutor;
46
+ /** Share with the executor and lifecycle only after the host negotiates marker v1. */
47
+ readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
45
48
  }
46
- /** Converts arbitrary registered-tool output into the string-bearing run protocol. */
47
- export declare function serializeDelegatedToolOutput({ output }: {
48
- readonly output: unknown;
49
- }): string;
50
- /**
51
- * Maps a `ToolExecutionResult`'s status to the string a delegated-tool caller (this bridge, or
52
- * gap 3's stdin-tool-result injector in `agent-executor.ts`) reports back as the tool's visible
53
- * output/failure reason. Exported so both real callers share one mapping rather than drifting.
54
- */
55
- export declare function resultContent({ result }: {
56
- readonly result: ToolExecutionResult;
57
- }): string;
49
+ export { serializeDelegatedToolOutput, resultContent } from './tool-result-media.js';
58
50
  /**
59
51
  * Creates the transport-neutral bridge used by future ACP-delegate, MCP, or
60
52
  * other host protocols. This module does not invent a server transport: a
61
53
  * concrete protocol calls this bridge after it has decoded and authenticated a
62
54
  * delegated tool request.
63
55
  */
64
- export declare function createDelegatedToolBridge(options: CreateDelegatedToolBridgeOptions): DelegatedToolBridge;
56
+ export declare function createDelegatedToolBridge(requiredArgs: Pick<CreateDelegatedToolBridgeOptions, 'lifecycle' | 'toolExecutor'>, optionalArgs?: Pick<CreateDelegatedToolBridgeOptions, 'delegatedResultCorrelation'>): DelegatedToolBridge;
65
57
  //# sourceMappingURL=delegated-tool-bridge.d.ts.map