@jini-ai/daemon 0.5.2 → 0.5.4

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 +11 -0
  2. package/dist/agent-executor/contracts.d.ts +26 -20
  3. package/dist/agent-executor/contracts.js +0 -9
  4. package/dist/agent-executor/event-translation.d.ts +2 -2
  5. package/dist/agent-executor/event-translation.js +1 -2
  6. package/dist/agent-executor/events.d.ts +9 -4
  7. package/dist/agent-executor/events.js +29 -7
  8. package/dist/agent-executor/handoff.d.ts +1 -1
  9. package/dist/agent-executor/handoff.js +0 -1
  10. package/dist/agent-executor/launch.d.ts +1 -1
  11. package/dist/agent-executor/launch.js +24 -1
  12. package/dist/agent-executor/mcp-bridge.d.ts +2 -2
  13. package/dist/agent-executor/mcp-bridge.js +0 -2
  14. package/dist/agent-executor/model-receipts.d.ts +6 -0
  15. package/dist/agent-executor/model-receipts.js +26 -0
  16. package/dist/agent-executor.d.ts +8 -1663
  17. package/dist/agent-executor.js +8 -3005
  18. package/dist/attachment-content.d.ts +45 -0
  19. package/dist/attachment-content.js +40 -0
  20. package/dist/core/read-only-tools.d.ts +130 -0
  21. package/dist/core/read-only-tools.js +115 -0
  22. package/dist/core/tool-audit.d.ts +131 -0
  23. package/dist/core/tool-audit.js +113 -0
  24. package/dist/http/active-context.d.ts +2 -2
  25. package/dist/http/agents.d.ts +8 -2
  26. package/dist/http/agents.js +6 -2
  27. package/dist/http/attachments.d.ts +16 -7
  28. package/dist/http/attachments.js +3 -2
  29. package/dist/http/component-catalog.d.ts +2 -2
  30. package/dist/http/connectors.d.ts +17 -17
  31. package/dist/http/daemon-status.d.ts +2 -2
  32. package/dist/http/db-ops.d.ts +3 -3
  33. package/dist/http/delegated-tools.d.ts +1 -1
  34. package/dist/http/delegated-tools.js +1 -1
  35. package/dist/http/frontend-sessions.d.ts +1 -1
  36. package/dist/http/health.d.ts +6 -6
  37. package/dist/http/host-tools.d.ts +2 -2
  38. package/dist/http/media.d.ts +4 -4
  39. package/dist/http/memory.d.ts +15 -15
  40. package/dist/http/remote-run-events.d.ts +2 -2
  41. package/dist/http/research.d.ts +1 -1
  42. package/dist/http/routines.d.ts +7 -7
  43. package/dist/http/run-credentials.d.ts +2 -2
  44. package/dist/http/run-credentials.js +2 -2
  45. package/dist/http/runs.d.ts +4 -4
  46. package/dist/http/terminals.d.ts +6 -6
  47. package/dist/http/tool-catalog.d.ts +2 -2
  48. package/dist/http/xai.d.ts +6 -6
  49. package/dist/index.d.ts +76 -571
  50. package/dist/index.js +76 -571
  51. package/dist/read-only-tools.d.ts +1 -129
  52. package/dist/read-only-tools.js +2 -114
  53. package/dist/run-credentials.d.ts +47 -23
  54. package/dist/run-credentials.js +24 -8
  55. package/dist/run-lifecycle.d.ts +1 -1
  56. package/dist/run-lifecycle.js +6 -5
  57. package/dist/scheduler.d.ts +5 -0
  58. package/dist/scheduler.js +13 -1
  59. package/dist/session-coordination.d.ts +58 -24
  60. package/dist/session-coordination.js +40 -12
  61. package/dist/store/event-log/sqlite.js +3 -3
  62. package/dist/surface-exchanges.d.ts +207 -61
  63. package/dist/surface-exchanges.js +167 -20
  64. package/dist/tool-audit.d.ts +1 -113
  65. package/dist/tool-audit.js +2 -94
  66. package/dist/tool-recovery/diagnostics.d.ts +132 -0
  67. package/dist/tool-recovery/diagnostics.js +110 -0
  68. package/dist/tool-recovery/federated-refusal.d.ts +92 -0
  69. package/dist/tool-recovery/federated-refusal.js +152 -0
  70. package/dist/tool-recovery/index.d.ts +6 -0
  71. package/dist/tool-recovery/index.js +6 -0
  72. package/dist/tool-recovery/recovery.d.ts +49 -0
  73. package/dist/tool-recovery/recovery.js +419 -0
  74. package/dist/tool-recovery/redaction.d.ts +166 -0
  75. package/dist/tool-recovery/redaction.js +192 -0
  76. package/dist/tool-recovery/stack.d.ts +69 -0
  77. package/dist/tool-recovery/stack.js +62 -0
  78. package/package.json +35 -12
