@jini-ai/core 0.6.15-rc.2 → 0.6.16
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 +28 -0
- package/README.md +3 -2
- package/dist/approval.d.ts +19 -4
- package/dist/approval.js +54 -14
- package/dist/batch-tool-inputs.d.ts +8 -0
- package/dist/batch-tool-inputs.js +63 -8
- package/dist/contribution-registry.d.ts +23 -0
- package/dist/fetch/cache.d.ts +1 -0
- package/dist/fetch/cache.js +15 -7
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/optional-peer.d.ts +17 -0
- package/dist/optional-peer.js +40 -0
- package/dist/origin-validation.js +9 -8
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +2 -1
- package/dist/primitives/sse.js +4 -3
- package/dist/primitives/trim-chars.d.ts +25 -0
- package/dist/primitives/trim-chars.js +28 -0
- package/dist/redact.d.ts +0 -1
- package/dist/redact.js +29 -7
- package/dist/registration-kit.js +1 -3
- package/dist/settings/agent-tools.js +7 -5
- package/dist/settings/agent-value-write-tools.js +7 -3
- package/dist/settings/ensure-definitions.js +4 -2
- package/dist/settings/express/http-request.js +2 -1
- package/dist/settings/express/http.js +12 -4
- package/dist/settings/repo.memory.js +5 -2
- package/dist/settings/settings.js +5 -1
- package/dist/settings/tool-registrations.js +9 -3
- package/dist/settings/write-service.js +11 -9
- package/dist/target-scope.js +2 -1
- package/dist/text/index.d.ts +2 -0
- package/dist/text/index.js +2 -0
- package/dist/text/line-tail.d.ts +17 -0
- package/dist/text/line-tail.js +25 -0
- package/dist/text/sanitize-untrusted-text.js +33 -4
- package/dist/tool-exposure.js +20 -16
- package/dist/tool-input-normalization.d.ts +11 -3
- package/dist/tool-input-normalization.js +14 -8
- package/dist/tool-metadata-types.d.ts +5 -0
- package/dist/tool-registry.d.ts +9 -0
- package/package.json +8 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.6.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Remove the GitHub Copilot CLI agent: its def, `createCopilotStreamHandler`/`CopilotStreamEvent`,
|
|
8
|
+
account model discovery and the `@github/copilot-sdk` optional peer, the MCP installer's `copilot`
|
|
9
|
+
slug, and its icon. A saved `copilot` agent id no longer resolves (`getAgentDef` returns `null`).
|
|
10
|
+
CopilotKit/AG-UI is unrelated and unchanged.
|
|
11
|
+
|
|
12
|
+
Drop peers no runtime entry loads: `express` from core and http-kit (type-only imports; type
|
|
13
|
+
consumers keep `@types/express`), `vue` from ui-kit, `react-dom` from vibecoding.
|
|
14
|
+
|
|
15
|
+
## 0.6.15
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- Export async WebCrypto `canonicalArgumentDigest` from `@jini-ai/core/records`, reusing
|
|
20
|
+
`canonicalJson` for identical plugin and host SHA-256 hex digests. Canonical JSON now
|
|
21
|
+
sorts keys recursively by Unicode code point and throws `CanonicalJsonError` for
|
|
22
|
+
non-JSON values and cycles; undefined object properties remain omitted.
|
|
23
|
+
|
|
24
|
+
MCP approval fingerprints reuse the core serializer. Existing ordinary JSON
|
|
25
|
+
fingerprints retain their bytes; non-JSON schemas now fail instead of colliding
|
|
26
|
+
with valid JSON, and astral object keys follow the shared code-point ordering.
|
|
27
|
+
|
|
28
|
+
- Export generic tool-contributor, derived-contributor and paired-registry contracts
|
|
29
|
+
from the existing contribution-registry owner without coupling core to host surfaces.
|
|
30
|
+
|
|
3
31
|
## 0.6.13
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ 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
|
-
The root has no runtime dependencies and never loads settings or Express.
|
|
19
|
-
|
|
18
|
+
The root has no runtime dependencies and never loads settings or Express. The
|
|
19
|
+
`@jini-ai/core/settings/express` adapter uses Express types and the host's injected app; it never
|
|
20
|
+
loads the Express runtime, so this package declares no Express peer.
|
|
20
21
|
|
|
21
22
|
## What you get
|
|
22
23
|
|
package/dist/approval.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type HumanConfirmer } from './registration-kit.js';
|
|
2
2
|
import type { ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolRegistration } from './tool-registry.js';
|
|
3
|
+
/** Maximum human approval wait for one invocation, independent of execution time. */
|
|
4
|
+
export declare const APPROVAL_WAIT_TIMEOUT_MS = 120000;
|
|
3
5
|
/** A host describes the action; core owns the decision lifecycle, never the card format. */
|
|
4
6
|
export interface ApprovalPlan<TDescription> {
|
|
5
7
|
readonly ask: boolean;
|
|
@@ -15,11 +17,13 @@ export type ApprovalAnswer = {
|
|
|
15
17
|
reason: 'declined' | 'expired' | 'abandoned';
|
|
16
18
|
};
|
|
17
19
|
/** 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.
|
|
20
|
+
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
21
|
+
* @param optional.onApprovalStateChange Reports only the human wait, released on every outcome.
|
|
22
|
+
* @complexity O(1) local time/space plus the injected human transport's wait. */
|
|
19
23
|
export declare function resolveApproval({ signal, askHuman }: {
|
|
20
24
|
signal: AbortSignal;
|
|
21
25
|
askHuman: () => Promise<ApprovalAnswer>;
|
|
22
|
-
},
|
|
26
|
+
}, { onApprovalStateChange }?: Pick<ToolExecutionOptions, 'onApprovalStateChange'>): Promise<ApprovalAnswer>;
|
|
23
27
|
export interface RememberedApprovalPort {
|
|
24
28
|
has(required: {
|
|
25
29
|
ctx: ToolExecutionContext;
|
|
@@ -101,6 +105,12 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
101
105
|
}, { createId }?: {
|
|
102
106
|
createId?: () => string;
|
|
103
107
|
}): {
|
|
108
|
+
request: ({ description, signal }: {
|
|
109
|
+
description: TDescription;
|
|
110
|
+
signal: AbortSignal;
|
|
111
|
+
}, { timeoutMs }?: {
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
}) => Promise<ApprovalAnswer>;
|
|
104
114
|
getSnapshot: (_required?: {}, _optional?: {}) => {
|
|
105
115
|
pending: {
|
|
106
116
|
approvalId: string;
|
|
@@ -115,7 +125,10 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
115
125
|
description: TDescription;
|
|
116
126
|
execute: () => Promise<unknown>;
|
|
117
127
|
signal: AbortSignal;
|
|
118
|
-
},
|
|
128
|
+
}, { humanOnly, decide }?: {
|
|
129
|
+
humanOnly?: boolean;
|
|
130
|
+
decide?: (answer: ApprovalAnswer) => void;
|
|
131
|
+
}): {
|
|
119
132
|
status: "approval_required";
|
|
120
133
|
approvalId: string;
|
|
121
134
|
description: TDescription;
|
|
@@ -124,7 +137,9 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
124
137
|
respond({ approvalId, approved }: {
|
|
125
138
|
approvalId: string;
|
|
126
139
|
approved: boolean;
|
|
127
|
-
},
|
|
140
|
+
}, { source }?: {
|
|
141
|
+
source?: "human";
|
|
142
|
+
}): Promise<unknown>;
|
|
128
143
|
};
|
|
129
144
|
/** Apply a host classification through the same handler owner used by domain plans.
|
|
130
145
|
* A host description's string `rememberKey` opts into reusable grants; descriptions remain opaque.
|
package/dist/approval.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { humanConfirmedHandler, requireInputRecord } from './registration-kit.js';
|
|
2
2
|
import { ToolInputError } from './tool-registry.js';
|
|
3
3
|
import { isNonArrayRecord } from './records.js';
|
|
4
|
+
/** Maximum human approval wait for one invocation, independent of execution time. */
|
|
5
|
+
export const APPROVAL_WAIT_TIMEOUT_MS = 120_000;
|
|
4
6
|
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
5
|
-
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
6
|
-
|
|
7
|
+
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
8
|
+
* @param optional.onApprovalStateChange Reports only the human wait, released on every outcome.
|
|
9
|
+
* @complexity O(1) local time/space plus the injected human transport's wait. */
|
|
10
|
+
export async function resolveApproval({ signal, askHuman }, { onApprovalStateChange } = {}) {
|
|
7
11
|
if (signal.aborted)
|
|
8
12
|
return { confirmed: false, reason: 'abandoned' };
|
|
9
13
|
let abandon;
|
|
10
14
|
const aborted = new Promise(resolve => { abandon = () => resolve({ confirmed: false, reason: 'abandoned' }); });
|
|
11
15
|
signal.addEventListener('abort', abandon, { once: true });
|
|
12
16
|
try {
|
|
17
|
+
onApprovalStateChange?.({ pending: true });
|
|
13
18
|
const answer = await Promise.race([askHuman(), aborted]);
|
|
14
19
|
return signal.aborted ? { confirmed: false, reason: 'abandoned' } : answer;
|
|
15
20
|
}
|
|
16
21
|
finally {
|
|
17
22
|
signal.removeEventListener('abort', abandon);
|
|
23
|
+
onApprovalStateChange?.({ pending: false });
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
/** The confirmer is always the run's own delegating principal, as a user. */
|
|
@@ -38,7 +44,7 @@ export function createApprovalHandler(steps, { remembered } = {}) {
|
|
|
38
44
|
/** Ask the host transport; record the choice and any reusable grant only on an explicit yes. */
|
|
39
45
|
const askFresh = async ({ prepared, plan }, options = {}) => {
|
|
40
46
|
const { ctx } = prepared;
|
|
41
|
-
const answer = await resolveApproval({ signal: ctx.signal, askHuman: () => steps.askHuman({ ctx, description: plan.description }, options) });
|
|
47
|
+
const answer = await resolveApproval({ signal: ctx.signal, askHuman: () => steps.askHuman({ ctx, description: plan.description }, options) }, options);
|
|
42
48
|
if (ctx.signal.aborted)
|
|
43
49
|
return abandoned(prepared);
|
|
44
50
|
if (answer.confirmed !== true)
|
|
@@ -153,33 +159,45 @@ export function createApprovalProposalStore({ responseTool, messages }, { create
|
|
|
153
159
|
const listeners = new Set();
|
|
154
160
|
const publish = (next) => { snapshot = next; for (const listener of listeners)
|
|
155
161
|
listener(); };
|
|
156
|
-
const clear = () => {
|
|
162
|
+
const clear = (answer = { confirmed: false, reason: 'abandoned' }) => {
|
|
163
|
+
const decide = action?.decide;
|
|
157
164
|
detach?.();
|
|
158
165
|
detach = null;
|
|
159
166
|
action = null;
|
|
160
167
|
publish({ pending: null, error: null });
|
|
168
|
+
decide?.(answer);
|
|
161
169
|
};
|
|
162
|
-
|
|
170
|
+
/** Resolve authority before consuming the single-use action; model input cannot set source. */
|
|
171
|
+
const actionForReply = ({ approvalId, source }) => {
|
|
172
|
+
if (!action || action.approvalId !== approvalId)
|
|
173
|
+
throw new Error(messages.missing);
|
|
174
|
+
if (action.humanOnly && source !== 'human')
|
|
175
|
+
throw new Error('This decision requires the app dialog');
|
|
176
|
+
return action;
|
|
177
|
+
};
|
|
178
|
+
const store = {
|
|
163
179
|
getSnapshot: (_required = {}, _optional = {}) => snapshot,
|
|
164
180
|
subscribe: ({ listener }, _optional = {}) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
165
|
-
propose({ description, execute, signal },
|
|
181
|
+
propose({ description, execute, signal }, { humanOnly = false, decide } = {}) {
|
|
166
182
|
if (signal.aborted)
|
|
167
183
|
throw new Error(messages.closed);
|
|
168
184
|
if (snapshot.pending)
|
|
169
185
|
throw new Error(messages.pending);
|
|
170
186
|
const frozenDescription = structuredClone(description);
|
|
171
187
|
const approvalId = createId();
|
|
172
|
-
action = { approvalId, execute, signal };
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
action = { approvalId, execute, signal, humanOnly, ...(decide ? { decide } : {}) };
|
|
189
|
+
const abort = () => clear();
|
|
190
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
191
|
+
detach = () => signal.removeEventListener('abort', abort);
|
|
175
192
|
publish({ pending: { approvalId, description: structuredClone(frozenDescription) }, error: null });
|
|
176
193
|
return { status: 'approval_required', approvalId, description: frozenDescription, responseTool };
|
|
177
194
|
},
|
|
178
|
-
async respond({ approvalId, approved },
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
async respond({ approvalId, approved }, { source } = {}) {
|
|
196
|
+
const prepared = actionForReply({ approvalId, source });
|
|
197
|
+
clear(approved === true ? { confirmed: true } : { confirmed: false, reason: 'declined' });
|
|
198
|
+
// An awaiting caller owns execution after this decision; do not start a second lifecycle.
|
|
199
|
+
if (prepared.decide)
|
|
200
|
+
return { status: approved === true ? 'approved' : 'declined' };
|
|
183
201
|
const answer = await resolveApproval({ signal: prepared.signal, askHuman: async () => approved === true ? { confirmed: true } : { confirmed: false, reason: 'declined' } });
|
|
184
202
|
if (!answer.confirmed)
|
|
185
203
|
return { status: 'declined' };
|
|
@@ -192,6 +210,28 @@ export function createApprovalProposalStore({ responseTool, messages }, { create
|
|
|
192
210
|
}
|
|
193
211
|
},
|
|
194
212
|
};
|
|
213
|
+
/** Park a call on this same store without publishing any assistant reply capability. The app
|
|
214
|
+
* dialog alone may decide it. Expiry/abort consume the proposal before resolving the waiter. */
|
|
215
|
+
async function request({ description, signal }, { timeoutMs = APPROVAL_WAIT_TIMEOUT_MS } = {}) {
|
|
216
|
+
const lifetime = new AbortController();
|
|
217
|
+
const abort = () => lifetime.abort();
|
|
218
|
+
if (signal.aborted)
|
|
219
|
+
return { confirmed: false, reason: 'abandoned' };
|
|
220
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
221
|
+
let expired = false;
|
|
222
|
+
const timer = setTimeout(() => { expired = true; lifetime.abort(); }, timeoutMs);
|
|
223
|
+
try {
|
|
224
|
+
return await resolveApproval({ signal: lifetime.signal, askHuman: () => new Promise(resolve => {
|
|
225
|
+
store.propose({ description, signal: lifetime.signal, execute: async () => undefined }, { humanOnly: true, decide: resolve });
|
|
226
|
+
}) }).then(answer => expired ? { confirmed: false, reason: 'expired' } : answer);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
signal.removeEventListener('abort', abort);
|
|
231
|
+
lifetime.abort();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ...store, request };
|
|
195
235
|
}
|
|
196
236
|
/** Apply a host classification through the same handler owner used by domain plans.
|
|
197
237
|
* A host description's string `rememberKey` opts into reusable grants; descriptions remain opaque.
|
|
@@ -20,6 +20,14 @@ export declare const BATCH_GUIDANCE_MARKER = " Batch: ";
|
|
|
20
20
|
export declare function withoutBatchGuidance({ description }: {
|
|
21
21
|
description: string;
|
|
22
22
|
}, _optional?: {}): string;
|
|
23
|
+
/** The top-level `required` fields {@link batchDescriptorFor} moved into its `items` description.
|
|
24
|
+
* A batch schema declares no top-level `required`, so a discovery signature built from it alone
|
|
25
|
+
* makes a single call look argument-free and the model sends `{}` — this recovers them.
|
|
26
|
+
* @returns The moved field names in order; [] for a non-batch schema or a tool that required none.
|
|
27
|
+
* @complexity O(n) in the items description length; pure. */
|
|
28
|
+
export declare function batchItemRequiredFields({ inputSchema }: {
|
|
29
|
+
inputSchema: unknown;
|
|
30
|
+
}, _optional?: {}): string[];
|
|
23
31
|
/** Used when a {@link BatchableTool} names no `maxItems`. */
|
|
24
32
|
export declare const DEFAULT_MAX_BATCH_ITEMS = 25;
|
|
25
33
|
/** One item's outcome. `error` is the domain's own message without the appended input schema,
|
|
@@ -16,6 +16,22 @@ export function withoutBatchGuidance({ description }, _optional = {}) {
|
|
|
16
16
|
return end === -1 ? `${BATCH_GUIDANCE_MARKER}to do several at once,${part}` : part.slice(end + BATCH_GUIDANCE_END.length);
|
|
17
17
|
}).join("");
|
|
18
18
|
}
|
|
19
|
+
const ITEM_NEEDS = " Each item needs ";
|
|
20
|
+
const ITEM_NEEDS_END = " (inside each item).";
|
|
21
|
+
/** The top-level `required` fields {@link batchDescriptorFor} moved into its `items` description.
|
|
22
|
+
* A batch schema declares no top-level `required`, so a discovery signature built from it alone
|
|
23
|
+
* makes a single call look argument-free and the model sends `{}` — this recovers them.
|
|
24
|
+
* @returns The moved field names in order; [] for a non-batch schema or a tool that required none.
|
|
25
|
+
* @complexity O(n) in the items description length; pure. */
|
|
26
|
+
export function batchItemRequiredFields({ inputSchema }, _optional = {}) {
|
|
27
|
+
const items = isPlainObject(inputSchema) && isPlainObject(inputSchema.properties) ? inputSchema.properties.items : undefined;
|
|
28
|
+
const text = isPlainObject(items) && typeof items.description === "string" ? items.description : "";
|
|
29
|
+
const start = text.indexOf(ITEM_NEEDS);
|
|
30
|
+
const end = text.indexOf(ITEM_NEEDS_END, start);
|
|
31
|
+
if (start === -1 || end === -1)
|
|
32
|
+
return [];
|
|
33
|
+
return text.slice(start + ITEM_NEEDS.length, end).split(", ");
|
|
34
|
+
}
|
|
19
35
|
/** Used when a {@link BatchableTool} names no `maxItems`. */
|
|
20
36
|
export const DEFAULT_MAX_BATCH_ITEMS = 25;
|
|
21
37
|
/** Classify a batch result, including legacy envelopes, without dropping per-item evidence.
|
|
@@ -26,7 +42,13 @@ export function batchOutcomeFor({ output }, _options = {}) {
|
|
|
26
42
|
if (!isPlainObject(output) || output.batch !== true || !Array.isArray(output.results) || output.results.length === 0)
|
|
27
43
|
return undefined;
|
|
28
44
|
const failed = output.results.filter(item => isPlainObject(item) && item.ok === false).length;
|
|
29
|
-
return failed
|
|
45
|
+
return outcomeFor({ failed, total: output.results.length });
|
|
46
|
+
}
|
|
47
|
+
/** No failure completes the batch, all failing is an error, anything between is partial. */
|
|
48
|
+
function outcomeFor({ failed, total }) {
|
|
49
|
+
if (failed === 0)
|
|
50
|
+
return 'completed';
|
|
51
|
+
return failed === total ? 'error' : 'partial_failure';
|
|
30
52
|
}
|
|
31
53
|
/** A JSON object: not null, not an array. */
|
|
32
54
|
function isPlainObject(value) {
|
|
@@ -54,8 +76,9 @@ export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
|
|
|
54
76
|
}
|
|
55
77
|
const maxItems = tool.maxItems ?? DEFAULT_MAX_BATCH_ITEMS;
|
|
56
78
|
const requiredFields = schema.required ?? [];
|
|
57
|
-
const
|
|
58
|
-
|
|
79
|
+
const rest = { ...schema };
|
|
80
|
+
delete rest.required;
|
|
81
|
+
const needs = requiredFields.length > 0 ? `${ITEM_NEEDS}${requiredFields.join(", ")}${ITEM_NEEDS_END}` : "";
|
|
59
82
|
const items = {
|
|
60
83
|
type: "array",
|
|
61
84
|
minItems: 1,
|
|
@@ -104,6 +127,38 @@ async function normalizeBatchItem({ registration, defaults, shared, item }) {
|
|
|
104
127
|
const normalized = await normalizeToolInput({ input, registration });
|
|
105
128
|
return { input: normalized.input, notes: [...layers.flatMap(layer => layer.notes), ...normalized.notes] };
|
|
106
129
|
}
|
|
130
|
+
/** Restore the single-call requirement the published batch schema intentionally omits, from the
|
|
131
|
+
* wrapped tool's own schema. Normalize first so repaired spellings and domain aliases satisfy it.
|
|
132
|
+
* @throws ToolInputError with every required name/type when any required value is missing.
|
|
133
|
+
* @complexity O(r) for r required fields, plus the existing normalization; no effects.
|
|
134
|
+
*/
|
|
135
|
+
async function normalizeSingleBatchInput({ input, registration }) {
|
|
136
|
+
const normalized = await normalizeToolInput({ input, registration });
|
|
137
|
+
const value = normalized.input;
|
|
138
|
+
const schema = registration.descriptor.inputSchema;
|
|
139
|
+
const required = schema.required ?? [];
|
|
140
|
+
if (!isPlainObject(value) || required.every(name => value[name] !== undefined))
|
|
141
|
+
return normalized;
|
|
142
|
+
const missing = required.filter(name => value[name] === undefined);
|
|
143
|
+
const fields = required.map(name => `${name} (${schemaTypeLabel(schema.properties?.[name])})`);
|
|
144
|
+
throw new ToolInputError({ message: `${registration.descriptor.id}: missing required inputs: ${missing.join(', ')}. Required inputs: ${fields.join(', ')}.` });
|
|
145
|
+
}
|
|
146
|
+
/** A property's declared JSON type, `a | b` for a type list, `unknown` when undeclared. */
|
|
147
|
+
function schemaTypeLabel(property) {
|
|
148
|
+
const type = isPlainObject(property) ? property.type : undefined;
|
|
149
|
+
if (Array.isArray(type))
|
|
150
|
+
return type.join(' | ');
|
|
151
|
+
return typeof type === 'string' ? type : 'unknown';
|
|
152
|
+
}
|
|
153
|
+
/** A batch result is one JSON value, so an item's MCP `image` block would reach the model and the
|
|
154
|
+
* chat as base64 text (a 20-item import batch, 20 previews). Drop images from item envelopes; the
|
|
155
|
+
* item's text blocks still carry its result. Anything that is not an envelope is returned as is. */
|
|
156
|
+
function withoutItemImages(output) {
|
|
157
|
+
if (!isPlainObject(output) || !Array.isArray(output.content))
|
|
158
|
+
return output;
|
|
159
|
+
const content = output.content.filter((block) => !isPlainObject(block) || block.type !== 'image');
|
|
160
|
+
return content.length === output.content.length ? output : { ...output, content };
|
|
161
|
+
}
|
|
107
162
|
/** Classify one returned output without dropping error payloads or normalization notes.
|
|
108
163
|
* @complexity O(n) for a nested batch's n results, plus formatting its notes; no effects.
|
|
109
164
|
*/
|
|
@@ -119,7 +174,7 @@ function batchItemResult({ index, output, notes }, _options = {}) {
|
|
|
119
174
|
*/
|
|
120
175
|
function summarizeBatchResults({ results }, _options = {}) {
|
|
121
176
|
const succeeded = results.filter((result) => result.ok).length;
|
|
122
|
-
const outcome =
|
|
177
|
+
const outcome = outcomeFor({ failed: results.length - succeeded, total: results.length });
|
|
123
178
|
return { batch: true, outcome, ...(succeeded === 0 ? { isError: true } : {}), total: results.length, succeeded, failed: results.length - succeeded, results };
|
|
124
179
|
}
|
|
125
180
|
/**
|
|
@@ -151,7 +206,7 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
|
|
|
151
206
|
// its policy and planner receive the same targets; never execute items to preflight them.
|
|
152
207
|
normalizeInput: async ({ input }) => {
|
|
153
208
|
if (!Object.hasOwn(input, 'items'))
|
|
154
|
-
return
|
|
209
|
+
return normalizeSingleBatchInput({ input, registration });
|
|
155
210
|
const { items: rawItems, ...shared } = input;
|
|
156
211
|
const items = readItems({ items: rawItems, toolId, maxItems });
|
|
157
212
|
const normalized = [];
|
|
@@ -182,7 +237,7 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
|
|
|
182
237
|
if (!options?.invokeTool)
|
|
183
238
|
throw new Error(`${toolId}: batch items require the gated ToolExecutor invocation port`);
|
|
184
239
|
const output = await options.invokeTool({ toolId, input: normalized.input }, { registry: itemRegistry });
|
|
185
|
-
results.push(batchItemResult({ index, output, notes: normalized.notes }));
|
|
240
|
+
results.push(batchItemResult({ index, output: withoutItemImages(output), notes: normalized.notes }));
|
|
186
241
|
}
|
|
187
242
|
catch (error) {
|
|
188
243
|
results.push({ index, ok: false, error: withNormalizationNotes({ output: itemErrorMessage({ error, toolId }), notes }) });
|
|
@@ -192,9 +247,9 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
|
|
|
192
247
|
};
|
|
193
248
|
return { ...registration, descriptor, handler,
|
|
194
249
|
// Domain normalization belongs to each merged item, not the incomplete shared layer.
|
|
195
|
-
normalizeInput: async ({ input }
|
|
250
|
+
normalizeInput: async ({ input }) => Object.hasOwn(input, 'items')
|
|
196
251
|
? { input, notes: [] }
|
|
197
|
-
:
|
|
252
|
+
: normalizeSingleBatchInput({ input, registration }),
|
|
198
253
|
};
|
|
199
254
|
}
|
|
200
255
|
/**
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import type { DerivedRiskByToolId } from './registration-kit.js';
|
|
2
|
+
import type { ToolRegistration } from './tool-registry.js';
|
|
3
|
+
/** A domain owns its builder and risk classification; the host binds dependencies and surfaces. */
|
|
4
|
+
export interface ToolContributor<TDeps, TSurfaces> {
|
|
5
|
+
readonly domain: string;
|
|
6
|
+
readonly build: (deps: TDeps, surfaces: TSurfaces) => ToolRegistration[];
|
|
7
|
+
readonly risk: DerivedRiskByToolId;
|
|
8
|
+
}
|
|
9
|
+
/** Post-processing sees the domain registrations; ordering and collision checks belong to the host. */
|
|
10
|
+
export interface DerivedToolContributor<TDeps, TSurfaces> {
|
|
11
|
+
readonly domain: string;
|
|
12
|
+
readonly derive: (input: {
|
|
13
|
+
readonly registrations: readonly ToolRegistration[];
|
|
14
|
+
readonly routeDeps: TDeps;
|
|
15
|
+
readonly surfaces: TSurfaces;
|
|
16
|
+
}) => ToolRegistration[];
|
|
17
|
+
readonly risk: DerivedRiskByToolId;
|
|
18
|
+
}
|
|
19
|
+
/** The two ordered registries belong to one composition root, never shared across workspaces. */
|
|
20
|
+
export interface ToolContributions<TDeps, TSurfaces> {
|
|
21
|
+
readonly contributors: ContributionRegistry<ToolContributor<TDeps, TSurfaces>>;
|
|
22
|
+
readonly derivedContributors: ContributionRegistry<DerivedToolContributor<TDeps, TSurfaces>>;
|
|
23
|
+
}
|
|
1
24
|
/** Replace-by-key registry for host-owned contributions; no singleton or execution surface. */
|
|
2
25
|
export interface ContributionRegistry<TContribution> {
|
|
3
26
|
register(required: {
|
package/dist/fetch/cache.d.ts
CHANGED
|
@@ -110,6 +110,7 @@ export declare class FetchQueryCache {
|
|
|
110
110
|
invalidate({ prefix }: {
|
|
111
111
|
prefix: QueryKey;
|
|
112
112
|
}): void;
|
|
113
|
+
private reloadInvalidated;
|
|
113
114
|
onFocus(): void;
|
|
114
115
|
/** Execute a read/write once connectivity returns; failures are not retried.
|
|
115
116
|
* @param required.run Operation to invoke online.
|
package/dist/fetch/cache.js
CHANGED
|
@@ -16,7 +16,9 @@ const browserScheduler = {
|
|
|
16
16
|
// Normalize unknown promise rejections once so consumers receive Error without repeating
|
|
17
17
|
// the same narrowing boilerplate at every read and write call site.
|
|
18
18
|
export function toQueryError(value) {
|
|
19
|
-
|
|
19
|
+
if (value instanceof Error)
|
|
20
|
+
return value;
|
|
21
|
+
return new Error(typeof value === 'string' && value.trim() ? value : 'request failed');
|
|
20
22
|
}
|
|
21
23
|
/** One provider-owned cache. Requests are shared by structural primitive keys.
|
|
22
24
|
* Failed reads retain earlier data; replacement writes supersede older requests.
|
|
@@ -162,12 +164,17 @@ export class FetchQueryCache {
|
|
|
162
164
|
const pendingRead = entry.pendingRead;
|
|
163
165
|
entry.pending = undefined;
|
|
164
166
|
entry.pendingRead = undefined;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
this.reloadInvalidated(entry, pendingRead);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
reloadInvalidated(entry, pendingRead) {
|
|
171
|
+
if (entry.observers.size) {
|
|
172
|
+
this.refresh(entry);
|
|
173
|
+
return;
|
|
170
174
|
}
|
|
175
|
+
// An awaiting loader still needs an answer even if no observer is mounted.
|
|
176
|
+
if (pendingRead)
|
|
177
|
+
void this.load({ entry, ...pendingRead });
|
|
171
178
|
}
|
|
172
179
|
onFocus() {
|
|
173
180
|
for (const entry of this.entries.values()) {
|
|
@@ -249,7 +256,8 @@ export class FetchQueryCache {
|
|
|
249
256
|
}
|
|
250
257
|
scheduleEviction(entry) {
|
|
251
258
|
this.cancelEviction(entry);
|
|
252
|
-
|
|
259
|
+
const inUse = entry.listeners.size > 0 || entry.observers.size > 0 || Boolean(entry.pending);
|
|
260
|
+
if (this.disposed || inUse || !Number.isFinite(entry.retention))
|
|
253
261
|
return;
|
|
254
262
|
entry.eviction = this.scheduler.schedule({ callback: () => this.entries.delete(JSON.stringify(entry.key)), delayMs: entry.retention });
|
|
255
263
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,13 +11,14 @@ export * from './api-token-auth.js';
|
|
|
11
11
|
export * from './origin-validation.js';
|
|
12
12
|
export * from './principal.js';
|
|
13
13
|
export * from './surface-exchanges.js';
|
|
14
|
-
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
14
|
+
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolApprovalDecision, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
15
15
|
export { createToolRegistrationScope, toolRegistryRevision, createToolRegistry, isReadOnlyTool, toolDiscoveryMetadata, ToolInputError } from './tool-registry.js';
|
|
16
16
|
export * from './tool-tokens.js';
|
|
17
17
|
export * from './tool-exposure.js';
|
|
18
18
|
export * from './gated-mutations.js';
|
|
19
19
|
export * from './model-facing-tool-errors.js';
|
|
20
|
-
export
|
|
20
|
+
export { createContributionRegistry } from './contribution-registry.js';
|
|
21
|
+
export type { ContributionRegistry, ToolContributor, DerivedToolContributor, ToolContributions } from './contribution-registry.js';
|
|
21
22
|
export * from './provider-registry.js';
|
|
22
23
|
export * from './object-store.js';
|
|
23
24
|
export * from './naming.js';
|
|
@@ -29,6 +30,6 @@ export * from "./native-regexp.js";
|
|
|
29
30
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, type LegacyToolAuthorizeFn, type AuthorizationPort as ToolPermissionAuthorizationPort, type RequireToolPermissionRequired, type RequireToolPermissionOptional, } from "./tool-permissions.js";
|
|
30
31
|
export { evaluateFeatureFlag, type FeatureFlagDecision } from './feature-flags.js';
|
|
31
32
|
export { normalizeToolInput, withNormalizationNotes, ToolInputNormalizationError, type NormalizedToolInput, type ToolInputNormalizer } from './tool-input-normalization.js';
|
|
32
|
-
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS, type BatchableTool, type BatchEligibility, type BatchOptOut, type BatchResult, type BatchItemResult } from './batch-tool-inputs.js';
|
|
33
|
+
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchItemRequiredFields, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS, type BatchableTool, type BatchEligibility, type BatchOptOut, type BatchResult, type BatchItemResult } from './batch-tool-inputs.js';
|
|
33
34
|
export type { MediaProviderCatalogPolicy, MediaProviderOption, ProviderPreset, ProviderPresetKind } from './provider-catalog.js';
|
|
34
35
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export * from './tool-tokens.js';
|
|
|
14
14
|
export * from './tool-exposure.js';
|
|
15
15
|
export * from './gated-mutations.js';
|
|
16
16
|
export * from './model-facing-tool-errors.js';
|
|
17
|
-
export
|
|
17
|
+
export { createContributionRegistry } from './contribution-registry.js';
|
|
18
18
|
export * from './provider-registry.js';
|
|
19
19
|
export * from './object-store.js';
|
|
20
20
|
export * from './naming.js';
|
|
@@ -25,5 +25,5 @@ export * from "./native-regexp.js";
|
|
|
25
25
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, } from "./tool-permissions.js";
|
|
26
26
|
export { evaluateFeatureFlag } from './feature-flags.js';
|
|
27
27
|
export { normalizeToolInput, withNormalizationNotes, ToolInputNormalizationError } from './tool-input-normalization.js';
|
|
28
|
-
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS } from './batch-tool-inputs.js';
|
|
28
|
+
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchItemRequiredFields, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS } from './batch-tool-inputs.js';
|
|
29
29
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a host-selected feature's optional library, synchronously or asynchronously.
|
|
3
|
+
* Keep the import/require literal in the caller so bundlers and closure guards can see it.
|
|
4
|
+
* @throws A feature-specific load error with the original resolution/ABI error as `cause`.
|
|
5
|
+
* @complexity O(1) wrapper work; loading cost belongs to the supplied loader.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadOptionalPeer<T>({ feature, dependency, load }: {
|
|
8
|
+
feature: string;
|
|
9
|
+
dependency: string;
|
|
10
|
+
load: () => T;
|
|
11
|
+
}, { createError }?: {
|
|
12
|
+
createError?: (input: {
|
|
13
|
+
message: string;
|
|
14
|
+
cause: unknown;
|
|
15
|
+
}) => Error;
|
|
16
|
+
}): T;
|
|
17
|
+
//# sourceMappingURL=optional-peer.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a host-selected feature's optional library, synchronously or asynchronously.
|
|
3
|
+
* Keep the import/require literal in the caller so bundlers and closure guards can see it.
|
|
4
|
+
* @throws A feature-specific load error with the original resolution/ABI error as `cause`.
|
|
5
|
+
* @complexity O(1) wrapper work; loading cost belongs to the supplied loader.
|
|
6
|
+
*/
|
|
7
|
+
export function loadOptionalPeer({ feature, dependency, load }, { createError = ({ message, cause }) => new Error(message, { cause }) } = {}) {
|
|
8
|
+
const failed = (cause) => {
|
|
9
|
+
throw createError({ message: optionalPeerLoadMessage({ feature, dependency, cause }), cause });
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
const library = load();
|
|
13
|
+
// Native import() returns a Promise. Synchronous factories must keep their return contract.
|
|
14
|
+
if (library instanceof Promise)
|
|
15
|
+
return library.catch(failed);
|
|
16
|
+
return library;
|
|
17
|
+
}
|
|
18
|
+
catch (cause) {
|
|
19
|
+
return failed(cause);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Classify loader failures without echoing paths or other untrusted driver diagnostics.
|
|
23
|
+
* Resolution of an internal dependency is not evidence the selected peer is absent.
|
|
24
|
+
* Pure; O(1) branches and O(message length) matching; no errors or side effects. */
|
|
25
|
+
function optionalPeerLoadMessage({ feature, dependency, cause }, _options = {}) {
|
|
26
|
+
const prefix = `${feature} could not load ${dependency}.`;
|
|
27
|
+
const { message: detail = '', code } = cause instanceof Error ? cause : {};
|
|
28
|
+
if (/NODE_MODULE_VERSION|compiled against a different Node\.js version/.test(detail)) {
|
|
29
|
+
return `${prefix} Its native binary was built for a different Node.js/Electron ABI. Rebuild ${dependency} for the host runtime.`;
|
|
30
|
+
}
|
|
31
|
+
if (code === 'ERR_DLOPEN_FAILED') {
|
|
32
|
+
return `${prefix} Its native binary is incompatible with this runtime or architecture. Rebuild ${dependency} for the host runtime.`;
|
|
33
|
+
}
|
|
34
|
+
const missing = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
|
|
35
|
+
if (missing && (detail.includes(`Cannot find module '${dependency}'`) || detail.includes(`Cannot find package '${dependency}'`))) {
|
|
36
|
+
return `${prefix} It is not installed. Install ${dependency} to enable this feature.`;
|
|
37
|
+
}
|
|
38
|
+
return `${prefix} Check its installed dependencies and native binaries.`;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=optional-peer.js.map
|
|
@@ -134,7 +134,13 @@ function parseDottedQuad(host) {
|
|
|
134
134
|
}
|
|
135
135
|
/** RFC 1918 (10/8, 172.16/12, 192.168/16) or link-local (169.254/16), from the first two octets. */
|
|
136
136
|
function isPrivateIpv4Prefix([a, b]) {
|
|
137
|
-
|
|
137
|
+
if (a === 10)
|
|
138
|
+
return true;
|
|
139
|
+
if (a === 172)
|
|
140
|
+
return b >= 16 && b <= 31;
|
|
141
|
+
if (a === 192)
|
|
142
|
+
return b === 168;
|
|
143
|
+
return a === 169 && b === 254;
|
|
138
144
|
}
|
|
139
145
|
/** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
|
|
140
146
|
* Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
|
|
@@ -152,17 +158,12 @@ export function isIpLiteralHostname({ hostname }) {
|
|
|
152
158
|
return true;
|
|
153
159
|
return parseDottedQuad(host) !== undefined;
|
|
154
160
|
}
|
|
161
|
+
const LOOPBACK_OR_UNSPECIFIED_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0', '::']);
|
|
155
162
|
/** Whether `hostname` is loopback, unspecified, or a private-LAN address.
|
|
156
163
|
* Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
|
|
157
164
|
export function isLoopbackOrPrivateLanHost({ hostname }) {
|
|
158
165
|
const host = String(hostname || '').toLowerCase();
|
|
159
|
-
return (host
|
|
160
|
-
host === '127.0.0.1' ||
|
|
161
|
-
host === '::1' ||
|
|
162
|
-
host === '[::1]' ||
|
|
163
|
-
host === '0.0.0.0' ||
|
|
164
|
-
host === '::' ||
|
|
165
|
-
isPrivateIpv4({ hostname: host }));
|
|
166
|
+
return LOOPBACK_OR_UNSPECIFIED_HOSTS.has(host) || isPrivateIpv4({ hostname: host });
|
|
166
167
|
}
|
|
167
168
|
/** Whether a request's `Host` header names a locally-served or explicitly allow-listed host. */
|
|
168
169
|
export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowedOrigins }) {
|
package/dist/primitives/index.js
CHANGED
package/dist/primitives/paths.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimCharsEnd } from './trim-chars.js';
|
|
1
2
|
/** Root of an already `/`-separated Windows path: a drive root (`C:/`) or a UNC share root. */
|
|
2
3
|
function windowsRoot(path) {
|
|
3
4
|
return /^[A-Za-z]:\//.exec(path)?.[0] ?? /^\/\/[^/]+\/[^/]+(?:\/|$)/.exec(path)?.[0];
|
|
@@ -22,7 +23,7 @@ function normalizeAbsolutePath(value, separator) {
|
|
|
22
23
|
const prefix = (windows ? windowsRoot(path) : undefined) ?? (path.startsWith('/') ? '/' : undefined);
|
|
23
24
|
if (!prefix)
|
|
24
25
|
throw new RangeError('pathContains requires resolved absolute paths');
|
|
25
|
-
return
|
|
26
|
+
return trimCharsEnd({ text: prefix, chars: '/' }) + '/' + resolveSegments(path.slice(prefix.length)).join('/');
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Check lexical containment using whole directory segments, including equality.
|