@jini-ai/core 0.4.1 → 0.4.3
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 +8 -0
- package/README.md +19 -1
- package/dist/agent-tools.d.ts +1 -0
- package/dist/approval.d.ts +134 -0
- package/dist/approval.js +141 -0
- package/dist/fetch/cache.d.ts +129 -0
- package/dist/fetch/cache.js +243 -0
- package/dist/fetch/index.d.ts +5 -0
- package/dist/fetch/index.js +3 -0
- package/dist/fetch/types.d.ts +93 -0
- package/dist/fetch/types.js +2 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- 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 +13 -0
- package/dist/registration-kit.js +12 -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/text/escape.d.ts +20 -0
- package/dist/text/escape.js +23 -0
- package/dist/text/index.d.ts +1 -0
- package/dist/text/index.js +1 -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 +19 -1
- package/dist/tool-registry.js +44 -0
- package/package.json +79 -4
- package/plugin-directory.d.ts +32 -0
- package/plugin-directory.js +140 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared surface-exchange ABI. The daemon owns the store and ask/report implementation;
|
|
3
|
+
* consumers inject those ports without depending on the daemon runtime.
|
|
4
|
+
*/
|
|
5
|
+
import type { SurfaceEmission, SurfaceEmitter } from './tool-registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* The callback param carrying an exchange id from a rendered surface back to the server.
|
|
8
|
+
*
|
|
9
|
+
* Double-underscored to keep it clear of a surface's real fields, and named in one place because
|
|
10
|
+
* three parties must agree on it: whatever builds the surface, the route that routes on it, and the
|
|
11
|
+
* handler that opened the exchange.
|
|
12
|
+
*
|
|
13
|
+
* This is MCP-UI's carrier specifically, because an mcp-ui surface can only answer by issuing a tool
|
|
14
|
+
* call, so its correlation has to ride inside that call's params. A channel that can name the
|
|
15
|
+
* exchange directly should do so — `mcp-ui-tool-calls-route.ts` also accepts a top-level
|
|
16
|
+
* `exchangeId`, and A2UI/`surface_request` correlate by their own `surfaceId`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SURFACE_EXCHANGE_ID_PARAM = "__exchangeId";
|
|
19
|
+
/**
|
|
20
|
+
* The callback param a surface's Cancel action sets, so a dismissal resolves the exchange
|
|
21
|
+
* immediately instead of stranding the agent until the idle deadline.
|
|
22
|
+
*
|
|
23
|
+
* Carried as data rather than as a distinct {@link SurfaceMessage} status: "the human answered, and
|
|
24
|
+
* the answer was no" is genuinely an answer, and only the tool that built the surface knows what a
|
|
25
|
+
* dismissal means for its own result.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SURFACE_DISMISSED_PARAM = "__dismissed";
|
|
28
|
+
/**
|
|
29
|
+
* The callback param carrying free text a human TYPED — into the chat composer — as their answer to
|
|
30
|
+
* an outstanding surface, rather than clicking the rendered form.
|
|
31
|
+
*
|
|
32
|
+
* ## The deadlock this exists to break
|
|
33
|
+
*
|
|
34
|
+
* A held-open exchange can only be resolved by a message that names it, and until now every param
|
|
35
|
+
* shape a surface posts back was one the FORM produced. A human who reads the question, ignores the
|
|
36
|
+
* dialog and types the answer into the composer therefore resolved nothing: their message was queued
|
|
37
|
+
* behind the very run it was meant to unblock, and the agent sat parked until the idle deadline
|
|
38
|
+
* fired minutes later.
|
|
39
|
+
*
|
|
40
|
+
* ## Why it is a separate param rather than the tool's own answer fields
|
|
41
|
+
*
|
|
42
|
+
* Prose is not a selection. A tool whose answer fields are option VALUES the model itself supplied
|
|
43
|
+
* (`assistant_ask_choice`'s `choice`/`selections`) would, if prose were poured into them, report to
|
|
44
|
+
* the model that the administrator picked an option that was never offered. Carrying typed text
|
|
45
|
+
* under its own name is what forces each tool to decide, explicitly, what a typed answer means for
|
|
46
|
+
* its own result — and lets a tool that has no safe reading of one simply ignore it.
|
|
47
|
+
*
|
|
48
|
+
* ## What it deliberately cannot do
|
|
49
|
+
*
|
|
50
|
+
* It cannot confirm anything. `classifyConfirmationAnswer` reads only a literal `decision:
|
|
51
|
+
* "confirm"`, so a typed answer arriving at a confirmation-shaped exchange fails closed to
|
|
52
|
+
* `"declined"` with no code change and no exception — which is the correct reading of "the human
|
|
53
|
+
* wrote a sentence instead of clicking Confirm". Nothing here should ever grow a prose-to-consent
|
|
54
|
+
* mapping.
|
|
55
|
+
*
|
|
56
|
+
* Double-underscored, and named here rather than at a tool, for the same reason as
|
|
57
|
+
* {@link SURFACE_EXCHANGE_ID_PARAM}: several parties must agree on the spelling.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SURFACE_TYPED_ANSWER_PARAM = "__typedAnswer";
|
|
60
|
+
/**
|
|
61
|
+
* Which inbound route may answer an exchange. `"mcp-ui"` (the host's default) is a tool-call-shaped answer
|
|
62
|
+
* posted to the MCP-UI route; `"a2ui"` is a renderer message posted to the A2UI route. A2UI posts
|
|
63
|
+
* carry no `toolId`, so without this an A2UI message could land in, and cancel, a pending MCP-UI
|
|
64
|
+
* confirmation of the same principal.
|
|
65
|
+
*/
|
|
66
|
+
export type SurfaceAnswerChannel = string;
|
|
67
|
+
/** Who an exchange belongs to. Checked on every delivery; a mismatch is a rejection, not a warning. */
|
|
68
|
+
export interface SurfaceExchangeBinding {
|
|
69
|
+
/** The tool that opened it. A message for one tool cannot resolve another's exchange. */
|
|
70
|
+
readonly toolId: string;
|
|
71
|
+
/** The principal whose run opened it: one human's answer must not land in another's call. */
|
|
72
|
+
readonly principalId: string;
|
|
73
|
+
/** The route allowed to answer it; omission uses the store's injected default channel. */
|
|
74
|
+
readonly channel?: SurfaceAnswerChannel;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One inbound message, or the reason there will not be one.
|
|
78
|
+
*
|
|
79
|
+
* The non-`received` cases are part of the contract rather than error handling (ADR-055 Decision 6):
|
|
80
|
+
* blocking moves the timeout story, it does not remove it, and a handler must be able to tell the
|
|
81
|
+
* model something true about which happened.
|
|
82
|
+
*/
|
|
83
|
+
export type SurfaceMessage = {
|
|
84
|
+
status: 'received';
|
|
85
|
+
params: Record<string, unknown>;
|
|
86
|
+
} | {
|
|
87
|
+
status: 'expired';
|
|
88
|
+
} | {
|
|
89
|
+
status: 'abandoned';
|
|
90
|
+
};
|
|
91
|
+
/** Why a delivery did not reach an exchange. Reported to the browser, never to the model. */
|
|
92
|
+
export type SurfaceDeliveryRejectionReason = 'unknown-or-closed' | 'binding-mismatch';
|
|
93
|
+
export type DeliverResult = {
|
|
94
|
+
ok: true;
|
|
95
|
+
} | {
|
|
96
|
+
ok: false;
|
|
97
|
+
reason: SurfaceDeliveryRejectionReason;
|
|
98
|
+
};
|
|
99
|
+
/** A live, two-way conversation bound to one in-flight tool call. */
|
|
100
|
+
export interface SurfaceExchange {
|
|
101
|
+
/** Correlation handle embedded in the human's answer carrier; not a secret. */
|
|
102
|
+
readonly id: string;
|
|
103
|
+
/**
|
|
104
|
+
* Sends one message out to the human.
|
|
105
|
+
*
|
|
106
|
+
* @throws {Error} If the exchange has already ended — the same posture the daemon's emitter takes,
|
|
107
|
+
* because a message sent onto a settled call has no way to be answered.
|
|
108
|
+
*/
|
|
109
|
+
send(args: {
|
|
110
|
+
emission: SurfaceEmission;
|
|
111
|
+
}, optional?: Record<string, never>): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Waits for the next inbound message.
|
|
114
|
+
*
|
|
115
|
+
* Returns a buffered one immediately if the human answered while the handler was busy. Once the
|
|
116
|
+
* exchange has ended, returns that terminal status rather than hanging — so a handler looping on
|
|
117
|
+
* `receive()` terminates instead of waiting out the transport.
|
|
118
|
+
*/
|
|
119
|
+
receive(_args: Record<string, never>, optional?: Record<string, never>): Promise<SurfaceMessage>;
|
|
120
|
+
/** Ends the exchange idempotently. Pending receives resolve "abandoned"; an ended exchange is unchanged. */
|
|
121
|
+
close(_args: Record<string, never>, optional?: Record<string, never>): void;
|
|
122
|
+
/**
|
|
123
|
+
* When the exchange expires if nothing more happens, in epoch ms: the earlier of the idle and
|
|
124
|
+
* lifetime deadlines. Activity moves the idle one, so read it right before rendering a deadline
|
|
125
|
+
* for the human; a card drawn just before its `send()` shows a deadline a few ms early, never late.
|
|
126
|
+
*/
|
|
127
|
+
expiresAtMs(): number;
|
|
128
|
+
}
|
|
129
|
+
/** Messaging helpers do not require deadline tracking. Hosts may supply a conversation port
|
|
130
|
+
* without a deadline; the daemon store always returns the full, dated SurfaceExchange. */
|
|
131
|
+
export type SurfaceExchangeConversation = Omit<SurfaceExchange, 'expiresAtMs'> & Partial<Pick<SurfaceExchange, 'expiresAtMs'>>;
|
|
132
|
+
/**
|
|
133
|
+
* What a delivery must prove about itself. `principalId` is load-bearing for every channel — one
|
|
134
|
+
* human's answer must never land in another's call. `toolId` is load-bearing only for a channel
|
|
135
|
+
* whose correlation carrier is itself a tool call: MCP-UI's surface can only answer by issuing one,
|
|
136
|
+
* so it always has a real tool name to offer, and the store checks it.
|
|
137
|
+
*
|
|
138
|
+
* A channel that correlates directly by its own id — A2UI's `surfaceId`, the run protocol's own
|
|
139
|
+
* `surface_request`/`surface_response` — has no natural `toolId` to supply; the browser never
|
|
140
|
+
* learns which tool opened the exchange, only its id. Requiring one anyway would force such a
|
|
141
|
+
* channel to either lie (send a placeholder that means nothing) or grow a lookup this store does
|
|
142
|
+
* not expose. So `toolId` is optional here: when a caller supplies it, it must still match exactly
|
|
143
|
+
* (MCP-UI's behavior is unchanged, byte for byte); when omitted, only `principalId` and the
|
|
144
|
+
* exchange's own unguessable `randomUUID()` id gate the delivery. That pair is still sufficient —
|
|
145
|
+
* the id is not enumerable, and `principalId` is a server-verified session identity, never a value
|
|
146
|
+
* the browser chooses (see `run-ownership.ts`) — so relaxing the caller's OBLIGATION does not
|
|
147
|
+
* relax the exchange's own binding, which `open()` still records honestly as whatever tool actually
|
|
148
|
+
* opened it.
|
|
149
|
+
*/
|
|
150
|
+
export interface SurfaceDeliverySpec {
|
|
151
|
+
exchangeId: string;
|
|
152
|
+
params: Record<string, unknown>;
|
|
153
|
+
principalId: string;
|
|
154
|
+
toolId?: string;
|
|
155
|
+
channel?: SurfaceAnswerChannel;
|
|
156
|
+
}
|
|
157
|
+
export interface SurfaceExchangeStore {
|
|
158
|
+
/**
|
|
159
|
+
* Opens an exchange bound to one in-flight tool call.
|
|
160
|
+
*
|
|
161
|
+
* @param args.emit - The call's live {@link SurfaceEmitter}. Required, not optional: an exchange whose
|
|
162
|
+
* messages cannot reach anybody is a guaranteed deadlock, and taking the emitter here is what makes
|
|
163
|
+
* that state unrepresentable rather than merely discouraged.
|
|
164
|
+
*/
|
|
165
|
+
open(args: {
|
|
166
|
+
binding: SurfaceExchangeBinding;
|
|
167
|
+
emit: SurfaceEmitter;
|
|
168
|
+
}, optional?: Record<string, never>): SurfaceExchange;
|
|
169
|
+
/**
|
|
170
|
+
* Routes one inbound message to the exchange it names, queueing it if nothing is waiting yet.
|
|
171
|
+
* See {@link SurfaceDeliverySpec} for why `toolId` is optional rather than part of
|
|
172
|
+
* {@link SurfaceExchangeBinding} here.
|
|
173
|
+
*/
|
|
174
|
+
deliver(requiredArgs: Pick<SurfaceDeliverySpec, 'exchangeId' | 'params' | 'principalId'>, optionalArgs?: Pick<SurfaceDeliverySpec, 'toolId' | 'channel'>): DeliverResult;
|
|
175
|
+
/**
|
|
176
|
+
* Resolves which open exchange a TYPED answer is for, when the human named none.
|
|
177
|
+
*
|
|
178
|
+
* A person typing into the chat composer does not know exchanges exist, so they cannot carry a
|
|
179
|
+
* {@link SURFACE_EXCHANGE_ID_PARAM} the way a rendered form does. This store is the only thing
|
|
180
|
+
* that knows what is open, so the correlation is recovered here rather than guessed by a client
|
|
181
|
+
* scraping an id out of the surface's own (model-influenced) HTML.
|
|
182
|
+
*
|
|
183
|
+
* Scoped by `toolId` as well as `principalId` on purpose: it is an OPT-IN, not a broadcast. Only
|
|
184
|
+
* a tool that has a safe reading of prose should ever be named here, which is what keeps typed
|
|
185
|
+
* text away from confirmation-shaped exchanges (see {@link SURFACE_TYPED_ANSWER_PARAM}).
|
|
186
|
+
*
|
|
187
|
+
* @returns The single matching open exchange's id, or `undefined` when there is none — or more
|
|
188
|
+
* than one. Ambiguity fails closed: delivering to the wrong one would silently answer a question
|
|
189
|
+
* the human was not looking at, and report their words as an answer to it.
|
|
190
|
+
* @complexity O(n) in the number of open exchanges, which is bounded by concurrent agent runs.
|
|
191
|
+
*/
|
|
192
|
+
findTypedAnswerTarget(args: {
|
|
193
|
+
principalId: string;
|
|
194
|
+
toolId: string;
|
|
195
|
+
}, optional?: Record<string, never>): string | undefined;
|
|
196
|
+
/** Open, unsettled count — for tests and diagnostics only. */
|
|
197
|
+
size(): number;
|
|
198
|
+
}
|
|
199
|
+
/** Send a form, handle its answer, report the outcome, and finally close the exchange.
|
|
200
|
+
* Supply `askThenReport` from `@jini-ai/daemon/surface-exchanges` at the host boundary. */
|
|
201
|
+
export type SurfaceAskThenReport = <Result>(args: {
|
|
202
|
+
exchange: SurfaceExchangeConversation;
|
|
203
|
+
confirmationEmission: SurfaceEmission;
|
|
204
|
+
handle: (answer: SurfaceMessage) => Promise<{
|
|
205
|
+
result: Result;
|
|
206
|
+
outcome?: SurfaceEmission;
|
|
207
|
+
}>;
|
|
208
|
+
}, optional?: Record<string, never>) => Promise<Result>;
|
|
209
|
+
//# sourceMappingURL=surface-exchanges.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The callback param carrying an exchange id from a rendered surface back to the server.
|
|
3
|
+
*
|
|
4
|
+
* Double-underscored to keep it clear of a surface's real fields, and named in one place because
|
|
5
|
+
* three parties must agree on it: whatever builds the surface, the route that routes on it, and the
|
|
6
|
+
* handler that opened the exchange.
|
|
7
|
+
*
|
|
8
|
+
* This is MCP-UI's carrier specifically, because an mcp-ui surface can only answer by issuing a tool
|
|
9
|
+
* call, so its correlation has to ride inside that call's params. A channel that can name the
|
|
10
|
+
* exchange directly should do so — `mcp-ui-tool-calls-route.ts` also accepts a top-level
|
|
11
|
+
* `exchangeId`, and A2UI/`surface_request` correlate by their own `surfaceId`.
|
|
12
|
+
*/
|
|
13
|
+
export const SURFACE_EXCHANGE_ID_PARAM = '__exchangeId';
|
|
14
|
+
/**
|
|
15
|
+
* The callback param a surface's Cancel action sets, so a dismissal resolves the exchange
|
|
16
|
+
* immediately instead of stranding the agent until the idle deadline.
|
|
17
|
+
*
|
|
18
|
+
* Carried as data rather than as a distinct {@link SurfaceMessage} status: "the human answered, and
|
|
19
|
+
* the answer was no" is genuinely an answer, and only the tool that built the surface knows what a
|
|
20
|
+
* dismissal means for its own result.
|
|
21
|
+
*/
|
|
22
|
+
export const SURFACE_DISMISSED_PARAM = '__dismissed';
|
|
23
|
+
/**
|
|
24
|
+
* The callback param carrying free text a human TYPED — into the chat composer — as their answer to
|
|
25
|
+
* an outstanding surface, rather than clicking the rendered form.
|
|
26
|
+
*
|
|
27
|
+
* ## The deadlock this exists to break
|
|
28
|
+
*
|
|
29
|
+
* A held-open exchange can only be resolved by a message that names it, and until now every param
|
|
30
|
+
* shape a surface posts back was one the FORM produced. A human who reads the question, ignores the
|
|
31
|
+
* dialog and types the answer into the composer therefore resolved nothing: their message was queued
|
|
32
|
+
* behind the very run it was meant to unblock, and the agent sat parked until the idle deadline
|
|
33
|
+
* fired minutes later.
|
|
34
|
+
*
|
|
35
|
+
* ## Why it is a separate param rather than the tool's own answer fields
|
|
36
|
+
*
|
|
37
|
+
* Prose is not a selection. A tool whose answer fields are option VALUES the model itself supplied
|
|
38
|
+
* (`assistant_ask_choice`'s `choice`/`selections`) would, if prose were poured into them, report to
|
|
39
|
+
* the model that the administrator picked an option that was never offered. Carrying typed text
|
|
40
|
+
* under its own name is what forces each tool to decide, explicitly, what a typed answer means for
|
|
41
|
+
* its own result — and lets a tool that has no safe reading of one simply ignore it.
|
|
42
|
+
*
|
|
43
|
+
* ## What it deliberately cannot do
|
|
44
|
+
*
|
|
45
|
+
* It cannot confirm anything. `classifyConfirmationAnswer` reads only a literal `decision:
|
|
46
|
+
* "confirm"`, so a typed answer arriving at a confirmation-shaped exchange fails closed to
|
|
47
|
+
* `"declined"` with no code change and no exception — which is the correct reading of "the human
|
|
48
|
+
* wrote a sentence instead of clicking Confirm". Nothing here should ever grow a prose-to-consent
|
|
49
|
+
* mapping.
|
|
50
|
+
*
|
|
51
|
+
* Double-underscored, and named here rather than at a tool, for the same reason as
|
|
52
|
+
* {@link SURFACE_EXCHANGE_ID_PARAM}: several parties must agree on the spelling.
|
|
53
|
+
*/
|
|
54
|
+
export const SURFACE_TYPED_ANSWER_PARAM = '__typedAnswer';
|
|
55
|
+
//# sourceMappingURL=surface-exchanges.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one HTML/XML text escaper. A leaf module with NO imports, on purpose: the old copies
|
|
3
|
+
* lived inside `render.ts`, `form-render.ts` and `static-render.ts` and were duplicated rather than
|
|
4
|
+
* shared because importing them would have closed a runtime import cycle. A module that imports
|
|
5
|
+
* nothing can be imported from anywhere.
|
|
6
|
+
*
|
|
7
|
+
* `&` is replaced first: it is the escape character for every entity after it, so replacing any
|
|
8
|
+
* other character first would double-escape the `&` that replacement introduced. The apostrophe
|
|
9
|
+
* becomes the numeric `'` in HTML, because `'` is undefined in HTML4/XHTML1.
|
|
10
|
+
*/
|
|
11
|
+
/** Escapes `& < > " '` for HTML text or a quoted attribute value. @complexity O(text length). */
|
|
12
|
+
export declare function escapeHtml({ text }: {
|
|
13
|
+
text: string;
|
|
14
|
+
}, _optional?: {}): string;
|
|
15
|
+
/** {@link escapeHtml} with the XML named entity `'` for the apostrophe (sitemap and feed XML).
|
|
16
|
+
* @complexity O(text length). */
|
|
17
|
+
export declare function escapeXml({ text }: {
|
|
18
|
+
text: string;
|
|
19
|
+
}, _optional?: {}): string;
|
|
20
|
+
//# sourceMappingURL=escape.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one HTML/XML text escaper. A leaf module with NO imports, on purpose: the old copies
|
|
3
|
+
* lived inside `render.ts`, `form-render.ts` and `static-render.ts` and were duplicated rather than
|
|
4
|
+
* shared because importing them would have closed a runtime import cycle. A module that imports
|
|
5
|
+
* nothing can be imported from anywhere.
|
|
6
|
+
*
|
|
7
|
+
* `&` is replaced first: it is the escape character for every entity after it, so replacing any
|
|
8
|
+
* other character first would double-escape the `&` that replacement introduced. The apostrophe
|
|
9
|
+
* becomes the numeric `'` in HTML, because `'` is undefined in HTML4/XHTML1.
|
|
10
|
+
*/
|
|
11
|
+
const HTML_ENTITIES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
12
|
+
const XML_ENTITIES = { ...HTML_ENTITIES, "'": "'" };
|
|
13
|
+
const SPECIAL = /[&<>"']/g;
|
|
14
|
+
/** Escapes `& < > " '` for HTML text or a quoted attribute value. @complexity O(text length). */
|
|
15
|
+
export function escapeHtml({ text }, _optional = {}) {
|
|
16
|
+
return text.replace(SPECIAL, (char) => HTML_ENTITIES[char] ?? char);
|
|
17
|
+
}
|
|
18
|
+
/** {@link escapeHtml} with the XML named entity `'` for the apostrophe (sitemap and feed XML).
|
|
19
|
+
* @complexity O(text length). */
|
|
20
|
+
export function escapeXml({ text }, _optional = {}) {
|
|
21
|
+
return text.replace(SPECIAL, (char) => XML_ENTITIES[char] ?? char);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=escape.js.map
|
package/dist/text/index.d.ts
CHANGED
package/dist/text/index.js
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Domain registrations own these declarations; projections never infer risk or authorization. */
|
|
2
|
+
export interface ToolApprovalMetadata {
|
|
3
|
+
readonly class: 'read' | 'edit' | 'trash' | 'delete' | 'restore-over-existing' | 'publish' | 'replace-secret' | 'escalation';
|
|
4
|
+
readonly confirmation: 'direct' | 'policy' | 'plan';
|
|
5
|
+
readonly input?: 'human-form';
|
|
6
|
+
/** Host-owned input classification rule, separate from the declared side effects. */
|
|
7
|
+
readonly rule?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolMetadata<A extends ToolApprovalMetadata = ToolApprovalMetadata> {
|
|
10
|
+
readonly search?: {
|
|
11
|
+
readonly keywords?: string;
|
|
12
|
+
readonly queries?: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
readonly approval?: A;
|
|
15
|
+
readonly mcpUi?: {
|
|
16
|
+
readonly redeemable?: boolean;
|
|
17
|
+
readonly exchangeOnly?: boolean;
|
|
18
|
+
readonly secretField?: {
|
|
19
|
+
readonly secret: true;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export type ToolMetadataById<A extends ToolApprovalMetadata = ToolApprovalMetadata> = Readonly<Record<string, ToolMetadata<A>>>;
|
|
24
|
+
/** Own-property lookup: inherited object keys can never declare a tool's approval or callback access. */
|
|
25
|
+
export declare function toolMetadataFor({ toolId, metadata }: {
|
|
26
|
+
toolId: string;
|
|
27
|
+
metadata: ToolMetadataById | undefined;
|
|
28
|
+
}, _optional?: Record<string, never>): ToolMetadata | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Bind host-owned metadata to registrations returned by a reusable domain package. Existing
|
|
31
|
+
* catalog metadata wins; handlers, authorization and independently validated risk are preserved.
|
|
32
|
+
* @returns New registrations for declared ids; undeclared registrations retain their identity.
|
|
33
|
+
* @complexity O(t) time and space in registrations.
|
|
34
|
+
* @example withToolMetadata({ registrations, metadata: notesMetadata });
|
|
35
|
+
*/
|
|
36
|
+
export declare function withToolMetadata({ registrations, metadata }: {
|
|
37
|
+
registrations: readonly import('./tool-registry.js').ToolRegistration[];
|
|
38
|
+
metadata: ToolMetadataById;
|
|
39
|
+
}, _optional?: Record<string, never>): import('./tool-registry.js').ToolRegistration[];
|
|
40
|
+
/** Immutable domain declarations projected for discovery and human callback gates. */
|
|
41
|
+
export interface ToolMetadataProjections<A extends ToolApprovalMetadata = ToolApprovalMetadata> {
|
|
42
|
+
readonly byId: ToolMetadataById<A>;
|
|
43
|
+
readonly keywords: Readonly<Record<string, string>>;
|
|
44
|
+
readonly queries: Readonly<Record<string, readonly string[]>>;
|
|
45
|
+
readonly approvals: Readonly<Record<string, A>>;
|
|
46
|
+
readonly policyConfirmationIds: ReadonlySet<string>;
|
|
47
|
+
readonly secretFormIds: ReadonlySet<string>;
|
|
48
|
+
readonly secretForms: Readonly<Record<string, {
|
|
49
|
+
readonly secretField: {
|
|
50
|
+
readonly secret: true;
|
|
51
|
+
};
|
|
52
|
+
}>>;
|
|
53
|
+
readonly redeemableIds: ReadonlySet<string>;
|
|
54
|
+
readonly exchangeOnlyIds: ReadonlySet<string>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate projections from the same metadata domain registration consumes. Duplicate ownership
|
|
58
|
+
* fails before any projection is returned. No handler, risk classification or permission is inferred.
|
|
59
|
+
* @param required.domains Domain names and their independently authored declarations.
|
|
60
|
+
* @returns Fresh maps and sets; inputs are never mutated.
|
|
61
|
+
* @throws Error naming both domains claiming the same id.
|
|
62
|
+
* @complexity O(t) time and space in declared tools, performed at composition time.
|
|
63
|
+
* @example projectToolMetadata({ domains: [{ domain: 'notes', tools: notesMetadata }] });
|
|
64
|
+
*/
|
|
65
|
+
export declare function projectToolMetadata<A extends ToolApprovalMetadata>({ domains }: {
|
|
66
|
+
domains: readonly {
|
|
67
|
+
readonly domain: string;
|
|
68
|
+
readonly tools: ToolMetadataById<A>;
|
|
69
|
+
}[];
|
|
70
|
+
}, _optional?: Record<string, never>): ToolMetadataProjections<A>;
|
|
71
|
+
//# sourceMappingURL=tool-metadata.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Own-property lookup: inherited object keys can never declare a tool's approval or callback access. */
|
|
2
|
+
export function toolMetadataFor({ toolId, metadata }, _optional = {}) {
|
|
3
|
+
return metadata && Object.hasOwn(metadata, toolId) ? metadata[toolId] : undefined;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Bind host-owned metadata to registrations returned by a reusable domain package. Existing
|
|
7
|
+
* catalog metadata wins; handlers, authorization and independently validated risk are preserved.
|
|
8
|
+
* @returns New registrations for declared ids; undeclared registrations retain their identity.
|
|
9
|
+
* @complexity O(t) time and space in registrations.
|
|
10
|
+
* @example withToolMetadata({ registrations, metadata: notesMetadata });
|
|
11
|
+
*/
|
|
12
|
+
export function withToolMetadata({ registrations, metadata }, _optional = {}) {
|
|
13
|
+
return registrations.map(registration => {
|
|
14
|
+
const declared = toolMetadataFor({ toolId: registration.descriptor.id, metadata });
|
|
15
|
+
if (!declared || registration.descriptor.metadata)
|
|
16
|
+
return registration;
|
|
17
|
+
return { ...registration, descriptor: { ...registration.descriptor, metadata: declared } };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate projections from the same metadata domain registration consumes. Duplicate ownership
|
|
22
|
+
* fails before any projection is returned. No handler, risk classification or permission is inferred.
|
|
23
|
+
* @param required.domains Domain names and their independently authored declarations.
|
|
24
|
+
* @returns Fresh maps and sets; inputs are never mutated.
|
|
25
|
+
* @throws Error naming both domains claiming the same id.
|
|
26
|
+
* @complexity O(t) time and space in declared tools, performed at composition time.
|
|
27
|
+
* @example projectToolMetadata({ domains: [{ domain: 'notes', tools: notesMetadata }] });
|
|
28
|
+
*/
|
|
29
|
+
export function projectToolMetadata({ domains }, _optional = {}) {
|
|
30
|
+
const byId = Object.create(null);
|
|
31
|
+
const owners = new Map();
|
|
32
|
+
for (const { domain, tools } of domains) {
|
|
33
|
+
for (const [id, metadata] of Object.entries(tools)) {
|
|
34
|
+
const owner = owners.get(id);
|
|
35
|
+
if (owner !== undefined)
|
|
36
|
+
throw new Error(`tool-metadata: '${id}' is declared by both ${owner} and ${domain}`);
|
|
37
|
+
owners.set(id, domain);
|
|
38
|
+
byId[id] = metadata;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const entries = Object.entries(byId);
|
|
42
|
+
const approvals = Object.fromEntries(entries.filter(([, metadata]) => metadata.approval !== undefined).map(([id, metadata]) => [id, metadata.approval]));
|
|
43
|
+
const policyConfirmationIds = new Set(Object.entries(approvals).filter(([, policy]) => policy.confirmation === 'policy' || policy.confirmation === 'plan' || !!policy.rule).map(([id]) => id));
|
|
44
|
+
const secretEntries = entries.filter(([, metadata]) => metadata.mcpUi?.secretField?.secret === true);
|
|
45
|
+
const secretFormIds = new Set(secretEntries.map(([id]) => id));
|
|
46
|
+
return {
|
|
47
|
+
byId,
|
|
48
|
+
keywords: Object.fromEntries(entries.filter(([, metadata]) => metadata.search?.keywords !== undefined).map(([id, metadata]) => [id, metadata.search.keywords])),
|
|
49
|
+
queries: Object.fromEntries(entries.filter(([, metadata]) => metadata.search?.queries !== undefined).map(([id, metadata]) => [id, metadata.search.queries])),
|
|
50
|
+
approvals, policyConfirmationIds, secretFormIds,
|
|
51
|
+
secretForms: Object.fromEntries(secretEntries.map(([id, metadata]) => [id, { secretField: metadata.mcpUi.secretField }])),
|
|
52
|
+
redeemableIds: new Set([...policyConfirmationIds, ...secretFormIds, ...entries.filter(([, metadata]) => metadata.mcpUi?.redeemable === true).map(([id]) => id)]),
|
|
53
|
+
exchangeOnlyIds: new Set([...policyConfirmationIds, ...secretFormIds, ...entries.filter(([, metadata]) => metadata.mcpUi?.exchangeOnly === true).map(([id]) => id)]),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=tool-metadata.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Shared permission checking preserves the command/tool denial contract without loading a domain. */
|
|
2
|
+
export interface AuthorizationRequired {
|
|
3
|
+
principalId: string;
|
|
4
|
+
permission: string;
|
|
5
|
+
workspaceId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AuthorizationOptional {
|
|
8
|
+
entityType?: string | undefined;
|
|
9
|
+
entityId?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export type AuthorizationPort = (required: AuthorizationRequired, optional?: AuthorizationOptional) => Promise<{
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
}>;
|
|
15
|
+
export type LegacyToolAuthorizeFn = (required: AuthorizationRequired & AuthorizationOptional) => Promise<{
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
reason: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** Permission denial shared with the CMS command compatibility export.
|
|
20
|
+
* Keeps the original class name, message and permission/reason fields; no domain imports.
|
|
21
|
+
* @example new ForbiddenError({ message: "denied", permission: "settings.read", reason: "no_grant" }, {});
|
|
22
|
+
*/
|
|
23
|
+
export declare class ForbiddenError extends Error {
|
|
24
|
+
readonly permission: string;
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
constructor({ message, permission, reason }: {
|
|
27
|
+
message: string;
|
|
28
|
+
permission: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}, _optional?: Record<string, never>);
|
|
31
|
+
}
|
|
32
|
+
export interface RequireToolPermissionRequired {
|
|
33
|
+
authorize: AuthorizationPort;
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
principalId: string;
|
|
36
|
+
permission: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RequireToolPermissionOptional {
|
|
39
|
+
entityType?: string | undefined;
|
|
40
|
+
entityId?: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
|
|
44
|
+
* tool whose own domain function carries no `authorize()` call to inherit.
|
|
45
|
+
*
|
|
46
|
+
* This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
|
|
47
|
+
* rule's concern. For each tool that
|
|
48
|
+
* calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
|
|
49
|
+
* to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
|
|
50
|
+
* they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
|
|
51
|
+
* inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
|
|
52
|
+
* Database/Recovery gate in the route instead, so their handlers call this in the route's place.
|
|
53
|
+
* Which kind a given tool is stays documented next to that tool's handler, not here.
|
|
54
|
+
*
|
|
55
|
+
* Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
|
|
56
|
+
* tool caller and a route caller see the identical error shape for an identical denial. Throw
|
|
57
|
+
* rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
|
|
58
|
+
* failed execution and has no response to write to.
|
|
59
|
+
*
|
|
60
|
+
* The defining command module re-exports this class: its barrel also exports mutation appliers
|
|
61
|
+
* that name content features. Pulling that barrel into shared permission wiring would couple
|
|
62
|
+
* every tool domain to those features; kernel ownership preserves the identical denial class
|
|
63
|
+
* without that dependency closure.
|
|
64
|
+
*
|
|
65
|
+
* @param required - The `authorize()` evaluator and the workspace the run is scoped to.
|
|
66
|
+
* @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
|
|
67
|
+
* @throws {ForbiddenError} If `authorize()` denies.
|
|
68
|
+
* @complexity O(1) beyond the injected `authorize()` call.
|
|
69
|
+
* @overallScore 100
|
|
70
|
+
*/
|
|
71
|
+
export declare function requireToolPermission(required: RequireToolPermissionRequired, optional?: RequireToolPermissionOptional): Promise<void>;
|
|
72
|
+
/** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
|
|
73
|
+
export declare function adaptLegacyAuthorize(required: {
|
|
74
|
+
authorize: LegacyToolAuthorizeFn;
|
|
75
|
+
}, _optional?: Record<string, never>): AuthorizationPort;
|
|
76
|
+
//# sourceMappingURL=tool-permissions.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Permission denial shared with the CMS command compatibility export.
|
|
2
|
+
* Keeps the original class name, message and permission/reason fields; no domain imports.
|
|
3
|
+
* @example new ForbiddenError({ message: "denied", permission: "settings.read", reason: "no_grant" }, {});
|
|
4
|
+
*/
|
|
5
|
+
export class ForbiddenError extends Error {
|
|
6
|
+
permission;
|
|
7
|
+
reason;
|
|
8
|
+
constructor({ message, permission, reason }, _optional = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.permission = permission;
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
|
|
16
|
+
* tool whose own domain function carries no `authorize()` call to inherit.
|
|
17
|
+
*
|
|
18
|
+
* This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
|
|
19
|
+
* rule's concern. For each tool that
|
|
20
|
+
* calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
|
|
21
|
+
* to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
|
|
22
|
+
* they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
|
|
23
|
+
* inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
|
|
24
|
+
* Database/Recovery gate in the route instead, so their handlers call this in the route's place.
|
|
25
|
+
* Which kind a given tool is stays documented next to that tool's handler, not here.
|
|
26
|
+
*
|
|
27
|
+
* Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
|
|
28
|
+
* tool caller and a route caller see the identical error shape for an identical denial. Throw
|
|
29
|
+
* rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
|
|
30
|
+
* failed execution and has no response to write to.
|
|
31
|
+
*
|
|
32
|
+
* The defining command module re-exports this class: its barrel also exports mutation appliers
|
|
33
|
+
* that name content features. Pulling that barrel into shared permission wiring would couple
|
|
34
|
+
* every tool domain to those features; kernel ownership preserves the identical denial class
|
|
35
|
+
* without that dependency closure.
|
|
36
|
+
*
|
|
37
|
+
* @param required - The `authorize()` evaluator and the workspace the run is scoped to.
|
|
38
|
+
* @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
|
|
39
|
+
* @throws {ForbiddenError} If `authorize()` denies.
|
|
40
|
+
* @complexity O(1) beyond the injected `authorize()` call.
|
|
41
|
+
* @overallScore 100
|
|
42
|
+
*/
|
|
43
|
+
export async function requireToolPermission(required, optional = {}) {
|
|
44
|
+
const result = await required.authorize({
|
|
45
|
+
principalId: required.principalId, permission: required.permission, workspaceId: required.workspaceId,
|
|
46
|
+
}, optional);
|
|
47
|
+
if (!result.allowed)
|
|
48
|
+
throw new ForbiddenError({
|
|
49
|
+
message: `principal '${required.principalId}' is not authorized for '${required.permission}' (${result.reason})`,
|
|
50
|
+
permission: required.permission, reason: result.reason,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
|
|
54
|
+
export function adaptLegacyAuthorize(required, _optional = {}) {
|
|
55
|
+
return (request, optional = {}) => required.authorize({ ...request, ...optional });
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=tool-permissions.js.map
|
package/dist/tool-registry.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `{descriptor, handler, policy}` triples; the registry's *public* surface
|
|
5
5
|
* (this file's `ToolRegistry` interface, and everything re-exported from
|
|
6
6
|
* this package's root `index.ts`) exposes only `ToolDescriptor`s —
|
|
7
|
-
* `register`/`has`/`list`. It never exposes a way to retrieve a `handler`
|
|
7
|
+
* `register`/`has`/`list`. Revocable host scopes add replacement without retrieval. It never exposes a way to retrieve a `handler`
|
|
8
8
|
* or `policy` back out.
|
|
9
9
|
*
|
|
10
10
|
* That's the load-bearing invariant this task is named after: "handlers
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* `archived provenance ledger` for the 2026-07-29 hardening note.
|
|
27
27
|
*/
|
|
28
28
|
import type { Principal } from './principal.js';
|
|
29
|
+
import type { ToolMetadata } from './tool-metadata.js';
|
|
29
30
|
/** A structural run reference — anything with a stable `id` satisfies this (e.g. `@jini-ai/protocol`'s `RunStatus`). No import needed to satisfy it; see the module doc. */
|
|
30
31
|
export interface RunRef {
|
|
31
32
|
readonly id: string;
|
|
@@ -35,6 +36,8 @@ export interface RunRef {
|
|
|
35
36
|
* handler or policy — those are only reachable via {@link authorizeToolInvocation}.
|
|
36
37
|
*/
|
|
37
38
|
export interface ToolDescriptor {
|
|
39
|
+
/** Domain-owned discovery and approval metadata; grants no execution authority. */
|
|
40
|
+
readonly metadata?: ToolMetadata;
|
|
38
41
|
readonly id: string;
|
|
39
42
|
readonly description?: string;
|
|
40
43
|
/**
|
|
@@ -196,6 +199,21 @@ export interface ToolRegistry {
|
|
|
196
199
|
/** Descriptors only, in registration order. Never the underlying handlers/policies. */
|
|
197
200
|
list(_required: Record<string, never>): readonly ToolDescriptor[];
|
|
198
201
|
}
|
|
202
|
+
/** A host can invalidate its disposable discovery snapshot without exposing handlers. */
|
|
203
|
+
export declare function toolRegistryRevision(required: {
|
|
204
|
+
registry: object;
|
|
205
|
+
}, _optional?: Record<string, never>): number | undefined;
|
|
206
|
+
/** Revocable registration scope on the SAME registry the executor owns. Collision validation
|
|
207
|
+
* completes before mutation, so a failed replacement cannot lose the working generation.
|
|
208
|
+
* Only this closure may remove its own registrations; native and other scopes remain protected.
|
|
209
|
+
* Already-authorized closures are revoked too: confirmation can await a human across disable. */
|
|
210
|
+
export declare function createToolRegistrationScope(required: {
|
|
211
|
+
registry: ToolRegistry;
|
|
212
|
+
}, _optional?: Record<string, never>): {
|
|
213
|
+
replace({ registrations: next }: {
|
|
214
|
+
registrations: readonly ToolRegistration[];
|
|
215
|
+
}, _optional?: Record<string, never>): void;
|
|
216
|
+
};
|
|
199
217
|
/**
|
|
200
218
|
* Creates an empty, append-only `ToolRegistry`.
|
|
201
219
|
*
|