@jini-ai/core 0.6.17 → 0.6.21
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/dist/approval.d.ts +10 -0
- package/dist/approval.js +15 -1
- package/dist/batch-tool-inputs.d.ts +2 -0
- package/dist/batch-tool-inputs.js +7 -3
- package/dist/redact.d.ts +13 -0
- package/dist/redact.js +84 -7
- package/dist/settings/express/stream.js +3 -1
- package/dist/surface-exchanges.d.ts +15 -0
- package/dist/tool-exposure.js +11 -2
- package/dist/tool-registry.d.ts +14 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.6.21
|
|
4
|
+
|
|
5
|
+
- `createPreparedBatchHandler`: prepares a complete direct batch before any effect, reusing the approval handler's input snapshot, cap, duplicate detection and cancellation lifecycle, with no review card.
|
|
6
|
+
|
|
7
|
+
## 0.6.19
|
|
8
|
+
|
|
9
|
+
- Preserve detached, frozen batch schema symbol metadata in tool exposure snapshots and single-call required-field guidance in plan-mode descriptions.
|
|
10
|
+
- Redact complete quoted credential keys and values in inspected and JSON diagnostics, including API keys, cookies, tokens and private keys, while preserving diagnostic context.
|
|
11
|
+
- Add universal `formatErrorForLog` diagnostics that retain stacks, codes and structured context, redact secrets and never interrupt recovery for hostile thrown values.
|
|
12
|
+
- Redact default settings-stream exception logs through the shared secret policy.
|
|
13
|
+
|
|
3
14
|
## 0.6.17
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/dist/approval.d.ts
CHANGED
|
@@ -73,6 +73,16 @@ export interface ApprovalSteps<TPrepared, TDescription> {
|
|
|
73
73
|
export declare function createApprovalHandler<TPrepared, TDescription>(steps: ApprovalSteps<TPrepared, TDescription>, { remembered }?: {
|
|
74
74
|
remembered?: RememberedApprovalPort;
|
|
75
75
|
}): ToolHandler;
|
|
76
|
+
/** Prepare a complete direct batch before effects, reusing the approval owner's input snapshot,
|
|
77
|
+
* cap, duplicate detection and cancellation lifecycle. No review description or transport exists.
|
|
78
|
+
* @returns A handler preserving scalar results and ordered batch results.
|
|
79
|
+
* @throws ToolInputError on cancellation or invalid batches; preparation/effect failures propagate.
|
|
80
|
+
* @complexity Same bounds as createBatchApprovalHandler: O(input bytes) plus bounded port effects.
|
|
81
|
+
* @example createPreparedBatchHandler({ prepare, run });
|
|
82
|
+
*/
|
|
83
|
+
export declare function createPreparedBatchHandler<TPrepared>(steps: Pick<ApprovalSteps<TPrepared, undefined>, 'prepare' | 'run'>, options?: {
|
|
84
|
+
maxItems?: number;
|
|
85
|
+
}): ToolHandler;
|
|
76
86
|
/** Prepare every item before asking once for the complete batch. Reuses the single-call approval
|
|
77
87
|
* lifecycle, including its detached inputs, authenticated confirmer and abort checks.
|
|
78
88
|
* Single inputs preserve their result shape. Batch effects run in order and stop on the first
|
package/dist/approval.js
CHANGED
|
@@ -90,6 +90,20 @@ export function createApprovalHandler(steps, { remembered } = {}) {
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
+
/** Prepare a complete direct batch before effects, reusing the approval owner's input snapshot,
|
|
94
|
+
* cap, duplicate detection and cancellation lifecycle. No review description or transport exists.
|
|
95
|
+
* @returns A handler preserving scalar results and ordered batch results.
|
|
96
|
+
* @throws ToolInputError on cancellation or invalid batches; preparation/effect failures propagate.
|
|
97
|
+
* @complexity Same bounds as createBatchApprovalHandler: O(input bytes) plus bounded port effects.
|
|
98
|
+
* @example createPreparedBatchHandler({ prepare, run });
|
|
99
|
+
*/
|
|
100
|
+
export function createPreparedBatchHandler(steps, options = {}) {
|
|
101
|
+
return createBatchApprovalHandler({ steps: { ...steps,
|
|
102
|
+
describe: () => ({ ask: false, description: undefined }),
|
|
103
|
+
askHuman: async () => { throw new Error('Direct batches have no approval transport'); },
|
|
104
|
+
notConfirmed: ({ reason }) => { throw new ToolInputError({ message: `This tool call was ${reason} before the next change. Retry the remaining changes when ready.` }); },
|
|
105
|
+
}, combineDescriptions: () => undefined }, options);
|
|
106
|
+
}
|
|
93
107
|
/** Prepare every item before asking once for the complete batch. Reuses the single-call approval
|
|
94
108
|
* lifecycle, including its detached inputs, authenticated confirmer and abort checks.
|
|
95
109
|
* Single inputs preserve their result shape. Batch effects run in order and stop on the first
|
|
@@ -128,7 +142,7 @@ export function createBatchApprovalHandler({ steps, combineDescriptions }, { max
|
|
|
128
142
|
plans.push(await steps.describe(entry, options));
|
|
129
143
|
if (!prepared.batch)
|
|
130
144
|
return plans[0];
|
|
131
|
-
return { ask:
|
|
145
|
+
return { ask: plans.some(plan => plan.ask), description: combineDescriptions({ descriptions: plans.map(plan => plan.description) }) };
|
|
132
146
|
},
|
|
133
147
|
askHuman: steps.askHuman,
|
|
134
148
|
run: async ({ prepared, confirmer, choice }, options) => {
|
|
@@ -5,6 +5,8 @@ import { type ToolRegistration } from './tool-registry.js';
|
|
|
5
5
|
* smaller window per item that the caller may still raise. */
|
|
6
6
|
export interface BatchableTool {
|
|
7
7
|
readonly toolId: string;
|
|
8
|
+
/** A domain that owns a combined approval plan receives the whole normalized batch once. */
|
|
9
|
+
readonly mode?: 'items' | 'plan';
|
|
8
10
|
readonly maxItems?: number;
|
|
9
11
|
readonly itemDefaults?: Readonly<Record<string, unknown>>;
|
|
10
12
|
/** Host-specific result constraints, checked on the normalized item before its handler runs. */
|
|
@@ -20,6 +20,10 @@ const ITEM_NEEDS = " Each item needs ";
|
|
|
20
20
|
const ITEM_NEEDS_END = " (inside each item).";
|
|
21
21
|
// Keep discovery requirements through descriptor/schema spreads without adding wire-schema keys.
|
|
22
22
|
const BATCH_ITEM_REQUIRED_FIELDS = Symbol('batchItemRequiredFields');
|
|
23
|
+
/** Shared by item and plan descriptions so neither can make single calls look argument-free. */
|
|
24
|
+
function singleCallGuidance({ fields }, _options = {}) {
|
|
25
|
+
return fields.length > 0 ? `Single call requires: ${fields.join(', ')}. ` : '';
|
|
26
|
+
}
|
|
23
27
|
/** The original top-level `required` fields retained by {@link batchDescriptorFor} as schema metadata.
|
|
24
28
|
* A batch schema declares no top-level `required`, so a discovery signature built from it alone
|
|
25
29
|
* makes a single call look argument-free and the model sends `{}` — this recovers them.
|
|
@@ -87,7 +91,7 @@ export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
|
|
|
87
91
|
delete rest.required;
|
|
88
92
|
const needs = requiredFields.length > 0 ? `${ITEM_NEEDS}${requiredFields.join(", ")}${ITEM_NEEDS_END}` : "";
|
|
89
93
|
// Inside the removable guidance so search keeps only the authored description and signature.
|
|
90
|
-
const singleNeeds =
|
|
94
|
+
const singleNeeds = singleCallGuidance({ fields: requiredFields });
|
|
91
95
|
const items = {
|
|
92
96
|
type: "array",
|
|
93
97
|
minItems: 1,
|
|
@@ -203,14 +207,14 @@ function summarizeBatchResults({ results }, _options = {}) {
|
|
|
203
207
|
* @complexity O(n) bounded normalization/target checks for n items, plus the existing
|
|
204
208
|
* per-item executor or combined plan lifecycle. The default cap is 25.
|
|
205
209
|
*/
|
|
206
|
-
export function withBatchInput({ registration, tool }, { mode = 'items' } = {}) {
|
|
210
|
+
export function withBatchInput({ registration, tool }, { mode = tool.mode ?? 'items' } = {}) {
|
|
207
211
|
const toolId = registration.descriptor.id;
|
|
208
212
|
const maxItems = tool.maxItems ?? DEFAULT_MAX_BATCH_ITEMS;
|
|
209
213
|
const descriptor = batchDescriptorFor({ descriptor: registration.descriptor, tool });
|
|
210
214
|
if (mode === 'plan')
|
|
211
215
|
return {
|
|
212
216
|
...registration, descriptor: { ...descriptor,
|
|
213
|
-
description: `${registration.descriptor.description ?? ''}${BATCH_GUIDANCE_MARKER}up to ${maxItems} inputs are normalized before authorization and passed to the existing combined approval plan.`,
|
|
217
|
+
description: `${registration.descriptor.description ?? ''}${BATCH_GUIDANCE_MARKER}${singleCallGuidance({ fields: batchItemRequiredFields({ inputSchema: descriptor.inputSchema }) })}up to ${maxItems} inputs are normalized before authorization and passed to the existing combined approval plan.`,
|
|
214
218
|
},
|
|
215
219
|
// The plan owns one confirmation. Normalize every merged item before the outer gate so
|
|
216
220
|
// its policy and planner receive the same targets; never execute items to preflight them.
|
package/dist/redact.d.ts
CHANGED
|
@@ -99,6 +99,19 @@ export declare const NON_SECRET_TOKEN_RE: RegExp;
|
|
|
99
99
|
export declare function redactSecrets({ input }: {
|
|
100
100
|
input: string;
|
|
101
101
|
}, options?: SecretRedactionOptions): string;
|
|
102
|
+
/**
|
|
103
|
+
* Universal diagnostic text for any thrown value. Keeps native stacks and structured metadata,
|
|
104
|
+
* then applies the shared credential policy (PII stays readable unless explicitly requested).
|
|
105
|
+
* Diagnostic getters, serialization and coercion can themselves fail; fixed fallback text keeps
|
|
106
|
+
* those failures from replacing the original error or interrupting the caller's recovery.
|
|
107
|
+
* @param required.error - Untrusted thrown value; never mutated.
|
|
108
|
+
* @param options - Shared redaction options, including caller-owned exact secrets.
|
|
109
|
+
* @returns Redacted text; never throws, including for circular objects and hostile accessors.
|
|
110
|
+
* @complexity O(n) serialization in the diagnostic's fields/characters, plus shared redaction passes.
|
|
111
|
+
*/
|
|
112
|
+
export declare function formatErrorForLog(required: {
|
|
113
|
+
error: unknown;
|
|
114
|
+
}, options?: SecretRedactionOptions): string;
|
|
102
115
|
/**
|
|
103
116
|
* Same as {@link redactSecrets} but also returns per-category counts so the
|
|
104
117
|
* caller can attach an audit summary to the trace metadata ('we stripped 2
|
package/dist/redact.js
CHANGED
|
@@ -165,7 +165,9 @@ const PATTERNS = [
|
|
|
165
165
|
// non-card numbers (timestamps, IDs, hashes). We isolate the candidate
|
|
166
166
|
// then run a Luhn check before redacting.
|
|
167
167
|
const CARD_CANDIDATE = /\b(?:\d[ -]?){12,18}\d\b/g;
|
|
168
|
-
|
|
168
|
+
// Inspect/JSON credentials can contain spaces and escaped quotes; consume the entire value.
|
|
169
|
+
const QUOTED_SECRET_VALUE = String.raw `"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'`;
|
|
170
|
+
const API_KEY_HEADER = new RegExp(`(^|[^?&\\w-])(["']?)(x-api-key|api-key|x-goog-api-key)\\2(\\s*[:=]\\s*)(${QUOTED_SECRET_VALUE}|[^\\s,;"'#}]+)`, 'gi');
|
|
169
171
|
const API_KEY_QUERY = /([?&](?:key|api_key|api-key)=)[^&#\s,;"']+/gi;
|
|
170
172
|
// The length guard and the per-digit `d < 0 || d > 9` guard from the origin
|
|
171
173
|
// were both dropped here (a documented deviation from the otherwise-verbatim
|
|
@@ -199,6 +201,9 @@ function isLuhnValid(digits) {
|
|
|
199
201
|
// operator; delegated-tool-calls is the route segment named in delegated tool failures.
|
|
200
202
|
// Matching the ENTIRE run keeps a secret glued onto either spelling redactable.
|
|
201
203
|
const SECRET_LABELS = 'authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie';
|
|
204
|
+
// Keep historical URL categories and the bounded sanitizer's vocabulary stable.
|
|
205
|
+
const SERIALIZED_SECRET_LABELS = `${SECRET_LABELS}|x[-_](?:goog[-_])?api[-_]?key|api key|token|set[-_]cookie|private[-_]?key`;
|
|
206
|
+
const EMPTY_QUOTED_ASSIGNMENT_RE = new RegExp(`\\b(?:${SERIALIZED_SECRET_LABELS})\\s*[:=]\\s*$`, 'i');
|
|
202
207
|
export const LABELED_SECRET_RE = new RegExp(`\\b(${SECRET_LABELS})\\b\\s*[:=]\\s*\\S+`, 'gi');
|
|
203
208
|
export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
|
|
204
209
|
// Pattern parts, only ever used inside the anchored NON_SECRET_TOKEN_RE.
|
|
@@ -209,7 +214,9 @@ export const NON_SECRET_TOKEN_RE = new RegExp(`^(?:${CORRELATION_ID}|delegated-t
|
|
|
209
214
|
// its historical lowercase marker contract and uses the unmodified lexical vocabulary.
|
|
210
215
|
// Reject a masked header/query value before matching, so a following field's credential
|
|
211
216
|
// remains available to the global scan rather than being consumed beside the marker.
|
|
212
|
-
|
|
217
|
+
// Quoted values end at their matching quote; opaque unquoted values retain punctuation.
|
|
218
|
+
// An ampersand cannot start a value: an empty URL parameter must not eat its neighbor.
|
|
219
|
+
const MASK_AWARE_LABELED_SECRET_RE = new RegExp(`(?<![\\w-])((["']?)(?:${SERIALIZED_SECRET_LABELS})\\2\\s*[:=]\\s*)(?!Bearer\\s+\\[REDACTED:[^\\]]+\\](?:\\s|$))(?!\\[REDACTED:[^\\]]+\\][;&])(${QUOTED_SECRET_VALUE}|(?!&)\\S+)`, 'gi');
|
|
213
220
|
// Provider failures also echo short bearer values. Exclude our own marker on subsequent passes.
|
|
214
221
|
// The leading lookahead only lets the lookbehind run where a value can start (see bearer_token).
|
|
215
222
|
const SHORT_BEARER_RE = /(?=[\w.+/=-])(?<=Bearer\s+)[\w.+/=-]+/gi;
|
|
@@ -249,6 +256,59 @@ function isMaskedValue(value) {
|
|
|
249
256
|
export function redactSecrets({ input }, options = {}) {
|
|
250
257
|
return redactSecretsWithCounts({ input }, options).redacted;
|
|
251
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Universal diagnostic text for any thrown value. Keeps native stacks and structured metadata,
|
|
261
|
+
* then applies the shared credential policy (PII stays readable unless explicitly requested).
|
|
262
|
+
* Diagnostic getters, serialization and coercion can themselves fail; fixed fallback text keeps
|
|
263
|
+
* those failures from replacing the original error or interrupting the caller's recovery.
|
|
264
|
+
* @param required.error - Untrusted thrown value; never mutated.
|
|
265
|
+
* @param options - Shared redaction options, including caller-owned exact secrets.
|
|
266
|
+
* @returns Redacted text; never throws, including for circular objects and hostile accessors.
|
|
267
|
+
* @complexity O(n) serialization in the diagnostic's fields/characters, plus shared redaction passes.
|
|
268
|
+
*/
|
|
269
|
+
export function formatErrorForLog(required, options = {}) {
|
|
270
|
+
try {
|
|
271
|
+
const { error } = required;
|
|
272
|
+
let text;
|
|
273
|
+
if (error instanceof Error) {
|
|
274
|
+
const message = String(error.message);
|
|
275
|
+
text = String(error.stack ?? `${error.name}: ${message}`);
|
|
276
|
+
if (!text.includes(message))
|
|
277
|
+
text += `\n${message}`;
|
|
278
|
+
const fields = Object.fromEntries(Object.getOwnPropertyNames(error)
|
|
279
|
+
.filter(key => !['name', 'message', 'stack'].includes(key)).map(key => [key, Reflect.get(error, key)]));
|
|
280
|
+
if (Object.keys(fields).length)
|
|
281
|
+
text += `\n${diagnosticJson({ error: fields }, options)}`;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
text = error !== null && typeof error === 'object' ? diagnosticJson({ error }, options) : String(error);
|
|
285
|
+
}
|
|
286
|
+
return redactSecrets({ input: text }, { redactPii: false, ...options });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return '[Unformattable error]';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** JSON preserves structured fields without coercing objects; track cycles and native Error fields.
|
|
293
|
+
* Property access remains inside formatErrorForLog's recovery boundary. O(n) visited fields. */
|
|
294
|
+
function diagnosticJson({ error }, options = {}) {
|
|
295
|
+
const seen = new WeakSet();
|
|
296
|
+
return JSON.stringify(error, (_key, value) => {
|
|
297
|
+
if (value !== null && typeof value === 'object') {
|
|
298
|
+
if (seen.has(value))
|
|
299
|
+
return '[Circular]';
|
|
300
|
+
seen.add(value);
|
|
301
|
+
if (value instanceof Error)
|
|
302
|
+
return Object.fromEntries(Object.getOwnPropertyNames(value).map(key => [key, Reflect.get(value, key)]));
|
|
303
|
+
}
|
|
304
|
+
// Scrub string contents before JSON escapes their credential labels. Exact-only is kept
|
|
305
|
+
// for the final pass so a caller's literal marker is never reprocessed as a supplied secret.
|
|
306
|
+
if (typeof value === 'string' && options.policy !== 'exact-only') {
|
|
307
|
+
return redactSecrets({ input: value }, { redactPii: false, ...options });
|
|
308
|
+
}
|
|
309
|
+
return typeof value === 'bigint' ? String(value) : value;
|
|
310
|
+
}) ?? String(error);
|
|
311
|
+
}
|
|
252
312
|
/**
|
|
253
313
|
* Same as {@link redactSecrets} but also returns per-category counts so the
|
|
254
314
|
* caller can attach an audit summary to the trace metadata ('we stripped 2
|
|
@@ -317,7 +377,7 @@ function applyPatterns({ text, marker, patterns }) {
|
|
|
317
377
|
// and left `hunter2` visible; `api_key` swallowed the whole `api_key=` label), and they split
|
|
318
378
|
// earlier markers (`a` inside `exact_secret`, `email` inside `[REDACTED:email]`).
|
|
319
379
|
const PROTECTED_SPAN_RE = new RegExp([
|
|
320
|
-
|
|
380
|
+
`(?<![\\w-])["']?(?:${SERIALIZED_SECRET_LABELS})["']?\\s*[:=]`,
|
|
321
381
|
'\\bbearer\\s+',
|
|
322
382
|
URL_SECRET_PARAM_RE.source.slice(0, URL_SECRET_PARAM_RE.source.lastIndexOf('=') + 1),
|
|
323
383
|
URL_CREDENTIALS_RE.source.slice(0, URL_CREDENTIALS_RE.source.indexOf(')') + 1),
|
|
@@ -373,14 +433,30 @@ function redactUrlCredentials({ text, marker }) {
|
|
|
373
433
|
.replace(URL_CREDENTIALS_RE, (match, scheme, userinfo) => isMaskedValue(userinfo) ? match : `${scheme}${marker('url_credentials')}@`)
|
|
374
434
|
.replace(SIGNED_QUERY_RE, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('signed_query')}`);
|
|
375
435
|
}
|
|
436
|
+
/** Mask quoted hints ending in an empty credential assignment before scanning labels. Otherwise
|
|
437
|
+
* their closing quote is mistaken for a value's opening quote and consumes the next field's key,
|
|
438
|
+
* leaving that field's secret unrecognized. Only the hint token is replaced; neighboring labels
|
|
439
|
+
* remain available to the existing matchers, including inspect's unquoted keys.
|
|
440
|
+
* @complexity O(n) scanning the diagnostic's quoted tokens; no I/O or input mutation.
|
|
441
|
+
*/
|
|
442
|
+
function redactEmptyQuotedAssignments({ text, marker }, _options = {}) {
|
|
443
|
+
return text.replace(new RegExp(QUOTED_SECRET_VALUE, 'g'), (quoted) => {
|
|
444
|
+
if (!EMPTY_QUOTED_ASSIGNMENT_RE.test(quoted.slice(1, -1)))
|
|
445
|
+
return quoted;
|
|
446
|
+
const quote = quoted.charAt(0);
|
|
447
|
+
return `${quote}${marker('labeled_secret')}${quote}`;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
376
450
|
/** Values named by a header, query parameter, bearer prefix or credential label. */
|
|
377
451
|
function redactKeyedCredentials({ text, marker }) {
|
|
378
|
-
let out =
|
|
452
|
+
let out = redactEmptyQuotedAssignments({ text, marker });
|
|
453
|
+
out = out.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
|
|
379
454
|
// Already masked values are not new credentials and should not inflate the audit count.
|
|
380
455
|
if (isMaskedValue(value))
|
|
381
456
|
return `${prefix}${quote}${name}${quote}${separator}${value}`;
|
|
382
457
|
marker('api_key_header');
|
|
383
|
-
const
|
|
458
|
+
const valueQuote = /^["']/.test(value) ? value.charAt(0) : '';
|
|
459
|
+
const redactedValue = `${valueQuote}[REDACTED:api_key_header]${valueQuote}`;
|
|
384
460
|
return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
|
|
385
461
|
});
|
|
386
462
|
// The provider rule includes a query fragment in an echoed key; mask that whole value
|
|
@@ -392,12 +468,13 @@ function redactKeyedCredentials({ text, marker }) {
|
|
|
392
468
|
// Sweep other URL parameters before the general labeled rule can consume harmless
|
|
393
469
|
// neighboring parameters (`password=x&page=2` used to lose `&page=2`).
|
|
394
470
|
out = redactUrlSecretParams({ text: out, marker });
|
|
395
|
-
return out.replace(MASK_AWARE_LABELED_SECRET_RE, (match,
|
|
471
|
+
return out.replace(MASK_AWARE_LABELED_SECRET_RE, (match, prefix, _quote, value) => {
|
|
396
472
|
// Replace only the captured value, retaining labels even when no whitespace separates
|
|
397
473
|
// them. Glued credential suffixes fail the complete-marker check and remain maskable.
|
|
398
474
|
if (isMaskedValue(value))
|
|
399
475
|
return match;
|
|
400
|
-
|
|
476
|
+
const valueQuote = /^["']/.test(value) ? value.charAt(0) : '';
|
|
477
|
+
return `${prefix}${valueQuote}${marker('labeled_secret')}${valueQuote}`;
|
|
401
478
|
});
|
|
402
479
|
}
|
|
403
480
|
/** Secret-named URL query/fragment values; values already masked above keep their category. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { redactSecrets } from '../../redact.js';
|
|
1
3
|
/** Starts after HTTP authorization. All scheduling and storage are injected.
|
|
2
4
|
* Poll with bounded pages, tenant-scoped ids and retryable reads; close cancels all schedules.
|
|
3
5
|
* O(page size) per tick and O(1) connection state; repository effects live behind the feed port.
|
|
@@ -149,7 +151,7 @@ function createErrorReporter(optional) {
|
|
|
149
151
|
if (optional.onError)
|
|
150
152
|
optional.onError({ operation, error });
|
|
151
153
|
else
|
|
152
|
-
console.error(`[settings] change feed ${operation} failed`, error);
|
|
154
|
+
console.error(`[settings] change feed ${operation} failed`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
153
155
|
}
|
|
154
156
|
catch { /* Diagnostic failures cannot interrupt stream cleanup or retries. */ }
|
|
155
157
|
};
|
|
@@ -123,6 +123,7 @@ export interface SurfaceExchange {
|
|
|
123
123
|
* When the exchange expires if nothing more happens, in epoch ms: the earlier of the idle and
|
|
124
124
|
* lifetime deadlines. Activity moves the idle one, so read it right before rendering a deadline
|
|
125
125
|
* for the human; a card drawn just before its `send()` shows a deadline a few ms early, never late.
|
|
126
|
+
* Positive infinity denotes a run-lifetime lease; renderers omit a countdown for nonfinite dates.
|
|
126
127
|
*/
|
|
127
128
|
expiresAtMs(): number;
|
|
128
129
|
}
|
|
@@ -155,6 +156,20 @@ export interface SurfaceDeliverySpec {
|
|
|
155
156
|
channel?: SurfaceAnswerChannel;
|
|
156
157
|
}
|
|
157
158
|
export interface SurfaceExchangeStore {
|
|
159
|
+
/** Restore an unanswered call through its original executor, or observe its still-live card.
|
|
160
|
+
* Streams human-only emissions until the exchange settles. A late answer is delivered once to
|
|
161
|
+
* the replacement exchange; it is never retained, logged or used as model input. Optional for
|
|
162
|
+
* compatibility with host-supplied stores that do not support restoration. */
|
|
163
|
+
restore?(required: {
|
|
164
|
+
exchangeId: string;
|
|
165
|
+
principalId: string;
|
|
166
|
+
emit: SurfaceEmitter;
|
|
167
|
+
}, optional?: {
|
|
168
|
+
toolId?: string;
|
|
169
|
+
channel?: SurfaceAnswerChannel;
|
|
170
|
+
params?: Record<string, unknown>;
|
|
171
|
+
signal?: AbortSignal;
|
|
172
|
+
}): Promise<DeliverResult>;
|
|
158
173
|
/**
|
|
159
174
|
* Opens an exchange bound to one in-flight tool call.
|
|
160
175
|
*
|
package/dist/tool-exposure.js
CHANGED
|
@@ -72,14 +72,23 @@ function freezeData(value, seen = new Set()) {
|
|
|
72
72
|
if (value === null || typeof value !== 'object' || Object.isFrozen(value) || seen.has(value))
|
|
73
73
|
return;
|
|
74
74
|
seen.add(value);
|
|
75
|
-
for (const
|
|
76
|
-
freezeData(
|
|
75
|
+
for (const key of Reflect.ownKeys(value))
|
|
76
|
+
freezeData(Reflect.get(value, key), seen);
|
|
77
77
|
Object.freeze(value);
|
|
78
78
|
}
|
|
79
79
|
/** Clone and measure before publication. Invalid data cannot partially change exposure state. */
|
|
80
80
|
function pinDescriptor(descriptor) {
|
|
81
81
|
try {
|
|
82
82
|
const pinned = structuredClone(descriptor);
|
|
83
|
+
// structuredClone omits symbol keys. Batch schemas keep discovery requirements there
|
|
84
|
+
// so providers see a plain wire schema; detach that metadata before freezing it too.
|
|
85
|
+
if (descriptor.inputSchema !== null && typeof descriptor.inputSchema === 'object') {
|
|
86
|
+
for (const symbol of Object.getOwnPropertySymbols(descriptor.inputSchema)) {
|
|
87
|
+
Object.defineProperty(pinned.inputSchema, symbol, {
|
|
88
|
+
value: structuredClone(Reflect.get(descriptor.inputSchema, symbol)), enumerable: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
83
92
|
const json = JSON.stringify(pinned.inputSchema);
|
|
84
93
|
const bytes = json === undefined ? 0 : new TextEncoder().encode(json).byteLength;
|
|
85
94
|
freezeData(pinned);
|
package/dist/tool-registry.d.ts
CHANGED
|
@@ -150,7 +150,20 @@ export interface SurfaceEmission {
|
|
|
150
150
|
* headless or synthetic execution) omits it, and a handler that finds it absent must fall back to
|
|
151
151
|
* returning its surface rather than parking — see the `@jini-ai/daemon` bridge for the supplying end.
|
|
152
152
|
*/
|
|
153
|
-
export type SurfaceEmitter = (emission: SurfaceEmission) => Promise<void
|
|
153
|
+
export type SurfaceEmitter = ((emission: SurfaceEmission) => Promise<void>) & {
|
|
154
|
+
/** Executor-owned lifetime, never supplied by a rendered card. A retry uses the original public
|
|
155
|
+
* tool input and re-enters the complete authorization/audit stack with a fresh human emitter. */
|
|
156
|
+
readonly exchangeLifetime?: {
|
|
157
|
+
readonly signal: AbortSignal;
|
|
158
|
+
readonly runId: string;
|
|
159
|
+
readonly toolId: string;
|
|
160
|
+
readonly principalId: string;
|
|
161
|
+
readonly retry: (required: {
|
|
162
|
+
emit: SurfaceEmitter;
|
|
163
|
+
signal: AbortSignal;
|
|
164
|
+
}, optional?: Record<string, never>) => Promise<void>;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
154
167
|
/** What a `ToolHandler` receives — everything it needs and nothing it could use to bypass the gate (no registry access, no other tools' state). */
|
|
155
168
|
export interface ToolExecutionContext {
|
|
156
169
|
/** The id `ToolExecutor` assigned this call — for the handler's own logging/correlation; carries no authority (the gate already ran before the handler is invoked). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.21",
|
|
4
4
|
"description": "The Jini kernel: typed DI tokens, pack composition and lifecycle, the tool registry, redaction, and origin/API-token auth primitives. Zero dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|