@jini-ai/core 0.4.0 → 0.4.2

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 (120) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +18 -1
  3. package/dist/agent-tools.d.ts +1 -0
  4. package/dist/approval.d.ts +134 -0
  5. package/dist/approval.js +141 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +5 -0
  8. package/dist/model-facing-tool-errors.d.ts +152 -0
  9. package/dist/model-facing-tool-errors.js +124 -0
  10. package/dist/native-regexp.d.ts +10 -0
  11. package/dist/native-regexp.js +2 -0
  12. package/dist/primitives/index.d.ts +2 -0
  13. package/dist/primitives/index.js +1 -0
  14. package/dist/primitives/sse.d.ts +60 -0
  15. package/dist/primitives/sse.js +122 -0
  16. package/dist/redact.js +4 -3
  17. package/dist/registration-kit.d.ts +47 -0
  18. package/dist/registration-kit.js +55 -0
  19. package/dist/settings/agent-tools.d.ts +10 -0
  20. package/dist/settings/agent-tools.js +204 -0
  21. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  22. package/dist/settings/agent-value-write-tools.js +157 -0
  23. package/dist/settings/agent-writable-preferences.d.ts +67 -0
  24. package/dist/settings/agent-writable-preferences.js +132 -0
  25. package/dist/settings/agent-write-denylist.d.ts +19 -0
  26. package/dist/settings/agent-write-denylist.js +15 -0
  27. package/dist/settings/change-feed.d.ts +83 -0
  28. package/dist/settings/change-feed.js +61 -0
  29. package/dist/settings/definitions-dispatch.d.ts +56 -0
  30. package/dist/settings/definitions-dispatch.js +86 -0
  31. package/dist/settings/dictionaries/index.d.ts +48 -0
  32. package/dist/settings/dictionaries/index.js +69 -0
  33. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
  34. package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
  35. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
  36. package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
  37. package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
  38. package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
  39. package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
  40. package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
  41. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
  42. package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
  43. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
  44. package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
  45. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
  46. package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
  47. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
  48. package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
  49. package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
  50. package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
  51. package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
  52. package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
  53. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
  54. package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
  55. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
  56. package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
  57. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
  58. package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
  59. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
  60. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
  61. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
  62. package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
  63. package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
  64. package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
  65. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
  66. package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
  67. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
  68. package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
  69. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
  70. package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
  71. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
  72. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
  73. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
  74. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
  75. package/dist/settings/ensure-definitions.d.ts +101 -0
  76. package/dist/settings/ensure-definitions.js +101 -0
  77. package/dist/settings/errors.d.ts +42 -0
  78. package/dist/settings/errors.js +46 -0
  79. package/dist/settings/express/cms-adapter.d.ts +20 -0
  80. package/dist/settings/express/cms-adapter.js +124 -0
  81. package/dist/settings/express/contracts.d.ts +135 -0
  82. package/dist/settings/express/contracts.js +2 -0
  83. package/dist/settings/express/http.d.ts +10 -0
  84. package/dist/settings/express/http.js +211 -0
  85. package/dist/settings/express/index.d.ts +5 -0
  86. package/dist/settings/express/index.js +4 -0
  87. package/dist/settings/express/shared.d.ts +68 -0
  88. package/dist/settings/express/shared.js +54 -0
  89. package/dist/settings/express/stream.d.ts +24 -0
  90. package/dist/settings/express/stream.js +147 -0
  91. package/dist/settings/index.d.ts +40 -0
  92. package/dist/settings/index.js +37 -0
  93. package/dist/settings/ports.d.ts +118 -0
  94. package/dist/settings/ports.js +2 -0
  95. package/dist/settings/principal-lookup.d.ts +9 -0
  96. package/dist/settings/principal-lookup.js +2 -0
  97. package/dist/settings/purge-service.d.ts +54 -0
  98. package/dist/settings/purge-service.js +106 -0
  99. package/dist/settings/repo.memory.d.ts +119 -0
  100. package/dist/settings/repo.memory.js +199 -0
  101. package/dist/settings/settings.d.ts +139 -0
  102. package/dist/settings/settings.js +259 -0
  103. package/dist/settings/tool-registrations.d.ts +54 -0
  104. package/dist/settings/tool-registrations.js +250 -0
  105. package/dist/settings/types.d.ts +125 -0
  106. package/dist/settings/types.js +3 -0
  107. package/dist/settings/ui-tab-definitions.d.ts +41 -0
  108. package/dist/settings/ui-tab-definitions.js +148 -0
  109. package/dist/settings/write-service.d.ts +320 -0
  110. package/dist/settings/write-service.js +683 -0
  111. package/dist/surface-exchanges.d.ts +209 -0
  112. package/dist/surface-exchanges.js +55 -0
  113. package/dist/tool-metadata.d.ts +71 -0
  114. package/dist/tool-metadata.js +56 -0
  115. package/dist/tool-permissions.d.ts +76 -0
  116. package/dist/tool-permissions.js +57 -0
  117. package/dist/tool-registry.d.ts +3 -0
  118. package/package.json +62 -4
  119. package/dist/internal.d.ts +0 -41
  120. package/dist/internal.js +0 -39
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @jini-ai/core
2
2
 