@@ -0,0 +1,45 @@
1
+ export interface MessageAttachmentImage {
2
+ readonly mimeType: string;
3
+ readonly data: string;
4
+ }
5
+ export interface MessageAttachmentSource {
6
+ readonly name: string;
7
+ readonly mimeType: string;
8
+ readonly bytes: Uint8Array;
9
+ readonly path?: string;
10
+ }
11
+ export interface MessageAttachmentReader {
12
+ (required: {
13
+ ref: string;
14
+ }, optional: Record<string, never>): Promise<MessageAttachmentSource>;
15
+ }
16
+ /** One byte-bearing message path for CLI and API consumers. The host reader owns authorization,
17
+ * integrity and MIME sniffing; this module never opens a caller-selected path itself. Unsupported
18
+ * files are explicit so the model cannot mistake a filename for content it has received. */
19
+ export declare function prepareMessageAttachments({ refs, read }: {
20
+ refs: readonly string[];
21
+ read: MessageAttachmentReader;
22
+ }, { fileAccess, imageAccess }?: {
23
+ fileAccess?: boolean;
24
+ imageAccess?: boolean;
25
+ }): Promise<{
26
+ images: MessageAttachmentImage[];
27
+ imagePaths: string[];
28
+ notice: string;
29
+ }>;
30
+ /** Stream-JSON runtimes use the same normalized pixels as provider APIs. */
31
+ export declare function messageContentWithImages({ prompt, images }: {
32
+ prompt: string;
33
+ images: readonly MessageAttachmentImage[];
34
+ }, _optional?: {}): ({
35
+ type: "image";
36
+ source: {
37
+ type: "base64";
38
+ media_type: string;
39
+ data: string;
40
+ };
41
+ } | {
42
+ type: "text";
43
+ text: string;
44
+ })[];
45
+ //# sourceMappingURL=attachment-content.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { Buffer } from 'node:buffer';
2
+ /** One byte-bearing message path for CLI and API consumers. The host reader owns authorization,
3
+ * integrity and MIME sniffing; this module never opens a caller-selected path itself. Unsupported
4
+ * files are explicit so the model cannot mistake a filename for content it has received. */
5
+ export async function prepareMessageAttachments({ refs, read }, { fileAccess = false, imageAccess = true } = {}) {
6
+ const images = [];
7
+ const notices = [];
8
+ const imagePaths = [];
9
+ for (const ref of refs) {
10
+ 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;
20
+ }
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}.`);
26
+ }
27
+ else {
28
+ notices.push(`${source.name}: ${video ? 'video' : 'file'} attachments are unsupported by this runtime.`);
29
+ }
30
+ }
31
+ return { images, imagePaths, notice: notices.length ? `\n\nAttachments to this message:\n${notices.join('\n')}` : '' };
32
+ }
33
+ /** Stream-JSON runtimes use the same normalized pixels as provider APIs. */
34
+ export function messageContentWithImages({ prompt, images }, _optional = {}) {
35
+ return [
36
+ { type: 'text', text: prompt },
37
+ ...images.map(image => ({ type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } })),
38
+ ];
39
+ }
40
+ //# sourceMappingURL=attachment-content.js.map
@@ -0,0 +1,130 @@
1
+ import { type Principal, type ToolRegistry } from '@jini-ai/core';
2
+ import type { ToolExecutor } from '../tool-executor.js';
3
+ import type { IdGenerator } from '@jini-ai/core/primitives';
4
+ /**
5
+ * A `Principal` narrowed to tools that only read.
6
+ *
7
+ * An HTTP route preflight only ever validates the OUTER `toolId` — the one the caller
8
+ * named in the HTTP request. It cannot see what a consumer's own `ToolExecutor` composition does
9
+ * once `bridge.execute()` hands control to `deps.toolExecutor`: a decorator that composition
10
+ * stacks on top of the bare executor (a recovery loop that retries through a different tool id
11
+ * after a failure, for instance) can dispatch a tool the caller never named, using the exact same
12
+ * `principal` the outer call carried — one hop past the only check the HTTP route ever ran. Adding a
13
+ * second request-shaped flag would not close that gap either: it would have to be threaded, by
14
+ * hand, through every decorator between here and the handler, and a decorator that forgot would
15
+ * fail open silently. `principal` is the one value every layer already passes unchanged, so the
16
+ * constraint travels as an ATTENUATED IDENTITY instead — a principal permitted to invoke only
17
+ * read-only tools, checkable by {@link refuseNonReadOnlyDispatch} at any depth a consumer's own
18
+ * composition cares to ask.
19
+ *
20
+ * A distinct field rather than a `Principal.roles` entry: `roles` is what a `ToolPolicy` branches
21
+ * on, and a synthetic role would silently join that decision for every registration in the
22
+ * process. This field is inert to everything except {@link refuseNonReadOnlyDispatch}.
23
+ */
24
+ export interface ReadOnlyConstrainedPrincipal extends Principal {
25
+ readonly toolAccess: 'read-only';
26
+ }
27
+ /**
28
+ * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes share
29
+ * one refusal: neither corroborates a safe read, and catalog discovery already reveals existence.
30
+ * Missing verification metadata always refuses, naming the missing wiring so the host can fix it.
31
+ */
32
+ export declare const defaultDaemonMessages: {
33
+ readOnly: {
34
+ unverifiableMessage: string;
35
+ toolRefusalMessage: ({ toolId }: {
36
+ toolId: string;
37
+ }) => string;
38
+ };
39
+ };
40
+ /** Required host wording; absent metadata is always a refusal. */
41
+ export interface ReadOnlyToolMessages {
42
+ readonly unverifiableMessage: string;
43
+ toolRefusalMessage(required: {
44
+ toolId: string;
45
+ }): string;
46
+ }
47
+ export interface ReadOnlyToolCheckRequired {
48
+ readonly toolId: string;
49
+ readonly registry: Pick<ToolRegistry, 'list'> | undefined;
50
+ readonly messages: ReadOnlyToolMessages;
51
+ }
52
+ export interface ReadOnlyToolConstraintRequired {
53
+ readonly inner: ToolExecutor;
54
+ readonly registry: Pick<ToolRegistry, 'list'> | undefined;
55
+ readonly idGenerator: IdGenerator;
56
+ readonly messages: ReadOnlyToolMessages;
57
+ }
58
+ /** @returns A copied, attenuated principal; its roles are unchanged. */
59
+ export declare function constrainPrincipalToReadOnlyTools({ principal }: {
60
+ principal: Principal;
61
+ }, _optional?: Record<string, never>): ReadOnlyConstrainedPrincipal;
62
+ /** @returns Whether attenuation survived the executor/decorator boundary. */
63
+ export declare function principalIsReadOnlyConstrained({ principal }: {
64
+ principal: Principal;
65
+ }, _optional?: Record<string, never>): boolean;
66
+ /** @returns Required host wording for an unknown or non-read-only tool. */
67
+ export declare function readOnlyToolRefusalMessage({ toolId, messages }: {
68
+ toolId: string;
69
+ messages: ReadOnlyToolMessages;
70
+ }, _optional?: Record<string, never>): string;
71
+ /**
72
+ * Single read-only registration decision, shared by routes and inner dispatch.
73
+ * @returns Null for an explicitly read-only registration, otherwise host refusal text.
74
+ */
75
+ export declare function checkReadOnlyTool({ toolId, registry, messages }: ReadOnlyToolCheckRequired, _optional?: Record<string, never>): string | null;
76
+ /**
77
+ * THE read-only dispatch decision for any tool id a consumer's own `ToolExecutor` composition is
78
+ * about to invoke — the outer call the route already checked, or a nested one a decorator issues
79
+ * one or more hops in. Every consumer's own composition should ask this before dispatching on
80
+ * behalf of a principal it did not itself resolve; none should re-implement the check, so changing
81
+ * what the constraint means stays one edit.
82
+ *
83
+ * @param required.principal - The principal the dispatch would run under. Unconstrained ⇒ always
84
+ * `null` — this is a no-op for every pre-existing caller that never attenuates a principal.
85
+ * @param required.toolId - The id actually about to be dispatched, which is not necessarily the id
86
+ * named in the original request — that difference is the whole reason this function exists.
87
+ * @param required.registry - Descriptors to resolve `toolId` against; `undefined` ⇒ refuse (an
88
+ * unverifiable constrained dispatch is refused, never waived, matching `checkReadOnlyTool`).
89
+ * @returns `null` when the dispatch may proceed, otherwise the refusal text to report.
90
+ * @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a constrained
91
+ * one, since `ToolRegistry` exposes enumeration rather than lookup by id.
92
+ */
93
+ export declare function refuseNonReadOnlyDispatch(required: ReadOnlyToolCheckRequired & {
94
+ readonly principal: Principal;
95
+ }, _optional?: Record<string, never>): string | null;
96
+ /** @returns Caller-formatted recovery refusal, preserving the original refusal verbatim as input. */
97
+ export declare function readOnlyRemedyRefusalMessage({ refusal, formatMessage }: {
98
+ refusal: string;
99
+ formatMessage: (required: {
100
+ refusal: string;
101
+ }) => string;
102
+ }, _optional?: Record<string, never>): string;
103
+ /**
104
+ * Wraps `inner` so a read-only-constrained principal (one {@link constrainPrincipalToReadOnlyTools}
105
+ * attenuated) cannot dispatch a tool that isn't registered read-only — whichever layer chose the
106
+ * id. This is what turns the attenuation into an actual gate rather than a field nobody reads:
107
+ * {@link refuseNonReadOnlyDispatch} is the decision, this is its enforcement point.
108
+ *
109
+ * **Compose this as close to the bare `ToolExecutor` as your own stack allows** — directly around
110
+ * `@jini-ai/daemon`'s `createToolExecutor` output, beneath any decorator of your own that might
111
+ * dispatch a tool id the caller never named (a retry, a recovery loop that calls a different tool
112
+ * as a remedy, ...). Every such decorator's own `inner.execute` call still has to come through here
113
+ * to reach a handler, which is what makes this a gate on the CLASS of defect (an unnamed nested
114
+ * dispatch) rather than on one instance of it.
115
+ *
116
+ * `delegatedToolExecuteRoute`'s own `handle` additionally composes this around whatever
117
+ * `ToolExecutor` a host supplies, whenever a call attenuates its principal. That outer composition
118
+ * only re-covers the SAME outer `toolId` the route preflight already checked — it
119
+ * cannot see a nested dispatch a host's own inner decorator issues, because that decorator holds a
120
+ * reference to whatever `ToolExecutor` IT was built with, not to the HTTP route's wrapped one. Closing
121
+ * the nested case for a given host's stack requires that host to compose this decorator itself,
122
+ * innermost, in its own `ToolExecutor` construction — this export exists so it can, without having
123
+ * to rediscover or reimplement the mechanism.
124
+ *
125
+ * @returns A drop-in `ToolExecutor`; `resumeConfirmation`/`cancel`/`getAuditRecord` delegate
126
+ * straight through.
127
+ * @complexity One extra property read per unconstrained call; one registry scan per constrained one.
128
+ */
129
+ export declare function withReadOnlyToolConstraint({ inner, registry, idGenerator, messages }: ReadOnlyToolConstraintRequired, _optional?: Record<string, never>): ToolExecutor;
130
+ //# sourceMappingURL=read-only-tools.d.ts.map
@@ -0,0 +1,115 @@
1
+ import { isReadOnlyTool } from '@jini-ai/core';
2
+ /**
3
+ * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes share
4
+ * one refusal: neither corroborates a safe read, and catalog discovery already reveals existence.
5
+ * Missing verification metadata always refuses, naming the missing wiring so the host can fix it.
6
+ */
7
+ export const defaultDaemonMessages = {
8
+ readOnly: {
9
+ unverifiableMessage: 'this host cannot verify read-only tools — POST /api/delegated-tool-calls was mounted without DelegatedToolsHttpDeps.toolRegistry, so a requireReadOnly call cannot be checked and is refused',
10
+ toolRefusalMessage: ({ toolId }) => `tool "${toolId}" is not registered as read-only — this gateway executes only tools whose registration declares readOnly; call it through execute_delegated_tool instead`,
11
+ },
12
+ };
13
+ /** @returns A copied, attenuated principal; its roles are unchanged. */
14
+ export function constrainPrincipalToReadOnlyTools({ principal }, _optional = {}) {
15
+ return { ...principal, toolAccess: 'read-only' };
16
+ }
17
+ // Read structurally so passing through layers typed as the base Principal cannot erase the
18
+ // execution's authority constraint.
19
+ /** @returns Whether attenuation survived the executor/decorator boundary. */
20
+ export function principalIsReadOnlyConstrained({ principal }, _optional = {}) {
21
+ return 'toolAccess' in principal && principal.toolAccess === 'read-only';
22
+ }
23
+ /** @returns Required host wording for an unknown or non-read-only tool. */
24
+ export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {}) {
25
+ return messages.toolRefusalMessage({ toolId });
26
+ }
27
+ // Unknown registrations and omitted read-only classification both lack evidence of safety.
28
+ // Every dispatch/preflight consults this decision; recovery preflight prevents asking a human
29
+ // for form input that the innermost gate would refuse after submission.
30
+ /**
31
+ * Single read-only registration decision, shared by routes and inner dispatch.
32
+ * @returns Null for an explicitly read-only registration, otherwise host refusal text.
33
+ */
34
+ export function checkReadOnlyTool({ toolId, registry, messages }, _optional = {}) {
35
+ if (registry === undefined)
36
+ return messages.unverifiableMessage;
37
+ const descriptor = registry.list({}).find((candidate) => candidate.id === toolId);
38
+ if (isReadOnlyTool({ descriptor }))
39
+ return null;
40
+ return readOnlyToolRefusalMessage({ toolId, messages });
41
+ }
42
+ /**
43
+ * THE read-only dispatch decision for any tool id a consumer's own `ToolExecutor` composition is
44
+ * about to invoke — the outer call the route already checked, or a nested one a decorator issues
45
+ * one or more hops in. Every consumer's own composition should ask this before dispatching on
46
+ * behalf of a principal it did not itself resolve; none should re-implement the check, so changing
47
+ * what the constraint means stays one edit.
48
+ *
49
+ * @param required.principal - The principal the dispatch would run under. Unconstrained ⇒ always
50
+ * `null` — this is a no-op for every pre-existing caller that never attenuates a principal.
51
+ * @param required.toolId - The id actually about to be dispatched, which is not necessarily the id
52
+ * named in the original request — that difference is the whole reason this function exists.
53
+ * @param required.registry - Descriptors to resolve `toolId` against; `undefined` ⇒ refuse (an
54
+ * unverifiable constrained dispatch is refused, never waived, matching `checkReadOnlyTool`).
55
+ * @returns `null` when the dispatch may proceed, otherwise the refusal text to report.
56
+ * @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a constrained
57
+ * one, since `ToolRegistry` exposes enumeration rather than lookup by id.
58
+ */
59
+ export function refuseNonReadOnlyDispatch(required, _optional = {}) {
60
+ if (!principalIsReadOnlyConstrained({ principal: required.principal }))
61
+ return null;
62
+ return checkReadOnlyTool(required);
63
+ }
64
+ // Report the original failure alongside the refused remedy; silently hiding an automatic
65
+ // recovery attempt would mislead the caller even when the constraint correctly prevents writes.
66
+ /** @returns Caller-formatted recovery refusal, preserving the original refusal verbatim as input. */
67
+ export function readOnlyRemedyRefusalMessage({ refusal, formatMessage }, _optional = {}) {
68
+ return formatMessage({ refusal });
69
+ }
70
+ // A route can validate the caller's read-only tool and still miss a recovery decorator choosing
71
+ // a writing remedy afterward. Enforce on every actual dispatch at the innermost executor to
72
+ // close that class of authority laundering, including dispatchers introduced later.
73
+ /**
74
+ * Wraps `inner` so a read-only-constrained principal (one {@link constrainPrincipalToReadOnlyTools}
75
+ * attenuated) cannot dispatch a tool that isn't registered read-only — whichever layer chose the
76
+ * id. This is what turns the attenuation into an actual gate rather than a field nobody reads:
77
+ * {@link refuseNonReadOnlyDispatch} is the decision, this is its enforcement point.
78
+ *
79
+ * **Compose this as close to the bare `ToolExecutor` as your own stack allows** — directly around
80
+ * `@jini-ai/daemon`'s `createToolExecutor` output, beneath any decorator of your own that might
81
+ * dispatch a tool id the caller never named (a retry, a recovery loop that calls a different tool
82
+ * as a remedy, ...). Every such decorator's own `inner.execute` call still has to come through here
83
+ * to reach a handler, which is what makes this a gate on the CLASS of defect (an unnamed nested
84
+ * dispatch) rather than on one instance of it.
85
+ *
86
+ * `delegatedToolExecuteRoute`'s own `handle` additionally composes this around whatever
87
+ * `ToolExecutor` a host supplies, whenever a call attenuates its principal. That outer composition
88
+ * only re-covers the SAME outer `toolId` the route preflight already checked — it
89
+ * cannot see a nested dispatch a host's own inner decorator issues, because that decorator holds a
90
+ * reference to whatever `ToolExecutor` IT was built with, not to the HTTP route's wrapped one. Closing
91
+ * the nested case for a given host's stack requires that host to compose this decorator itself,
92
+ * innermost, in its own `ToolExecutor` construction — this export exists so it can, without having
93
+ * to rediscover or reimplement the mechanism.
94
+ *
95
+ * @returns A drop-in `ToolExecutor`; `resumeConfirmation`/`cancel`/`getAuditRecord` delegate
96
+ * straight through.
97
+ * @complexity One extra property read per unconstrained call; one registry scan per constrained one.
98
+ */
99
+ export function withReadOnlyToolConstraint({ inner, registry, idGenerator, messages }, _optional = {}) {
100
+ return {
101
+ execute: async (requiredArgs, optionalArgs = {}) => {
102
+ const { principal, toolId } = requiredArgs;
103
+ const refusal = refuseNonReadOnlyDispatch({ principal, toolId, registry, messages });
104
+ // Deny before inner: no handler or kernel audit record exists. Use the authorization result
105
+ // union instead of throwing, and mint a denial ID because there is no inner execution ID.
106
+ if (refusal !== null)
107
+ return { executionId: idGenerator.newId(), status: 'denied', error: refusal };
108
+ return inner.execute(requiredArgs, optionalArgs);
109
+ },
110
+ resumeConfirmation: (requiredArgs) => inner.resumeConfirmation(requiredArgs),
111
+ cancel: (requiredArgs) => inner.cancel(requiredArgs),
112
+ getAuditRecord: (requiredArgs) => inner.getAuditRecord(requiredArgs),
113
+ };
114
+ }
115
+ //# sourceMappingURL=read-only-tools.js.map
@@ -0,0 +1,131 @@
1
+ import type { Principal, RunRef, SurfaceEmitter } from '@jini-ai/core';
2
+ import type { ConfirmationDecision, ToolExecutionAuditRecord, ToolExecutionPhase, ToolExecutionResult, ToolExecutionStatus, ToolExecutor } from '../tool-executor.js';
3
+ /** The daemon vocabulary, plus attempts that never acquired an execution ID. */
4
+ /** Execution telemetry never proves a mutation occurred; domain revisions own write provenance. */
5
+ export type ToolAttemptPhase = Extract<ToolExecutionPhase, 'requested' | ToolExecutionStatus> | 'unknown-tool';
6
+ export declare const TOOL_ATTEMPT_PHASES: readonly ["requested", "completed", "denied", "confirmation-denied", "timed-out", "cancelled", "failed", "unknown-tool"];
7
+ export interface ToolAttemptEvent {
8
+ readonly attemptId: string;
9
+ readonly executionId: string | null;
10
+ readonly workspaceId: string;
11
+ readonly runId: string;
12
+ readonly toolId: string;
13
+ readonly principalId: string;
14
+ readonly phase: ToolAttemptPhase;
15
+ readonly at: string;
16
+ /** Value-free metadata; never raw input, query, output or error text. */
17
+ readonly detail?: string | null;
18
+ }
19
+ export interface ToolAttemptAuditSink {
20
+ append(requiredArgs: Pick<ToolAttemptEvent, "attemptId" | "executionId" | "workspaceId" | "runId" | "toolId" | "principalId" | "phase" | "at">, optionalArgs?: Pick<ToolAttemptEvent, "detail">): Promise<void>;
21
+ }
22
+ export interface ToolAuditDependencies {
23
+ readonly sink: ToolAttemptAuditSink;
24
+ readonly now: () => string;
25
+ readonly newAttemptId: () => string;
26
+ }
27
+ export interface ToolAuditOptions {
28
+ readonly onSinkError?: (error: unknown) => void;
29
+ }
30
+ /** Retain key names and array lengths, never operator values; unreadable objects are summarized
31
+ * without throwing. The durable trail needs only enough metadata to distinguish attempts. */
32
+ export declare function describeInput({ input }: {
33
+ readonly input: unknown;
34
+ }, _optional?: Record<string, never>): string;
35
+ export interface AuditedToolExecutor {
36
+ execute(args: {
37
+ principal: Principal;
38
+ run: RunRef;
39
+ toolId: string;
40
+ input: unknown;
41
+ }, options?: {
42
+ signal?: AbortSignal;
43
+ emitSurface?: SurfaceEmitter;
44
+ }): Promise<ToolExecutionResult>;
45
+ resumeConfirmation(args: {
46
+ executionId: string;
47
+ decision: ConfirmationDecision;
48
+ }, optional?: Record<string, never>): void;
49
+ cancel(args: {
50
+ executionId: string;
51
+ }, optional?: Record<string, never>): void;
52
+ getAuditRecord(args: {
53
+ executionId: string;
54
+ }, optional?: Record<string, never>): ToolExecutionAuditRecord | null;
55
+ }
56
+ /** Append requested BEFORE delegation: unknown tools and throwing authorization have no execution
57
+ * record yet, and wrapping a handler would miss denied/confirmed attempts. The executor's own
58
+ * in-memory audit remains authoritative for getAuditRecord; an injected sink can retain history.
59
+ * Audit observes rather than gates: sink/reporting failures never alter execution outcomes.
60
+ * ADR-021 §2 keeps authorization with ToolPolicy.authorize and the domain evaluator.
61
+ * Forward emitSurface unchanged or human-answer tools would silently take their fallback path.
62
+ * Settled details retain truncation or an errorId link, never error text or operator content. */
63
+ export declare function withToolAttemptAudit(deps: ToolAuditDependencies & {
64
+ readonly inner: ToolExecutor;
65
+ readonly workspaceId: string;
66
+ readonly readErrorId: (args: {
67
+ result: ToolExecutionResult;
68
+ }) => string | undefined;
69
+ }, options?: ToolAuditOptions): AuditedToolExecutor;
70
+ /** Keep ids in returned rank order and record queryLength/limit, never raw query text, scores
71
+ * or descriptions: a pasted secret must not become durable audit content. */
72
+ export declare function searchToolsAuditDetail({ query, limit, hits }: {
73
+ query: string;
74
+ limit: number | null;
75
+ hits: readonly {
76
+ id: string;
77
+ }[];
78
+ }, _optional?: Record<string, never>): string;
79
+ /** Id and found flag suffice; repeating the entry schema/description adds no diagnostic value. */
80
+ export declare function describeToolAuditDetail({ id, entry }: {
81
+ id: string;
82
+ entry: unknown | null;
83
+ }, _optional?: Record<string, never>): string;
84
+ export interface ToolCatalogAuditIdentity {
85
+ readonly workspaceId: string;
86
+ readonly runId: string;
87
+ readonly principalId: string;
88
+ }
89
+ export interface ToolCatalogAttemptEvent extends ToolCatalogAuditIdentity {
90
+ readonly toolId: string;
91
+ readonly detail: string;
92
+ }
93
+ /** Catalog reads bypass ToolExecutor and therefore need their own observation. Fire without
94
+ * awaiting so synchronous catalog calls stay synchronous; appendSafely contains sink failures.
95
+ * Completed is a read phase, not an authorization/confirmation lifecycle. */
96
+ export declare function appendToolCatalogAttempt(deps: ToolAuditDependencies & {
97
+ readonly event: ToolCatalogAttemptEvent;
98
+ }, options?: ToolAuditOptions): void;
99
+ /** Structural source port: no database or catalog-provider dependency. */
100
+ export interface ToolCatalogAuditSource<Hit extends {
101
+ id: string;
102
+ }, Entry> {
103
+ search(args: {
104
+ query: string;
105
+ }, options?: {
106
+ limit?: number;
107
+ }): readonly Hit[];
108
+ describe(args: {
109
+ id: string;
110
+ }, optional?: Record<string, never>): Entry | null;
111
+ }
112
+ /** Preserve catalog results/ranking while appending under the supplied fixed identity. Hosts
113
+ * with per-call attribution use appendToolCatalogAttempt directly; no identity is inferred here. */
114
+ export declare function withToolCatalogAudit<Hit extends {
115
+ id: string;
116
+ }, Entry>(deps: ToolAuditDependencies & {
117
+ catalog: ToolCatalogAuditSource<Hit, Entry>;
118
+ identity: ToolCatalogAuditIdentity;
119
+ searchToolId: string;
120
+ describeToolId: string;
121
+ }, options?: ToolAuditOptions): {
122
+ search({ query }: {
123
+ query: string;
124
+ }, { limit }?: {
125
+ limit?: number;
126
+ }): readonly Hit[];
127
+ describe({ id }: {
128
+ id: string;
129
+ }): Entry | null;
130
+ };
131
+ //# sourceMappingURL=tool-audit.d.ts.map
@@ -0,0 +1,113 @@
1
+ export const TOOL_ATTEMPT_PHASES = [
2
+ 'requested', 'completed', 'denied', 'confirmation-denied', 'timed-out', 'cancelled', 'failed', 'unknown-tool',
3
+ ];
4
+ function reportSinkError(error, options) {
5
+ // An observer's reporter is itself an observer: a throwing reporter cannot gate a tool.
6
+ try {
7
+ options.onSinkError?.(error);
8
+ }
9
+ catch { /* execution remains authoritative */ }
10
+ }
11
+ async function appendSafely(event, deps, options) {
12
+ try {
13
+ await deps.sink.append(event);
14
+ }
15
+ catch (error) {
16
+ reportSinkError(error, options);
17
+ }
18
+ }
19
+ /** Retain key names and array lengths, never operator values; unreadable objects are summarized
20
+ * without throwing. The durable trail needs only enough metadata to distinguish attempts. */
21
+ export function describeInput({ input }, _optional = {}) {
22
+ if (input === null || input === undefined)
23
+ return `input: ${String(input)}`;
24
+ if (Array.isArray(input))
25
+ return `input: an array of ${input.length}`;
26
+ if (typeof input !== 'object')
27
+ return `input: a ${typeof input}`;
28
+ try {
29
+ const parts = Object.entries(input).map(([key, value]) => Array.isArray(value) ? `${key}[${value.length}]` : key).sort();
30
+ return parts.length === 0 ? 'keys: none' : `keys: ${parts.join(', ')}`;
31
+ }
32
+ catch {
33
+ return 'input: an unreadable object';
34
+ }
35
+ }
36
+ /** Append requested BEFORE delegation: unknown tools and throwing authorization have no execution
37
+ * record yet, and wrapping a handler would miss denied/confirmed attempts. The executor's own
38
+ * in-memory audit remains authoritative for getAuditRecord; an injected sink can retain history.
39
+ * Audit observes rather than gates: sink/reporting failures never alter execution outcomes.
40
+ * ADR-021 §2 keeps authorization with ToolPolicy.authorize and the domain evaluator.
41
+ * Forward emitSurface unchanged or human-answer tools would silently take their fallback path.
42
+ * Settled details retain truncation or an errorId link, never error text or operator content. */
43
+ export function withToolAttemptAudit(deps, options = {}) {
44
+ return {
45
+ async execute({ principal, run, toolId, input }, { signal, emitSurface } = {}) {
46
+ const base = { attemptId: deps.newAttemptId(), workspaceId: deps.workspaceId, runId: run.id, toolId, principalId: principal.id };
47
+ await appendSafely({ ...base, executionId: null, phase: 'requested', at: deps.now(), detail: describeInput({ input }) }, deps, options);
48
+ let result;
49
+ try {
50
+ result = await deps.inner.execute({ principal, run, toolId, input }, {
51
+ ...(signal === undefined ? {} : { signal }),
52
+ ...(emitSurface === undefined ? {} : { emitSurface }),
53
+ });
54
+ }
55
+ 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);
60
+ throw error;
61
+ }
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
+ }
74
+ await appendSafely({ ...base, executionId: result.executionId, phase: result.status, at: deps.now(), detail }, deps, options);
75
+ return result;
76
+ },
77
+ resumeConfirmation: ({ executionId, decision }) => deps.inner.resumeConfirmation({ executionId: executionId, decision: decision }),
78
+ cancel: ({ executionId }) => deps.inner.cancel({ executionId }),
79
+ getAuditRecord: ({ executionId }) => deps.inner.getAuditRecord({ executionId: executionId }),
80
+ };
81
+ }
82
+ /** Keep ids in returned rank order and record queryLength/limit, never raw query text, scores
83
+ * or descriptions: a pasted secret must not become durable audit content. */
84
+ export function searchToolsAuditDetail({ query, limit, hits }, _optional = {}) {
85
+ return JSON.stringify({ queryLength: query.length, limit, resultIds: hits.map(hit => hit.id), resultCount: hits.length });
86
+ }
87
+ /** Id and found flag suffice; repeating the entry schema/description adds no diagnostic value. */
88
+ export function describeToolAuditDetail({ id, entry }, _optional = {}) {
89
+ return JSON.stringify({ id, found: entry !== null });
90
+ }
91
+ /** Catalog reads bypass ToolExecutor and therefore need their own observation. Fire without
92
+ * awaiting so synchronous catalog calls stay synchronous; appendSafely contains sink failures.
93
+ * Completed is a read phase, not an authorization/confirmation lifecycle. */
94
+ export function appendToolCatalogAttempt(deps, options = {}) {
95
+ void appendSafely({ ...deps.event, attemptId: deps.newAttemptId(), executionId: null, phase: 'completed', at: deps.now() }, deps, options);
96
+ }
97
+ /** Preserve catalog results/ranking while appending under the supplied fixed identity. Hosts
98
+ * with per-call attribution use appendToolCatalogAttempt directly; no identity is inferred here. */
99
+ export function withToolCatalogAudit(deps, options = {}) {
100
+ return {
101
+ search({ query }, { limit } = {}) {
102
+ const hits = deps.catalog.search({ query }, limit === undefined ? {} : { limit });
103
+ appendToolCatalogAttempt({ ...deps, event: { ...deps.identity, toolId: deps.searchToolId, detail: searchToolsAuditDetail({ query, limit: limit ?? null, hits }) } }, options);
104
+ return hits;
105
+ },
106
+ describe({ id }) {
107
+ const entry = deps.catalog.describe({ id });
108
+ appendToolCatalogAttempt({ ...deps, event: { ...deps.identity, toolId: deps.describeToolId, detail: describeToolAuditDetail({ id, entry }) } }, options);
109
+ return entry;
110
+ },
111
+ };
112
+ }
113
+ //# sourceMappingURL=tool-audit.js.map
@@ -88,13 +88,13 @@ interface ActiveContextRouteDeps {
88
88
  * `POST /api/active` — records (or, with `{active: false}`, clears) the
89
89
  * caller's current resource focus.
90
90
  */
91
- export declare const setActiveRoute: import("@jini-ai/http-kit").JsonRouteSpec<SetActiveInput, SetActiveOutput, ActiveContextRouteDeps>;
91
+ export declare const setActiveRoute: import("@jini-ai/http-kit").JsonRouteSpec<SetActiveInput, SetActiveOutput, ActiveContextRouteDeps, undefined>;
92
92
  /**
93
93
  * `GET /api/active` — reads back the current resource focus, resolving a
94
94
  * display name for it, or `{active: false}` if nothing is set or the
95
95
  * recorded focus has aged past {@link ACTIVE_CONTEXT_TTL_MS}.
96
96
  */
97
- export declare const getActiveRoute: import("@jini-ai/http-kit").JsonRouteSpec<void, GetActiveOutput, ActiveContextRouteDeps>;
97
+ export declare const getActiveRoute: import("@jini-ai/http-kit").JsonRouteSpec<void, GetActiveOutput, ActiveContextRouteDeps, undefined>;
98
98
  /** Mounts both active-context routes on `app`, sharing one in-memory store between them. A pack's `http(app, services)` calls this directly. */
99
99
  export declare function registerActiveContextRoutes({ app, deps, adapter }: {
100
100
  readonly app: Express;
@@ -16,9 +16,12 @@
16
16
  */
17
17
  import type { Express } from 'express';
18
18
  import { type AdapterContext } from '@jini-ai/http-kit';
19
+ import type { ModelCatalogSnapshot, DefaultModelResolution, ModelIdentityKind } from '@jini-ai/agent-runtime';
19
20
  export interface AgentModelSummary {
20
21
  readonly id: string;
21
22
  readonly label: string;
23
+ readonly identityKind?: ModelIdentityKind;
24
+ readonly resolvedId?: string;
22
25
  }
23
26
  /**
24
27
  * Client-safe agent discovery data. Optional probe fields preserve the
@@ -34,7 +37,10 @@ export interface AgentSummary {
34
37
  readonly models?: readonly AgentModelSummary[];
35
38
  readonly reasoningOptions?: readonly AgentModelSummary[];
36
39
  readonly modelsSource?: 'live' | 'fallback';
40
+ readonly modelCatalog?: ModelCatalogSnapshot;
41
+ readonly defaultModelResolution?: DefaultModelResolution;
37
42
  readonly supportsCustomModel?: boolean;
43
+ readonly supportsConcreteModelSelection?: boolean;
38
44
  readonly diagnostic?: string;
39
45
  /**
40
46
  * Whether this runtime can receive external MCP servers (the host application's or this engine's
@@ -56,9 +62,9 @@ export interface AgentListResponse {
56
62
  readonly agents: readonly AgentSummary[];
57
63
  }
58
64
  /** `GET /api/agents` — read-only, no side effects; matches `runListRoute`/`runStatusRoute`'s posture of not requiring same-origin. */
59
- export declare const agentListRoute: import("@jini-ai/http-kit").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps>;
65
+ export declare const agentListRoute: import("@jini-ai/http-kit").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps, undefined>;
60
66
  /** `POST /api/agents/rescan` — explicit state refresh, protected by the local same-origin gate. */
61
- export declare const agentRescanRoute: import("@jini-ai/http-kit").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps>;
67
+ export declare const agentRescanRoute: import("@jini-ai/http-kit").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps, undefined>;
62
68
  /** Mounts the read and explicit-rescan agent discovery routes. */
63
69
  export declare function registerAgentRoutes({ app, deps, adapter }: {
64
70
  readonly app: Express;
@@ -1,9 +1,13 @@
1
1
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
2
2
  import { ok } from '@jini-ai/http-kit';
3
3
  /** `GET /api/agents` — read-only, no side effects; matches `runListRoute`/`runStatusRoute`'s posture of not requiring same-origin. */
4
- export const agentListRoute = defineJsonRoute({ method: 'get', path: '/api/agents', parse: () => ok({ value: undefined }), handle: async ({ input: _input, deps }) => ok({ value: { agents: await deps.listAgents() } }) });
4
+ export const agentListRoute = defineJsonRoute({ method: 'get', path: '/api/agents', parse: () => ok({ value: undefined }), handle: async ({ input: _input, deps }) => ok({ value: { agents: activeAgents(await deps.listAgents()) } }) });
5
5
  /** `POST /api/agents/rescan` — explicit state refresh, protected by the local same-origin gate. */
6
- export const agentRescanRoute = defineJsonRoute({ method: 'post', path: '/api/agents/rescan', parse: () => ok({ value: undefined }), handle: async ({ input: _input, deps }) => ok({ value: { agents: await (deps.rescanAgents ?? deps.listAgents)() } }) }, { requireSameOrigin: true });
6
+ export const agentRescanRoute = defineJsonRoute({ method: 'post', path: '/api/agents/rescan', parse: () => ok({ value: undefined }), handle: async ({ input: _input, deps }) => ok({ value: { agents: activeAgents(await (deps.rescanAgents ?? deps.listAgents)()) } }) }, { requireSameOrigin: true });
7
+ /** Retired CLI inventories can survive in a host cache; never advertise them over this route. */
8
+ function activeAgents(agents) {
9
+ return agents.filter((agent) => agent.id !== 'gemini');
10
+ }
7
11
  /** Mounts the read and explicit-rescan agent discovery routes. */
8
12
  export function registerAgentRoutes({ app, deps, adapter }, _optional = {}) {
9
13
  mountJsonRoute({ app, spec: agentListRoute, deps, adapter });