@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.
- package/CHANGELOG.md +11 -0
- package/README.md +18 -1
- package/dist/agent-tools.d.ts +1 -0
- package/dist/approval.d.ts +134 -0
- package/dist/approval.js +141 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/model-facing-tool-errors.d.ts +152 -0
- package/dist/model-facing-tool-errors.js +124 -0
- package/dist/native-regexp.d.ts +10 -0
- package/dist/native-regexp.js +2 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/sse.d.ts +60 -0
- package/dist/primitives/sse.js +122 -0
- package/dist/redact.js +4 -3
- package/dist/registration-kit.d.ts +47 -0
- package/dist/registration-kit.js +55 -0
- package/dist/settings/agent-tools.d.ts +10 -0
- package/dist/settings/agent-tools.js +204 -0
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +157 -0
- package/dist/settings/agent-writable-preferences.d.ts +67 -0
- package/dist/settings/agent-writable-preferences.js +132 -0
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +83 -0
- package/dist/settings/change-feed.js +61 -0
- package/dist/settings/definitions-dispatch.d.ts +56 -0
- package/dist/settings/definitions-dispatch.js +86 -0
- package/dist/settings/dictionaries/index.d.ts +48 -0
- package/dist/settings/dictionaries/index.js +69 -0
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
- package/dist/settings/ensure-definitions.d.ts +101 -0
- package/dist/settings/ensure-definitions.js +101 -0
- package/dist/settings/errors.d.ts +42 -0
- package/dist/settings/errors.js +46 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +4 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +40 -0
- package/dist/settings/index.js +37 -0
- package/dist/settings/ports.d.ts +118 -0
- package/dist/settings/ports.js +2 -0
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +54 -0
- package/dist/settings/purge-service.js +106 -0
- package/dist/settings/repo.memory.d.ts +119 -0
- package/dist/settings/repo.memory.js +199 -0
- package/dist/settings/settings.d.ts +139 -0
- package/dist/settings/settings.js +259 -0
- package/dist/settings/tool-registrations.d.ts +54 -0
- package/dist/settings/tool-registrations.js +250 -0
- package/dist/settings/types.d.ts +125 -0
- package/dist/settings/types.js +3 -0
- package/dist/settings/ui-tab-definitions.d.ts +41 -0
- package/dist/settings/ui-tab-definitions.js +148 -0
- package/dist/settings/write-service.d.ts +320 -0
- package/dist/settings/write-service.js +683 -0
- package/dist/surface-exchanges.d.ts +209 -0
- package/dist/surface-exchanges.js +55 -0
- package/dist/tool-metadata.d.ts +71 -0
- package/dist/tool-metadata.js +56 -0
- package/dist/tool-permissions.d.ts +76 -0
- package/dist/tool-permissions.js +57 -0
- package/dist/tool-registry.d.ts +3 -0
- package/package.json +62 -4
- package/dist/internal.d.ts +0 -41
- 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
|
-
|
|
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.
|
package/dist/agent-tools.d.ts
CHANGED
|
@@ -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
|
package/dist/approval.js
ADDED
|
@@ -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
|