3
+ ## 0.4.2 — pending release
4
+
5
+ - Own generic settings at `./settings` and its optional Express adapter at `./settings/express`; keep both out of the root runtime closure. Move the existing permission helper into core, preserving the CMS command denial class through re-export.
6
+
7
+ - Own the shared surface-exchange interfaces and reserved parameter constants, formerly declared in daemon. Add the `SurfaceAskThenReport` port so UI can consume the existing daemon lifecycle without depending on its runtime.
8
+
9
+ ## 0.4.1 — 2026-10-05
10
+
11
+ - New `readToolLimit({ input, max, fallback }, { key })`: an absent limit is `fallback`, one above `max` is capped at `max`, and a non-integer or one below 1 throws `ToolInputError` ("'limit' must be an integer between 1 and <max>").
12
+ - New `optionalOneOf({ input, key, values })`: a value outside `values` throws `ToolInputError` ("'<key>' must be one of: ..."). Both are additive.
13
+
3
14
  ## 0.4.0 — 2026-10-02
4
15
 
5
16
  ### BREAKING
package/README.md CHANGED
@@ -15,7 +15,8 @@ set of tokens, and fails to typecheck if any pack's declared dependency is left
15
15
  npm install @jini-ai/core
16
16
  ```
17
17
 
18
- No peer dependencies — this package is pure TypeScript with no runtime dependencies at all.
18
+ The root has no runtime dependencies and never loads settings or Express. `express` is an optional
19
+ peer used only by `@jini-ai/core/settings/express`; install it in hosts mounting those routes.
19
20
 
20
21
  ## What you get
21
22
 
@@ -239,3 +240,19 @@ Node or reading a working directory. Equality counts as contained. Directory
239
240
  prefix siblings and traversal escapes are rejected; a legal `..name` segment is
240
241
  allowed. Windows hosts pass `{ caseSensitive: false, separator: "\\\\" }`; POSIX hosts
241
242
  retain the default slash separator so backslashes in directory names remain literal.
243
+
244
+ ## Generic settings
245
+
246
+ `@jini-ai/core/settings` owns scoped definitions, defaults, values, revisions, change feeds,
247
+ boot-time definition registration, dictionaries and agent tools. Hosts inject settings storage,
248
+ active-principal lookup, authorization, clocks and IDs; no CMS or database adapter is required.
249
+ The keys, scopes, permissions and tool IDs are unchanged by the relocation.
250
+
251
+ `@jini-ai/core/settings/express` exports `registerSettingsRoutes`, HTTP policy contracts and
252
+ `createCmsSettingsService` / `createCmsSettingsChangeFeed` (factory names retained for compatibility).
253
+ Hosts provide route paths, readiness, principal resolution, scheduler, service and feed ports.
254
+ Registration returns a disposer that closes active feeds. The universal settings entry never
255
+ loads this adapter; neither entry is re-exported by the kernel root.
256
+
257
+ The former `@jini-ai/cms/settings` and `@jini-ai/cms/settings/express` entries re-export these
258
+ owners for one release. New consumers should import the core subpaths directly.
@@ -16,6 +16,7 @@ export type AgentToolSideEffect = "none" | "mutates-durable-state" | "deletes-du
16
16
  export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
17
17
  /** Structural catalog contract. Domains may require inputSchema or tighten their own metadata. */
18
18
  export interface AgentToolDefinition {
19
+ readonly metadata?: import('./tool-metadata.js').ToolMetadata;
19
20
  name: string;
20
21
  description: string;
21
22
  sideEffects: AgentToolSideEffect;
@@ -0,0 +1,134 @@
1
+ import { type HumanConfirmer } from './registration-kit.js';
2
+ import type { ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolRegistration } from './tool-registry.js';
3
+ /** A host describes the action; core owns the decision lifecycle, never the card format. */
4
+ export interface ApprovalPlan<TDescription> {
5
+ readonly ask: boolean;
6
+ readonly description: TDescription;
7
+ /** Only reusable escalation grants receive a key. Irreversible actions omit it. */
8
+ readonly rememberKey?: string;
9
+ }
10
+ export type ApprovalAnswer = {
11
+ confirmed: true;
12
+ choice?: string;
13
+ } | {
14
+ confirmed: false;
15
+ reason: 'declined' | 'expired' | 'abandoned';
16
+ };
17
+ /** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
18
+ * An answer never revives an aborted operation. Transport failures propagate without an effect. */
19
+ export declare function resolveApproval({ signal, askHuman }: {
20
+ signal: AbortSignal;
21
+ askHuman: () => Promise<ApprovalAnswer>;
22
+ }, _optional?: {}): Promise<ApprovalAnswer>;
23
+ export interface RememberedApprovalPort {
24
+ has(required: {
25
+ ctx: ToolExecutionContext;
26
+ key: string;
27
+ }): Promise<boolean>;
28
+ grant(required: {
29
+ ctx: ToolExecutionContext;
30
+ key: string;
31
+ confirmer: HumanConfirmer;
32
+ }): Promise<void>;
33
+ }
34
+ export interface ApprovalSteps<TPrepared, TDescription> {
35
+ prepare(required: {
36
+ ctx: ToolExecutionContext;
37
+ }, optional: ToolExecutionOptions): Promise<TPrepared>;
38
+ describe(required: {
39
+ ctx: ToolExecutionContext;
40
+ prepared: TPrepared;
41
+ }, optional: ToolExecutionOptions): Promise<ApprovalPlan<TDescription>> | ApprovalPlan<TDescription>;
42
+ askHuman(required: {
43
+ ctx: ToolExecutionContext;
44
+ description: TDescription;
45
+ }, optional: ToolExecutionOptions): Promise<ApprovalAnswer>;
46
+ run(required: {
47
+ ctx: ToolExecutionContext;
48
+ prepared: TPrepared;
49
+ confirmer: HumanConfirmer;
50
+ choice?: string | undefined;
51
+ }, optional: ToolExecutionOptions): Promise<unknown>;
52
+ /** Abort may happen before preparation; the lifecycle forwards that missing value explicitly. */
53
+ notConfirmed(required: {
54
+ reason: 'declined' | 'expired' | 'abandoned';
55
+ prepared?: TPrepared | undefined;
56
+ }): unknown;
57
+ }
58
+ /** Extend the branded human-confirm owner with descriptions and optional remembered grants.
59
+ * Inputs and actor identity are detached before any asynchronous port runs. No input field can
60
+ * approve an action; only the host transport or the bound grant store can answer. Abort checks
61
+ * bracket preparation, storage and the human wait, so a completed run never performs an effect.
62
+ * @param steps Domain preparation/effects and the host's description/transport ports.
63
+ * @param optional.remembered A persisted escalation store; omission requires a fresh decision.
64
+ * @returns A handler accepted by the registration kit's human-only actor rule.
65
+ * @throws Port failures propagate; a failed store cannot silently authorize an action.
66
+ * @complexity O(input size) for the snapshot plus bounded port calls and the human wait.
67
+ * @example createApprovalHandler({ prepare, describe, askHuman, run, notConfirmed });
68
+ */
69
+ export declare function createApprovalHandler<TPrepared, TDescription>(steps: ApprovalSteps<TPrepared, TDescription>, { remembered }?: {
70
+ remembered?: RememberedApprovalPort;
71
+ }): ToolHandler;
72
+ /** Page-local proposal transport for a browser that must return before its explicit reply arrives.
73
+ * It carries no principal or tool authorization: the host's execute port must re-check access.
74
+ * The private action is independent of the rendered snapshot; replies consume it before awaiting
75
+ * so double clicks/replayed replies cannot execute twice. Abort clears the proposal and listener.
76
+ * @returns An observable transport for an app dialog and its authenticated reply entry point.
77
+ * @complexity O(description size) to detach the proposal; O(listeners) to publish a change.
78
+ */
79
+ export declare function createApprovalProposalStore<TDescription>({ responseTool, messages }: {
80
+ responseTool: string;
81
+ messages: {
82
+ closed: string;
83
+ pending: string;
84
+ missing: string;
85
+ };
86
+ }, { createId }?: {
87
+ createId?: () => string;
88
+ }): {
89
+ getSnapshot: (_required?: {}, _optional?: {}) => {
90
+ pending: {
91
+ approvalId: string;
92
+ description: TDescription;
93
+ } | null;
94
+ error: string | null;
95
+ };
96
+ subscribe: ({ listener }: {
97
+ listener: () => void;
98
+ }, _optional?: {}) => () => void;
99
+ propose({ description, execute, signal }: {
100
+ description: TDescription;
101
+ execute: () => Promise<unknown>;
102
+ signal: AbortSignal;
103
+ }, _optional?: {}): {
104
+ status: "approval_required";
105
+ approvalId: string;
106
+ description: TDescription;
107
+ responseTool: string;
108
+ };
109
+ respond({ approvalId, approved }: {
110
+ approvalId: string;
111
+ approved: boolean;
112
+ }, _optional?: {}): Promise<unknown>;
113
+ };
114
+ /** Apply a host classification through the same handler owner used by domain plans.
115
+ * @returns The registration with one approval handler; authorization policy is preserved.
116
+ */
117
+ export declare function applyApprovalPolicy<TDescription>({ registration, classify, asks, describe, askHuman, notConfirmed }: {
118
+ registration: ToolRegistration;
119
+ classify(required: {
120
+ input: unknown;
121
+ }): string;
122
+ asks(required: {
123
+ class: string;
124
+ }): boolean;
125
+ describe(required: {
126
+ ctx: ToolExecutionContext;
127
+ class: string;
128
+ }): Promise<TDescription>;
129
+ askHuman: ApprovalSteps<string, TDescription>['askHuman'];
130
+ notConfirmed: ApprovalSteps<string, TDescription>['notConfirmed'];
131
+ }, optional?: {
132
+ remembered?: RememberedApprovalPort;
133
+ }): ToolRegistration;
134
+ //# sourceMappingURL=approval.d.ts.map
@@ -0,0 +1,141 @@
1
+ import { humanConfirmedHandler } from './registration-kit.js';
2
+ /** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
3
+ * An answer never revives an aborted operation. Transport failures propagate without an effect. */
4
+ export async function resolveApproval({ signal, askHuman }, _optional = {}) {
5
+ if (signal.aborted)
6
+ return { confirmed: false, reason: 'abandoned' };
7
+ let abandon;
8
+ const aborted = new Promise(resolve => { abandon = () => resolve({ confirmed: false, reason: 'abandoned' }); });
9
+ signal.addEventListener('abort', abandon, { once: true });
10
+ try {
11
+ const answer = await Promise.race([askHuman(), aborted]);
12
+ return signal.aborted ? { confirmed: false, reason: 'abandoned' } : answer;
13
+ }
14
+ finally {
15
+ signal.removeEventListener('abort', abandon);
16
+ }
17
+ }
18
+ /** Extend the branded human-confirm owner with descriptions and optional remembered grants.
19
+ * Inputs and actor identity are detached before any asynchronous port runs. No input field can
20
+ * approve an action; only the host transport or the bound grant store can answer. Abort checks
21
+ * bracket preparation, storage and the human wait, so a completed run never performs an effect.
22
+ * @param steps Domain preparation/effects and the host's description/transport ports.
23
+ * @param optional.remembered A persisted escalation store; omission requires a fresh decision.
24
+ * @returns A handler accepted by the registration kit's human-only actor rule.
25
+ * @throws Port failures propagate; a failed store cannot silently authorize an action.
26
+ * @complexity O(input size) for the snapshot plus bounded port calls and the human wait.
27
+ * @example createApprovalHandler({ prepare, describe, askHuman, run, notConfirmed });
28
+ */
29
+ export function createApprovalHandler(steps, { remembered } = {}) {
30
+ return humanConfirmedHandler({
31
+ prepare: async ({ ctx }, options = {}) => {
32
+ const snapshot = { ...ctx, input: structuredClone(ctx.input), principal: { ...ctx.principal }, run: { ...ctx.run } };
33
+ if (snapshot.signal.aborted)
34
+ return { ctx: snapshot };
35
+ const value = await steps.prepare({ ctx: snapshot }, options);
36
+ if (snapshot.signal.aborted)
37
+ return { ctx: snapshot, value };
38
+ const plan = await steps.describe({ ctx: snapshot, prepared: value }, options);
39
+ return { ctx: snapshot, value, plan };
40
+ },
41
+ askHuman: async ({ prepared }, options = {}) => {
42
+ const { ctx, plan, value } = prepared;
43
+ const abandoned = () => ({ confirmed: false, result: steps.notConfirmed({ reason: 'abandoned', prepared: value }) });
44
+ if (ctx.signal.aborted || !plan)
45
+ return abandoned();
46
+ if (!plan.ask)
47
+ return { confirmed: true };
48
+ if (plan.rememberKey !== undefined && remembered) {
49
+ const approved = await remembered.has({ ctx, key: plan.rememberKey });
50
+ if (ctx.signal.aborted)
51
+ return abandoned();
52
+ if (approved)
53
+ return { confirmed: true };
54
+ }
55
+ const answer = await resolveApproval({ signal: ctx.signal, askHuman: () => steps.askHuman({ ctx, description: plan.description }, options) });
56
+ if (ctx.signal.aborted)
57
+ return abandoned();
58
+ if (answer.confirmed !== true)
59
+ return { confirmed: false, result: steps.notConfirmed({ reason: answer.reason, prepared: value }) };
60
+ prepared.choice = answer.choice;
61
+ if (plan.rememberKey !== undefined && remembered) {
62
+ await remembered.grant({ ctx, key: plan.rememberKey, confirmer: { id: ctx.principal.id, kind: 'user' } });
63
+ }
64
+ if (ctx.signal.aborted)
65
+ return abandoned();
66
+ return { confirmed: true };
67
+ },
68
+ run: ({ prepared }, options = {}) => {
69
+ const { ctx, value, choice } = prepared;
70
+ if (ctx.signal.aborted)
71
+ return Promise.resolve(steps.notConfirmed({ reason: 'abandoned', prepared: value }));
72
+ return steps.run({ ctx, prepared: value, confirmer: { id: ctx.principal.id, kind: 'user' }, choice }, options);
73
+ },
74
+ });
75
+ }
76
+ /** Page-local proposal transport for a browser that must return before its explicit reply arrives.
77
+ * It carries no principal or tool authorization: the host's execute port must re-check access.
78
+ * The private action is independent of the rendered snapshot; replies consume it before awaiting
79
+ * so double clicks/replayed replies cannot execute twice. Abort clears the proposal and listener.
80
+ * @returns An observable transport for an app dialog and its authenticated reply entry point.
81
+ * @complexity O(description size) to detach the proposal; O(listeners) to publish a change.
82
+ */
83
+ export function createApprovalProposalStore({ responseTool, messages }, { createId = () => crypto.randomUUID() } = {}) {
84
+ let snapshot = { pending: null, error: null };
85
+ let action = null;
86
+ let detach = null;
87
+ const listeners = new Set();
88
+ const publish = (next) => { snapshot = next; for (const listener of listeners)
89
+ listener(); };
90
+ const clear = () => {
91
+ detach?.();
92
+ detach = null;
93
+ action = null;
94
+ publish({ pending: null, error: null });
95
+ };
96
+ return {
97
+ getSnapshot: (_required = {}, _optional = {}) => snapshot,
98
+ subscribe: ({ listener }, _optional = {}) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
99
+ propose({ description, execute, signal }, _optional = {}) {
100
+ if (signal.aborted)
101
+ throw new Error(messages.closed);
102
+ if (snapshot.pending)
103
+ throw new Error(messages.pending);
104
+ const frozenDescription = structuredClone(description);
105
+ const approvalId = createId();
106
+ action = { approvalId, execute, signal };
107
+ signal.addEventListener('abort', clear, { once: true });
108
+ detach = () => signal.removeEventListener('abort', clear);
109
+ publish({ pending: { approvalId, description: structuredClone(frozenDescription) }, error: null });
110
+ return { status: 'approval_required', approvalId, description: frozenDescription, responseTool };
111
+ },
112
+ async respond({ approvalId, approved }, _optional = {}) {
113
+ if (!action || action.approvalId !== approvalId)
114
+ throw new Error(messages.missing);
115
+ const prepared = action;
116
+ clear();
117
+ const answer = await resolveApproval({ signal: prepared.signal, askHuman: async () => approved === true ? { confirmed: true } : { confirmed: false, reason: 'declined' } });
118
+ if (!answer.confirmed)
119
+ return { status: 'declined' };
120
+ try {
121
+ return await prepared.execute();
122
+ }
123
+ catch (error) {
124
+ publish({ ...snapshot, error: error instanceof Error ? error.message : String(error) });
125
+ throw error;
126
+ }
127
+ },
128
+ };
129
+ }
130
+ /** Apply a host classification through the same handler owner used by domain plans.
131
+ * @returns The registration with one approval handler; authorization policy is preserved.
132
+ */
133
+ export function applyApprovalPolicy({ registration, classify, asks, describe, askHuman, notConfirmed }, optional = {}) {
134
+ return { ...registration, handler: createApprovalHandler({
135
+ prepare: async ({ ctx }) => classify({ input: ctx.input }),
136
+ describe: async ({ ctx, prepared }) => ({ ask: asks({ class: prepared }), description: await describe({ ctx, class: prepared }) }),
137
+ askHuman, notConfirmed,
138
+ run: ({ ctx }, options) => registration.handler(ctx, options),
139
+ }, optional) };
140
+ }
141
+ //# sourceMappingURL=approval.js.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './token.js';
2
+ export * from './tool-metadata.js';
2
3
  export * from './pack.js';
3
4
  export * from './bindings.js';
4
5
  export type { Daemon, DaemonConfig, DaemonOptions } from './daemon.js';
@@ -9,6 +10,7 @@ export * from './redact.js';
9
10
  export * from './api-token-auth.js';
10
11
  export * from './origin-validation.js';
11
12
  export * from './principal.js';
13
+ export * from './surface-exchanges.js';
12
14
  export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
13
15
  export { createToolRegistry, isReadOnlyTool, ToolInputError } from './tool-registry.js';
14
16
  export * from './tool-tokens.js';
@@ -18,4 +20,7 @@ export * from './contribution-registry.js';
18
20
  export * from './naming.js';
19
21
  export type { AgentToolSideEffect, AgentToolActorClassRule, AgentToolDefinition } from './agent-tools.js';
20
22
  export * from "./registration-kit.js";
23
+ export * from "./approval.js";
24
+ export * from "./native-regexp.js";
25
+ export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, type LegacyToolAuthorizeFn, type AuthorizationPort as ToolPermissionAuthorizationPort, type RequireToolPermissionRequired, type RequireToolPermissionOptional, } from "./tool-permissions.js";
21
26
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './token.js';
2
+ export * from './tool-metadata.js';
2
3
  export * from './pack.js';
3
4
  export * from './bindings.js';
4
5
  export { createDaemon } from './daemon.js';
@@ -7,6 +8,7 @@ export * from './redact.js';
7
8
  export * from './api-token-auth.js';
8
9
  export * from './origin-validation.js';
9
10
  export * from './principal.js';
11
+ export * from './surface-exchanges.js';
10
12
  export { createToolRegistry, isReadOnlyTool, ToolInputError } from './tool-registry.js';
11
13
  export * from './tool-tokens.js';
12
14
  export * from './gated-mutations.js';
@@ -14,4 +16,7 @@ export * from './model-facing-tool-errors.js';
14
16
  export * from './contribution-registry.js';
15
17
  export * from './naming.js';
16
18
  export * from "./registration-kit.js";
19
+ export * from "./approval.js";
20
+ export * from "./native-regexp.js";
21
+ export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, } from "./tool-permissions.js";
17
22
  //# sourceMappingURL=index.js.map
@@ -3,35 +3,187 @@
3
3
  * Rule evaluation is O(r); container wrapping O(n), with no I/O or reporting dependencies.
4
4
  */
5
5
  import { type ToolHandler, type ToolRegistration } from "./tool-registry.js";
6
+ /**
7
+ * One entry in a domain's allowlist: an error class, and the stable code prefix its message is
8
+ * published to the model under.
9
+ *
10
+ * The code is prepended before a `: ` separator (`MEMBERS_NOT_FOUND: member 'x' was not found`) so
11
+ * a model — and a test — can match on a stable token without parsing prose, mirroring the prefix
12
+ * convention `features/widgets/tool-registrations.ts` already shipped.
13
+ *
14
+ * `guidance`, when present, is appended after the message. Use it only for a failure where the
15
+ * caller's next action is not obvious from the message alone (a version conflict that needs a
16
+ * re-read, say); a not-found needs no coaching.
17
+ */
6
18
  export interface ModelFacingErrorRule {
19
+ /** The class to match with `instanceof`. Declared to accept any constructor shape so a domain can
20
+ * list an error class whose constructor takes extra fields. */
7
21
  readonly error: abstract new (...args: never[]) => Error;
22
+ /** The stable code published before `: `. Conventionally `<DOMAIN>_<REASON>`, SCREAMING_SNAKE. */
8
23
  readonly code: string;
24
+ /** Published INSTEAD of the error's own message. Set it for a class whose KIND is the actionable
25
+ * reason a caller needs but whose TEXT is not safe to publish — one that wraps another
26
+ * component's error (a sealer's, a driver's, a keyring's), which can carry plaintext, an internal
27
+ * address, an env var name or an absolute path. Omit it to publish the class's own message
28
+ * verbatim, which is only correct when every construction site builds that message from fixed
29
+ * text and the caller's own input.
30
+ *
31
+ * The same field, for the same reason, as {@link CallerSafeErrorRule.message} — these two
32
+ * allowlists are the RETURNED-result and THROWN-rejection halves of one policy, and a class whose
33
+ * kind is safe to name on one half is safe to name on the other. They drifted until 2026-09-18:
34
+ * `features/custom-credentials` listed `CustomCredentialSecretStoreUnconfiguredError` with a fixed
35
+ * message in its `CallerSafeErrorRule` allowlist, but could not list it here at all, because the
36
+ * only thing this shape could publish was the very text that had to stay in. A missing site root
37
+ * key — the one operator-fixable failure in that domain — therefore reached the model as a bare
38
+ * `INTERNAL_ERROR` on every credential-using tool. */
9
39
  readonly message?: string;
40
+ /** Optional recovery guidance appended after the message. */
10
41
  readonly guidance?: string;
11
42
  }
43
+ /**
44
+ * The code every domain publishes an authorization refusal under, parameterized by domain prefix.
45
+ *
46
+ * Exists because `ForbiddenError` is the ONE class in this ladder that no domain declares: the kit's
47
+ * `requireToolPermission` throws it on every domain's behalf. Deriving the rule here rather than
48
+ * retyping `{ error: ForbiddenError, code: "X_FORBIDDEN" }` in six files means a later decision to
49
+ * reconcile this code with the HTTP arm's own `FORBIDDEN` is a one-line change here, not a sweep.
50
+ *
51
+ * NOTE (2026-09-16): the model-facing and HTTP arms disagree on this code today — the HTTP mappers
52
+ * return `FORBIDDEN` while the model-facing arm returns `<DOMAIN>_FORBIDDEN`. That drift predates
53
+ * this file (see `features/widgets/tool-registrations.ts`'s `toModelFacingWidgetsError` doc, which
54
+ * documented it for Widgets) and is owned elsewhere. This helper deliberately matches the SHIPPED
55
+ * model-facing convention rather than inventing a third spelling.
56
+ *
57
+ * The refusal message `requireToolPermission` builds names the principal, the permission, and the
58
+ * `authorize()` reason — no data, no internals. Surfacing it tells a model whether to stop asking
59
+ * or to ask a human for access; redacting it tells it only that something broke.
60
+ *
61
+ * @param required.domainPrefix - The domain's SCREAMING_SNAKE prefix, e.g. `MEMBERS`.
62
+ * @param required.error - The consumer's permission-error class; core has no CMS dependency.
63
+ * @returns The rule to place in that domain's allowlist.
64
+ * @complexity O(1).
65
+ */
12
66
  export declare function forbiddenRule(required: {
13
67
  domainPrefix: string;
14
68
  error: ModelFacingErrorRule["error"];
15
69
  }): ModelFacingErrorRule;
70
+ /**
71
+ * Re-classifies one rejection against a domain's allowlist, so a listed domain error reaches the
72
+ * model as a `ToolInputError` carrying its real message instead of a redacted `INTERNAL_ERROR`.
73
+ *
74
+ * Rules are evaluated IN ORDER and the first `instanceof` match wins, so a domain listing both a
75
+ * subclass and its superclass must place the subclass first — the same ordering discipline any
76
+ * hand-written `instanceof` ladder carries, made explicit here because an array hides it less than
77
+ * a chain of `if`s does.
78
+ *
79
+ * A matched rule publishes the error's own message, or {@link ModelFacingErrorRule.message} instead
80
+ * when the rule sets one — see that field for which classes need it and why.
81
+ *
82
+ * An already-`ToolInputError` rejection is returned untouched: it is already correctly classified,
83
+ * and re-wrapping it would double a code prefix that a `withSchemaOnRejection` wrap may have
84
+ * already attached.
85
+ *
86
+ * @param required.err - The rejection, of unknown type — anything a handler can throw.
87
+ * @param required.rules - The domain's allowlist. Anything unmatched is returned UNCHANGED and stays
88
+ * redacted; see this file's header for why that default is the security-relevant half.
89
+ * @returns The value to re-throw: a `ToolInputError` for a matched rule, otherwise `err` itself.
90
+ * @complexity O(r) in the rule count, on the failure path only.
91
+ */
16
92
  export declare function reclassifyToolError(required: {
17
93
  err: unknown;
18
94
  rules: readonly ModelFacingErrorRule[];
19
95
  }): unknown;
96
+ /**
97
+ * Wraps EVERY handler in a domain's map with {@link reclassifyToolError}, returning a new map.
98
+ *
99
+ * Wrapping the whole map once — rather than reclassifying at each call site — is the point. This
100
+ * codebase's dominant defect is a correct primitive with an unwired call site, and a per-call-site
101
+ * reshape is exactly that defect waiting to happen: `features/post/tool-registrations.ts` had a
102
+ * correct `toModelFacingUpdateError` wired into ONE of its handlers while its siblings kept
103
+ * throwing the same class straight through to the redactor. A map-level wrap cannot have a missed
104
+ * arm, because it has no arms.
105
+ *
106
+ * The wrap is transparent on the success path: the handler's own return value is passed through
107
+ * untouched, and only the `catch` does work.
108
+ *
109
+ * @param required.handlers - The domain's handler map, keyed by tool id.
110
+ * @param rules - The domain's allowlist, applied identically to every handler.
111
+ * @returns A new map with the same keys; the input map is not mutated.
112
+ * @complexity O(h) handlers wrapped once at build time; O(r) per failed call, none per successful one.
113
+ */
20
114
  export declare function withModelFacingErrors(required: {
21
115
  handlers: Readonly<Record<string, ToolHandler>>;
22
116
  rules: readonly ModelFacingErrorRule[];
23
117
  }): Record<string, ToolHandler>;
118
+ /**
119
+ * Wraps EVERY registration's handler in a domain's `ToolRegistration[]` with
120
+ * {@link reclassifyToolError}, returning a new array. The `ToolRegistration[]`-shaped sibling of
121
+ * {@link withModelFacingErrors} — for the registry-converted domains whose `build*Registrations`
122
+ * returns an array of `{descriptor, handler, policy}` (see `assistant/tool-contribution-registry.ts`'s
123
+ * `ToolContributor.build`) rather than a `Record<toolId, handler>`. `features/entries`,
124
+ * `features/content-types`, `features/navigation` and `features/workspace` are re-exported Jini
125
+ * builders of exactly this shape — see each one's `contribute*Tools`.
126
+ *
127
+ * Same transparency guarantee as {@link withModelFacingErrors}: only the `catch` arm does work, and
128
+ * every other field of each registration (`descriptor`, `policy`) passes through unchanged.
129
+ *
130
+ * @param required.registrations - The domain's registrations, in whatever order `build*Registrations`
131
+ * returned them.
132
+ * @param rules - The domain's allowlist, applied identically to every registration.
133
+ * @returns A new array with the same length and order; the input array is not mutated.
134
+ * @complexity O(n) registrations wrapped once at build time; O(r) per failed call, none per successful one.
135
+ */
24
136
  export declare function withModelFacingRegistrationErrors(required: {
25
137
  registrations: readonly ToolRegistration[];
26
138
  rules: readonly ModelFacingErrorRule[];
27
139
  }): ToolRegistration[];
140
+ /**
141
+ * One entry in a structured-result allowlist — see {@link callerSafeErrorMessage}.
142
+ *
143
+ * No `code`: a structured result already carries its own `reason` discriminant, and its message is
144
+ * often rendered to a human as-is, so no prefix is added.
145
+ */
28
146
  export interface CallerSafeErrorRule {
147
+ /** The class to match with `instanceof`. Same constructor shape as {@link ModelFacingErrorRule.error}. */
29
148
  readonly error: abstract new (...args: never[]) => Error;
149
+ /** Published INSTEAD of the error's own message. Set it for a class whose KIND is safe and worth
150
+ * naming but whose message is not — one that embeds another error's text. Omit it to publish the
151
+ * class's own message verbatim, which is only correct when every construction site builds that
152
+ * message from fixed text and the caller's own input. */
30
153
  readonly message?: string;
31
154
  }
155
+ /**
156
+ * The message a RETURNED failure result may carry for `err`: a listed class's message (or its fixed
157
+ * replacement), otherwise `fallback`.
158
+ *
159
+ * Unknown means redacted. An error class added tomorrow, a raw driver error, or a non-`Error` throw
160
+ * all get `fallback`, never their own text. Rules are evaluated in order; the first `instanceof`
161
+ * match wins.
162
+ *
163
+ * @param required.err - The caught value, of unknown type.
164
+ * @param required.rules - The allowlist.
165
+ * @param required.fallback - Fixed text for everything unlisted. Must not be built from `err`.
166
+ * @returns The message to publish.
167
+ * @complexity O(r) in the rule count.
168
+ */
32
169
  export declare function callerSafeErrorMessage(required: {
33
170
  err: unknown;
34
171
  rules: readonly CallerSafeErrorRule[];
35
172
  fallback: string;
36
173
  }): string;
174
+ /**
175
+ * What a server-side log line may say about an error whose message is NOT known to be safe: its class
176
+ * name, a constant-token `code`, and the same for its direct `cause`. Never the message, which can
177
+ * quote a secret — `JSON.parse` quotes its input, and a sealer is handed plaintext.
178
+ *
179
+ * Use it wherever the raw message could carry a credential. Where a class's message is designed for
180
+ * logs (`EgressRefusedError.message`), log that instead.
181
+ *
182
+ * @param required.err - The caught value.
183
+ * @returns e.g. `SqliteError(SQLITE_BUSY)`, or `TypeError cause=Error(ECONNREFUSED)`.
184
+ * @complexity O(1).
185
+ */
186
+ export declare function describeErrorForLog(required: {
187
+ err: unknown;
188
+ }, _optional?: Record<string, never>): string;
37
189
  //# sourceMappingURL=model-facing-tool-errors.d.ts.map