@jini-ai/core 0.4.8 → 0.6.10
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 +23 -2
- package/README.md +5 -0
- package/dist/agent-tools.d.ts +1 -1
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +87 -22
- package/dist/batch-tool-inputs.d.ts +147 -0
- package/dist/batch-tool-inputs.js +257 -0
- package/dist/feature-flags.d.ts +21 -0
- package/dist/feature-flags.js +17 -0
- package/dist/fetch/index.d.ts +2 -1
- package/dist/fetch/index.js +1 -0
- package/dist/fetch/loader-read.d.ts +16 -0
- package/dist/fetch/loader-read.js +22 -0
- package/dist/fetch/types.d.ts +12 -2
- package/dist/gated-mutations/gateway.js +32 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -1
- package/dist/object-store.d.ts +115 -0
- package/dist/object-store.js +70 -0
- package/dist/origin-validation.js +64 -57
- package/dist/primitives/http.d.ts +2 -2
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +17 -10
- package/dist/primitives/tasks.d.ts +10 -0
- package/dist/primitives/tasks.js +8 -0
- package/dist/provider-catalog.d.ts +76 -0
- package/dist/provider-catalog.js +6 -0
- package/dist/provider-registry.d.ts +144 -0
- package/dist/provider-registry.js +138 -0
- package/dist/records.d.ts +24 -0
- package/dist/records.js +25 -0
- package/dist/redact.d.ts +32 -0
- package/dist/redact.js +91 -34
- package/dist/registration-kit.d.ts +7 -4
- package/dist/registration-kit.js +81 -62
- package/dist/settings/agent-value-write-tools.d.ts +19 -0
- package/dist/settings/agent-value-write-tools.js +131 -70
- package/dist/settings/ensure-definitions.d.ts +4 -2
- package/dist/settings/ensure-definitions.js +79 -63
- package/dist/settings/errors.d.ts +13 -0
- package/dist/settings/errors.js +15 -0
- package/dist/settings/express/cms-adapter.d.ts +2 -2
- package/dist/settings/express/cms-adapter.js +34 -22
- package/dist/settings/express/http-request.d.ts +33 -0
- package/dist/settings/express/http-request.js +49 -0
- package/dist/settings/express/http.d.ts +2 -2
- package/dist/settings/express/http.js +16 -39
- package/dist/settings/express/stream.d.ts +2 -2
- package/dist/settings/express/stream.js +49 -27
- package/dist/settings/index.d.ts +2 -2
- package/dist/settings/index.js +1 -1
- package/dist/settings/ports.d.ts +7 -1
- package/dist/settings/repo.memory.js +7 -4
- package/dist/settings/settings.js +54 -60
- package/dist/settings/tool-registrations.d.ts +2 -3
- package/dist/settings/tool-registrations.js +64 -39
- package/dist/settings/write-service.d.ts +42 -4
- package/dist/settings/write-service.js +72 -5
- package/dist/target-scope.d.ts +2 -0
- package/dist/target-scope.js +18 -2
- package/dist/text/escape.js +2 -2
- package/dist/text/sanitize-untrusted-text.d.ts +5 -0
- package/dist/text/sanitize-untrusted-text.js +55 -35
- package/dist/tool-exposure.d.ts +79 -0
- package/dist/tool-exposure.js +163 -0
- package/dist/tool-input-errors.d.ts +23 -0
- package/dist/tool-input-errors.js +24 -0
- package/dist/tool-input-normalization.d.ts +37 -0
- package/dist/tool-input-normalization.js +188 -0
- package/dist/tool-metadata-types.d.ts +26 -0
- package/dist/tool-metadata-types.js +2 -0
- package/dist/tool-metadata.d.ts +2 -23
- package/dist/tool-registry.d.ts +38 -22
- package/dist/tool-registry.js +35 -20
- package/package.json +20 -2
- package/plugin-directory.js +72 -33
package/dist/records.d.ts
CHANGED
|
@@ -21,4 +21,28 @@ export declare function isNonArrayRecord(required: {
|
|
|
21
21
|
}, _optional?: Record<string, never>): required is {
|
|
22
22
|
value: Record<string, unknown>;
|
|
23
23
|
};
|
|
24
|
+
/** Strict equality over the supplied own, enumerable string fields; omitted fields are unconstrained.
|
|
25
|
+
* Memory adapters keep their scope and identity fields in this selector, including on deletes.
|
|
26
|
+
* @complexity O(k) time and space for k selector fields.
|
|
27
|
+
*/
|
|
28
|
+
export declare function matchesRecordFields<Row extends object>({ record, fields }: {
|
|
29
|
+
record: Row;
|
|
30
|
+
fields: Partial<Row>;
|
|
31
|
+
}, _optional?: Record<string, never>): boolean;
|
|
32
|
+
/** Replace the first matching record in place, or append; preserves order and reference semantics.
|
|
33
|
+
* @complexity O(nk) time and O(k) space for n records and k identity fields.
|
|
34
|
+
*/
|
|
35
|
+
export declare function upsertRecord<Row extends object>({ records, record, identity }: {
|
|
36
|
+
records: Row[];
|
|
37
|
+
record: Row;
|
|
38
|
+
identity: Partial<Row>;
|
|
39
|
+
}, _optional?: Record<string, never>): void;
|
|
40
|
+
/** Remove a keyed record only when its additional scope fields match.
|
|
41
|
+
* @complexity O(k) time and space for k selector fields, plus the map lookup.
|
|
42
|
+
*/
|
|
43
|
+
export declare function deleteMapRecord<Key, Row extends object>({ records, key, fields }: {
|
|
44
|
+
records: Map<Key, Row>;
|
|
45
|
+
key: Key;
|
|
46
|
+
fields: Partial<Row>;
|
|
47
|
+
}, _optional?: Record<string, never>): void;
|
|
24
48
|
//# sourceMappingURL=records.d.ts.map
|
package/dist/records.js
CHANGED
|
@@ -18,4 +18,29 @@ export function isRecord(required, _optional = {}) {
|
|
|
18
18
|
export function isNonArrayRecord(required, _optional = {}) {
|
|
19
19
|
return isRecord(required) && !Array.isArray(required.value);
|
|
20
20
|
}
|
|
21
|
+
/** Strict equality over the supplied own, enumerable string fields; omitted fields are unconstrained.
|
|
22
|
+
* Memory adapters keep their scope and identity fields in this selector, including on deletes.
|
|
23
|
+
* @complexity O(k) time and space for k selector fields.
|
|
24
|
+
*/
|
|
25
|
+
export function matchesRecordFields({ record, fields }, _optional = {}) {
|
|
26
|
+
return Object.keys(fields).every(key => record[key] === fields[key]);
|
|
27
|
+
}
|
|
28
|
+
/** Replace the first matching record in place, or append; preserves order and reference semantics.
|
|
29
|
+
* @complexity O(nk) time and O(k) space for n records and k identity fields.
|
|
30
|
+
*/
|
|
31
|
+
export function upsertRecord({ records, record, identity }, _optional = {}) {
|
|
32
|
+
const index = records.findIndex(candidate => matchesRecordFields({ record: candidate, fields: identity }));
|
|
33
|
+
if (index === -1)
|
|
34
|
+
records.push(record);
|
|
35
|
+
else
|
|
36
|
+
records[index] = record;
|
|
37
|
+
}
|
|
38
|
+
/** Remove a keyed record only when its additional scope fields match.
|
|
39
|
+
* @complexity O(k) time and space for k selector fields, plus the map lookup.
|
|
40
|
+
*/
|
|
41
|
+
export function deleteMapRecord({ records, key, fields }, _optional = {}) {
|
|
42
|
+
const record = records.get(key);
|
|
43
|
+
if (record && matchesRecordFields({ record, fields }))
|
|
44
|
+
records.delete(key);
|
|
45
|
+
}
|
|
21
46
|
//# sourceMappingURL=records.js.map
|
package/dist/redact.d.ts
CHANGED
|
@@ -38,12 +38,44 @@ export interface SecretShapePattern {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const PEM_PATTERN_NAME = "PEM private key block";
|
|
40
40
|
export declare const SECRET_SHAPE_PATTERNS: readonly SecretShapePattern[];
|
|
41
|
+
/** Broad token shapes for agent-failure text; consumers retain their plain [REDACTED] pipeline. */
|
|
42
|
+
export declare const AGENT_FAILURE_SECRET_SHAPE_RE: RegExp;
|
|
43
|
+
/**
|
|
44
|
+
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
45
|
+
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
46
|
+
* a match). `secrets` is the owner's explicit example: whatever a site or app tree names its
|
|
47
|
+
* credentials folder (`sites/**\/secrets/`, `apps/admin/**\/secrets/`, or anywhere else), it never
|
|
48
|
+
* becomes reachable through this domain. A denied segment is refused by the guarded reader
|
|
49
|
+
* AND skipped entirely by the `fs_list_files` walk — its contents are never even enumerated,
|
|
50
|
+
* not merely refused once named. Besides `secrets`, the set names the
|
|
51
|
+
* well-known credential-store directories a home-directory `custom` root can expose: `.aws`
|
|
52
|
+
* (`credentials`), `.docker` (`config.json` with registry auth), `.kube` (`config`), and
|
|
53
|
+
* `gcloud`/`gh` for `.config/gcloud/application_default_credentials.json` and
|
|
54
|
+
* `.config/gh/hosts.yml`. Denied by SEGMENT rather than by leaf filename on purpose: `config`,
|
|
55
|
+
* `config.json`, and `hosts.yml` are generic names that appear harmlessly elsewhere, so denying the
|
|
56
|
+
* directory is the narrower of the two mistakes.
|
|
57
|
+
*
|
|
58
|
+
* A host's install-wide site-key directory (2026-09-24) needs the same protection; the host appends it.
|
|
59
|
+
* Production stores it beside individual site folders on a durable volume; local/dev stores
|
|
60
|
+
* it below the home directory, reachable through an operator-set `custom` root. Its key backs
|
|
61
|
+
* site-key administration, webhook/newsletter signing secrets, and permission checks, so the
|
|
62
|
+
* agent must never read it from disk. Denying the whole directory segment, rather than only
|
|
63
|
+
* a generated key filename, also prevents `fs_list_files` from enumerating its contents — the
|
|
64
|
+
* same "never even advertise it" property `secrets` gets above.
|
|
65
|
+
*/
|
|
66
|
+
export declare const CREDENTIAL_FILE_DENY_SEGMENTS: readonly string[];
|
|
67
|
+
export declare const LOCAL_OWNER_PROXY_MARKER_PREFIXES: readonly string[];
|
|
68
|
+
export declare const LOCAL_OWNER_PROXY_MARKER_HEADERS: readonly string[];
|
|
41
69
|
export interface SecretRedactionPattern {
|
|
42
70
|
name: string;
|
|
43
71
|
regex: RegExp;
|
|
44
72
|
}
|
|
45
73
|
/** Optional caller-owned secrets and named lexical rules, without ambient credential lookup. */
|
|
46
74
|
export interface SecretRedactionOptions {
|
|
75
|
+
/** Diagnostic text can retain addresses/numbers while using the same credential rules. Default true. */
|
|
76
|
+
redactPii?: boolean;
|
|
77
|
+
/** Exact trusted registration IDs; credential patterns always take precedence. */
|
|
78
|
+
diagnosticToolIds?: readonly string[];
|
|
47
79
|
/** Conservative by default; aggressive also masks bare 20+ character opaque runs. */
|
|
48
80
|
policy?: 'conservative' | 'aggressive';
|
|
49
81
|
exactSecrets?: ReadonlyArray<string | undefined | null>;
|
package/dist/redact.js
CHANGED
|
@@ -52,6 +52,40 @@ export const SECRET_SHAPE_PATTERNS = [
|
|
|
52
52
|
{ name: "Fastmail app password (fm2_)", pattern: /(?<![A-Za-z0-9+/=])fm2_[A-Za-z0-9+/=]{20,}/ },
|
|
53
53
|
{ name: PEM_PATTERN_NAME, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
54
54
|
];
|
|
55
|
+
/** Broad token shapes for agent-failure text; consumers retain their plain [REDACTED] pipeline. */
|
|
56
|
+
export const AGENT_FAILURE_SECRET_SHAPE_RE = /\b(?:AIza[\w-]+|AQ\.[\w.\/-]+|sk-[\w.-]+|gh[pousr]_[\w]+|github_pat_[\w]+|ya29\.[\w.-]+|eyJ[\w-]+\.[\w-]+(?:\.[\w-]+)?)/g;
|
|
57
|
+
/**
|
|
58
|
+
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
59
|
+
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
60
|
+
* a match). `secrets` is the owner's explicit example: whatever a site or app tree names its
|
|
61
|
+
* credentials folder (`sites/**\/secrets/`, `apps/admin/**\/secrets/`, or anywhere else), it never
|
|
62
|
+
* becomes reachable through this domain. A denied segment is refused by the guarded reader
|
|
63
|
+
* AND skipped entirely by the `fs_list_files` walk — its contents are never even enumerated,
|
|
64
|
+
* not merely refused once named. Besides `secrets`, the set names the
|
|
65
|
+
* well-known credential-store directories a home-directory `custom` root can expose: `.aws`
|
|
66
|
+
* (`credentials`), `.docker` (`config.json` with registry auth), `.kube` (`config`), and
|
|
67
|
+
* `gcloud`/`gh` for `.config/gcloud/application_default_credentials.json` and
|
|
68
|
+
* `.config/gh/hosts.yml`. Denied by SEGMENT rather than by leaf filename on purpose: `config`,
|
|
69
|
+
* `config.json`, and `hosts.yml` are generic names that appear harmlessly elsewhere, so denying the
|
|
70
|
+
* directory is the narrower of the two mistakes.
|
|
71
|
+
*
|
|
72
|
+
* A host's install-wide site-key directory (2026-09-24) needs the same protection; the host appends it.
|
|
73
|
+
* Production stores it beside individual site folders on a durable volume; local/dev stores
|
|
74
|
+
* it below the home directory, reachable through an operator-set `custom` root. Its key backs
|
|
75
|
+
* site-key administration, webhook/newsletter signing secrets, and permission checks, so the
|
|
76
|
+
* agent must never read it from disk. Denying the whole directory segment, rather than only
|
|
77
|
+
* a generated key filename, also prevents `fs_list_files` from enumerating its contents — the
|
|
78
|
+
* same "never even advertise it" property `secrets` gets above.
|
|
79
|
+
*/
|
|
80
|
+
// Generic credential-store segments only. A host adds its own product-specific key directory
|
|
81
|
+
// (e.g. its install-wide site-key folder) when it builds its deny-list; core stays product-neutral.
|
|
82
|
+
export const CREDENTIAL_FILE_DENY_SEGMENTS = ["secrets", ".aws", ".docker", ".kube", "gcloud", "gh"];
|
|
83
|
+
// A local tunnel has a loopback socket and may rewrite Host; refuse marker presence, even empty.
|
|
84
|
+
export const LOCAL_OWNER_PROXY_MARKER_PREFIXES = ["x-forwarded-", "x-envoy-", "x-ms-"];
|
|
85
|
+
export const LOCAL_OWNER_PROXY_MARKER_HEADERS = [
|
|
86
|
+
"forwarded", "x-real-ip", "via", "cf-connecting-ip", "true-client-ip", "x-original-host",
|
|
87
|
+
"x-client-ip", "x-cluster-client-ip", "fastly-client-ip", "tailscale-user-login", "x-original-forwarded-for",
|
|
88
|
+
];
|
|
55
89
|
// Order matters: list specific rules before more general ones. Langfuse
|
|
56
90
|
// keys (`sk-lf-...`) would otherwise be eaten by the generic `sk-...`
|
|
57
91
|
// rule and labeled as a generic OpenAI-style key.
|
|
@@ -139,21 +173,17 @@ function isLuhnValid(digits) {
|
|
|
139
173
|
}
|
|
140
174
|
return sum % 10 === 0;
|
|
141
175
|
}
|
|
142
|
-
function redactApiKeyHeaderValue(prefix, quote, name, separator, value) {
|
|
143
|
-
const redactedValue = value.startsWith('"') ? '"[REDACTED:api_key_header]"' : '[REDACTED:api_key_header]';
|
|
144
|
-
return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
|
|
145
|
-
}
|
|
146
176
|
// A labeled credential has its value masked even when too short to have a vendor token shape.
|
|
147
177
|
// Bare long opaque runs are opt-in CLI heuristics: UUIDs, hashes, model IDs and path segments
|
|
148
178
|
// share that alphabet, so the default telemetry policy must leave them readable. Whole
|
|
149
|
-
// correlation IDs and the delegated-tool route stay readable even in aggressive mode;
|
|
179
|
+
// correlation IDs, UPPER_SNAKE diagnostic codes and the delegated-tool route stay readable even in aggressive mode;
|
|
150
180
|
// appending secret characters defeats that exception. The host's ERR-XXXX-XXXX-XXXX-XXXX
|
|
151
181
|
// correlation ID uses uppercase hex and is meant to reach the model and be quoted to an
|
|
152
182
|
// operator; delegated-tool-calls is the route segment named in delegated tool failures.
|
|
153
183
|
// Matching the ENTIRE run keeps a secret glued onto either spelling redactable.
|
|
154
184
|
export const LABELED_SECRET_RE = /\b(authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie)\b\s*[:=]\s*\S+/gi;
|
|
155
185
|
export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
|
|
156
|
-
export const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls)$/;
|
|
186
|
+
export const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls|[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)$/;
|
|
157
187
|
// Runtime markers must survive a second pass; the bounded sanitizer deliberately retains
|
|
158
188
|
// its historical lowercase marker contract and uses the unmodified lexical vocabulary.
|
|
159
189
|
// Reject a masked header/query value before matching, so a following field's credential
|
|
@@ -199,39 +229,71 @@ export function redactSecretsWithCounts({ input }, options = {}) {
|
|
|
199
229
|
const counts = {};
|
|
200
230
|
if (!input)
|
|
201
231
|
return { redacted: input, counts };
|
|
202
|
-
let out = input;
|
|
203
232
|
const marker = (name) => {
|
|
204
233
|
counts[name] = (counts[name] ?? 0) + 1;
|
|
205
234
|
return `[REDACTED:${name}]`;
|
|
206
235
|
};
|
|
207
|
-
|
|
236
|
+
let out = redactExactSecrets({ text: input, marker, secrets: options.exactSecrets ?? [] });
|
|
237
|
+
out = redactUrlCredentials({ text: out, marker });
|
|
238
|
+
out = applyPatterns({ text: out, marker, patterns: credentialPatterns(options.extraPatterns ?? []) });
|
|
239
|
+
out = redactKeyedCredentials({ text: out, marker });
|
|
240
|
+
if (options.policy === 'aggressive') {
|
|
241
|
+
const trustedIds = new Set(options.diagnosticToolIds);
|
|
242
|
+
out = out.replace(/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g, token => trustedIds.has(token) ? token
|
|
243
|
+
: token.replace(OPAQUE_TOKEN_RE, match => NON_SECRET_TOKEN_RE.test(match) ? match : marker('opaque_token')));
|
|
244
|
+
}
|
|
245
|
+
if (options.redactPii !== false)
|
|
246
|
+
out = redactPii({ text: out, marker });
|
|
247
|
+
return { redacted: out, counts };
|
|
248
|
+
}
|
|
249
|
+
// PII rules run last (after credential and opt-in opaque rules) and only when PII redaction is on;
|
|
250
|
+
// splitting once keeps each list in PATTERNS order.
|
|
251
|
+
const PII_PATTERN_NAMES = ['email', 'ipv4', 'phone'];
|
|
252
|
+
const NON_PII_PATTERNS = PATTERNS.filter((pattern) => !PII_PATTERN_NAMES.includes(pattern.name));
|
|
253
|
+
const PII_PATTERNS = PATTERNS.filter((pattern) => PII_PATTERN_NAMES.includes(pattern.name));
|
|
254
|
+
/** Built-in credential rules, then the static-scan shapes (labeled `credential`), then the caller's extra rules. */
|
|
255
|
+
function credentialPatterns(extraPatterns) {
|
|
256
|
+
return [
|
|
257
|
+
...NON_PII_PATTERNS,
|
|
258
|
+
...SECRET_SHAPE_PATTERNS.map((shape) => ({ name: 'credential', regex: shape.pattern })),
|
|
259
|
+
...extraPatterns,
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
function applyPatterns({ text, marker, patterns }) {
|
|
263
|
+
let out = text;
|
|
264
|
+
for (const { name, regex } of patterns) {
|
|
208
265
|
// Clone host/global regexes so lastIndex state never leaks across requests. Named extra
|
|
209
266
|
// patterns use the same global replacement contract as the built-in vocabulary.
|
|
210
267
|
const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
|
211
268
|
out = out.replace(new RegExp(regex.source, flags), () => marker(name));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/** Masks caller-supplied secrets verbatim, longest first, ignoring empty and non-string entries. */
|
|
273
|
+
function redactExactSecrets({ text, marker, secrets }) {
|
|
274
|
+
const usable = [...new Set(secrets.filter((secret) => typeof secret === 'string' && secret.length > 0))].sort((a, b) => b.length - a.length);
|
|
275
|
+
let out = text;
|
|
276
|
+
for (const secret of usable) {
|
|
215
277
|
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
216
278
|
out = out.replace(new RegExp(escaped, 'g'), () => marker('exact_secret'));
|
|
217
279
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
out = out.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
/** URL userinfo and signed query values: credentials recognized by their URL context. */
|
|
283
|
+
function redactUrlCredentials({ text, marker }) {
|
|
284
|
+
return text
|
|
285
|
+
.replace(URL_CREDENTIALS_RE, (match, scheme, userinfo) => isMaskedValue(userinfo) ? match : `${scheme}${marker('url_credentials')}@`)
|
|
286
|
+
.replace(SIGNED_QUERY_RE, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('signed_query')}`);
|
|
287
|
+
}
|
|
288
|
+
/** Values named by a header, query parameter, bearer prefix or credential label. */
|
|
289
|
+
function redactKeyedCredentials({ text, marker }) {
|
|
290
|
+
let out = text.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
|
|
230
291
|
// Already masked values are not new credentials and should not inflate the audit count.
|
|
231
292
|
if (isMaskedValue(value))
|
|
232
293
|
return `${prefix}${quote}${name}${quote}${separator}${value}`;
|
|
233
294
|
marker('api_key_header');
|
|
234
|
-
|
|
295
|
+
const redactedValue = value.startsWith('"') ? '"[REDACTED:api_key_header]"' : '[REDACTED:api_key_header]';
|
|
296
|
+
return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
|
|
235
297
|
});
|
|
236
298
|
// The provider rule includes a query fragment in an echoed key; mask that whole value
|
|
237
299
|
// before the narrower core query rule can split it and leave a credential suffix behind.
|
|
@@ -239,24 +301,19 @@ export function redactSecretsWithCounts({ input }, options = {}) {
|
|
|
239
301
|
out = out.replace(API_KEY_QUERY, (match, prefix) => isMaskedValue(match.slice(prefix.length)) ? match : `${prefix}${marker('api_key_query')}`);
|
|
240
302
|
out = out.replace(SHORT_BEARER_RE, () => marker('bearer_token'));
|
|
241
303
|
out = out.replace(PROVIDER_API_KEY_HEADER, (match, name, value) => isMaskedValue(value) ? match : `${name}: ${marker('api_key_header')}`);
|
|
242
|
-
|
|
304
|
+
return out.replace(MASK_AWARE_LABELED_SECRET_RE, (match, _label, value) => {
|
|
243
305
|
// Replace only the captured value, retaining labels even when no whitespace separates
|
|
244
306
|
// them. Glued credential suffixes fail the complete-marker check and remain maskable.
|
|
245
307
|
if (isMaskedValue(value))
|
|
246
308
|
return match;
|
|
247
309
|
return `${match.slice(0, -value.length)}${marker('labeled_secret')}`;
|
|
248
310
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (piiNames.has(pattern.name))
|
|
254
|
-
applyPattern(pattern);
|
|
255
|
-
}
|
|
256
|
-
out = out.replace(CARD_CANDIDATE, (match) => {
|
|
311
|
+
}
|
|
312
|
+
/** Email/IPv4/phone rules, then Luhn-checked card candidates. */
|
|
313
|
+
function redactPii({ text, marker }) {
|
|
314
|
+
return applyPatterns({ text, marker, patterns: PII_PATTERNS }).replace(CARD_CANDIDATE, (match) => {
|
|
257
315
|
const digits = match.replace(/\D/g, '');
|
|
258
316
|
return isLuhnValid(digits) ? marker('credit_card') : match;
|
|
259
317
|
});
|
|
260
|
-
return { redacted: out, counts };
|
|
261
318
|
}
|
|
262
319
|
//# sourceMappingURL=redact.js.map
|
|
@@ -140,14 +140,16 @@ export declare function requireInputRecord(required: {
|
|
|
140
140
|
input: unknown;
|
|
141
141
|
}, _optional?: Record<string, never>): Record<string, unknown>;
|
|
142
142
|
/**
|
|
143
|
-
* Reads a required non-empty
|
|
144
|
-
* @throws {ToolInputError} If the field is missing,
|
|
143
|
+
* Reads a required string, non-empty by default; allowEmpty supports literal text deletion.
|
|
144
|
+
* @throws {ToolInputError} If the field is missing, not a string, or empty without allowEmpty.
|
|
145
145
|
* @complexity O(k) for k input keys on a missing-field rejection; otherwise O(1).
|
|
146
146
|
*/
|
|
147
147
|
export declare function requireString(required: {
|
|
148
148
|
input: Record<string, unknown>;
|
|
149
149
|
key: string;
|
|
150
|
-
},
|
|
150
|
+
}, { allowEmpty }?: {
|
|
151
|
+
allowEmpty?: boolean;
|
|
152
|
+
}): string;
|
|
151
153
|
export declare function requireNumber(requiredArgs: {
|
|
152
154
|
input: Record<string, unknown>;
|
|
153
155
|
key: string;
|
|
@@ -261,7 +263,7 @@ export declare function decorateWithSchema(params: {
|
|
|
261
263
|
}): Error;
|
|
262
264
|
/**
|
|
263
265
|
* Build-time gate on a single tool's risk metadata: refuses to wire a tool whose declared
|
|
264
|
-
* `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
|
|
266
|
+
* authorization permission is missing, whose `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
|
|
265
267
|
* whose id that layer has not classified at all, or whose `actorClassRule` needs a human confirmer
|
|
266
268
|
* its handler does not ask for.
|
|
267
269
|
*
|
|
@@ -327,6 +329,7 @@ export declare function buildDomainRegistrations(spec: {
|
|
|
327
329
|
catalogModule: string;
|
|
328
330
|
catalog: ReadonlyMap<string, AgentToolDefinition>;
|
|
329
331
|
handlers: Record<string, ToolHandler>;
|
|
332
|
+
inputNormalizers?: Readonly<Record<string, import('./tool-input-normalization.js').ToolInputNormalizer>>;
|
|
330
333
|
derivedRisk: DerivedRiskByToolId;
|
|
331
334
|
/** Host vocabulary and reviewed action policy; risk remains independently checked above. */
|
|
332
335
|
metadata?: import('./tool-metadata.js').ToolMetadataById;
|
package/dist/registration-kit.js
CHANGED
|
@@ -124,43 +124,37 @@ function missingFieldHint(required, _optional = {}) {
|
|
|
124
124
|
}
|
|
125
125
|
return hint;
|
|
126
126
|
}
|
|
127
|
+
/** The refusal for a required field: the expected kind, plus received-key hints only when the key is absent. */
|
|
128
|
+
function requiredFieldError(required, _optional = {}) {
|
|
129
|
+
const { input, key, kind } = required;
|
|
130
|
+
const hint = input[key] === undefined ? missingFieldHint({ input }) : "";
|
|
131
|
+
return new ToolInputError({ message: `'${key}' (${kind}) is required${hint}` });
|
|
132
|
+
}
|
|
127
133
|
/**
|
|
128
|
-
* Reads a required non-empty
|
|
129
|
-
* @throws {ToolInputError} If the field is missing,
|
|
134
|
+
* Reads a required string, non-empty by default; allowEmpty supports literal text deletion.
|
|
135
|
+
* @throws {ToolInputError} If the field is missing, not a string, or empty without allowEmpty.
|
|
130
136
|
* @complexity O(k) for k input keys on a missing-field rejection; otherwise O(1).
|
|
131
137
|
*/
|
|
132
|
-
export function requireString(required,
|
|
138
|
+
export function requireString(required, { allowEmpty = false } = {}) {
|
|
133
139
|
const { input, key } = required;
|
|
134
140
|
const value = input[key];
|
|
135
|
-
if (typeof value !== "string" || value.length === 0)
|
|
136
|
-
|
|
137
|
-
if (value === undefined)
|
|
138
|
-
message += missingFieldHint({ input });
|
|
139
|
-
throw new ToolInputError({ message });
|
|
140
|
-
}
|
|
141
|
+
if (typeof value !== "string" || (!allowEmpty && value.length === 0))
|
|
142
|
+
throw requiredFieldError({ input, key, kind: allowEmpty ? "string" : "non-empty string" });
|
|
141
143
|
return value;
|
|
142
144
|
}
|
|
143
145
|
export function requireNumber(requiredArgs, optionalArgs = {}) {
|
|
144
146
|
const { input, key } = requiredArgs;
|
|
145
147
|
const value = input[key];
|
|
146
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
147
|
-
|
|
148
|
-
if (value === undefined)
|
|
149
|
-
message += missingFieldHint({ input });
|
|
150
|
-
throw new ToolInputError({ message });
|
|
151
|
-
}
|
|
148
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
149
|
+
throw requiredFieldError({ input, key, kind: "number" });
|
|
152
150
|
return value;
|
|
153
151
|
}
|
|
154
152
|
export function requireObject(requiredArgs, optionalArgs = {}) {
|
|
155
153
|
const { input, key } = requiredArgs;
|
|
156
154
|
const value = input[key];
|
|
157
155
|
const candidate = { value };
|
|
158
|
-
if (!isRecord(candidate) || Array.isArray(candidate.value))
|
|
159
|
-
|
|
160
|
-
if (value === undefined)
|
|
161
|
-
message += missingFieldHint({ input });
|
|
162
|
-
throw new ToolInputError({ message });
|
|
163
|
-
}
|
|
156
|
+
if (!isRecord(candidate) || Array.isArray(candidate.value))
|
|
157
|
+
throw requiredFieldError({ input, key, kind: "object" });
|
|
164
158
|
return candidate.value;
|
|
165
159
|
}
|
|
166
160
|
export function optionalString(required, _optional = {}) {
|
|
@@ -319,7 +313,7 @@ export function decorateWithSchema(params) {
|
|
|
319
313
|
// ---------------------------------------------------------------------------
|
|
320
314
|
/**
|
|
321
315
|
* Build-time gate on a single tool's risk metadata: refuses to wire a tool whose declared
|
|
322
|
-
* `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
|
|
316
|
+
* authorization permission is missing, whose `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
|
|
323
317
|
* whose id that layer has not classified at all, or whose `actorClassRule` needs a human confirmer
|
|
324
318
|
* its handler does not ask for.
|
|
325
319
|
*
|
|
@@ -339,17 +333,34 @@ export function decorateWithSchema(params) {
|
|
|
339
333
|
*/
|
|
340
334
|
export function assertToolIsWirable(params, optional = {}) {
|
|
341
335
|
const { toolId, catalogEntry } = params;
|
|
342
|
-
|
|
336
|
+
if (!hasDeclaredPermission({ catalogEntry })) {
|
|
337
|
+
throw new Error(`tool-registrations: '${toolId}' must declare an authorization permission before wiring`);
|
|
338
|
+
}
|
|
339
|
+
assertRiskIsCorroborated({ toolId, catalogEntry, derivedRisk: params.derivedRisk });
|
|
340
|
+
if (needsHumanConfirmer({ catalogEntry }) && !isHumanConfirmedHandler({ handler: optional.handler })) {
|
|
341
|
+
throw new Error(`tool-registrations: '${toolId}' declares actorClassRule '${catalogEntry.actorClassRule}', which needs a human confirmer — build its handler with humanConfirmedHandler so a human answers through the host's confirmation transport (see ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT)`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** A non-blank string permission; a malformed catalog (no authorization object) counts as none. */
|
|
345
|
+
function hasDeclaredPermission({ catalogEntry }) {
|
|
346
|
+
const permission = catalogEntry.authorization?.permission;
|
|
347
|
+
return typeof permission === "string" && permission.trim().length > 0;
|
|
348
|
+
}
|
|
349
|
+
/** Refuses an unclassified tool, then a declaration the wiring layer's own classification contradicts. */
|
|
350
|
+
function assertRiskIsCorroborated({ toolId, catalogEntry, derivedRisk }) {
|
|
351
|
+
const derived = derivedRisk.get(toolId);
|
|
343
352
|
if (!derived) {
|
|
344
353
|
throw new Error(`tool-registrations: '${toolId}' has no entry in DERIVED_RISK_BY_TOOL_ID — classify what its handler actually does before wiring it (unknown operations are refused, never assumed safe)`);
|
|
345
354
|
}
|
|
346
355
|
if (derived !== catalogEntry.sideEffects) {
|
|
347
356
|
throw new Error(`tool-registrations: '${toolId}' declares sideEffects '${catalogEntry.sideEffects}' but this layer derives '${derived}' from what its handler calls — reconcile the two rather than trusting the declaration`);
|
|
348
357
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
}
|
|
359
|
+
function needsHumanConfirmer({ catalogEntry }) {
|
|
360
|
+
return catalogEntry.actorClassRule !== undefined && ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT.has(catalogEntry.actorClassRule);
|
|
361
|
+
}
|
|
362
|
+
function isHumanConfirmedHandler({ handler }) {
|
|
363
|
+
return handler !== undefined && HUMAN_CONFIRMED_HANDLERS.has(handler);
|
|
353
364
|
}
|
|
354
365
|
/**
|
|
355
366
|
* Turns one domain's handler map into `ToolRegistration`s, applying every build-time gate the
|
|
@@ -388,48 +399,56 @@ export function assertToolIsWirable(params, optional = {}) {
|
|
|
388
399
|
* @overallScore 100
|
|
389
400
|
*/
|
|
390
401
|
export function buildDomainRegistrations(spec, optional = {}) {
|
|
391
|
-
const registrations = [];
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
id,
|
|
405
|
-
description: catalogEntry.description,
|
|
406
|
-
inputSchema: catalogEntry.inputSchema,
|
|
407
|
-
...(metadata ? { metadata } : {}),
|
|
408
|
-
// The ONE place a domain's declared risk becomes the descriptor flag every read-only gate
|
|
409
|
-
// reads (`@jini-ai/core`'s `isReadOnlyTool`). Placed here rather than in each domain's
|
|
410
|
-
// `tool-registrations.ts` for the same reason the drift tripwire is: twelve copies of this
|
|
411
|
-
// line is twelve chances for one to say something different. Change what counts as
|
|
412
|
-
// read-only and every domain follows.
|
|
413
|
-
//
|
|
414
|
-
// Safe to derive from the declaration only because `assertToolIsWirable` has ALREADY run
|
|
415
|
-
// two lines above and refused any tool whose declared `sideEffects` disagrees with the
|
|
416
|
-
// wiring layer's own `DERIVED_RISK_BY_TOOL_ID` classification — so by this point the
|
|
417
|
-
// declaration has been independently corroborated, and a catalog entry cannot make itself
|
|
418
|
-
// read-only by editing one word.
|
|
419
|
-
readOnly: catalogEntry.sideEffects === "none",
|
|
420
|
-
},
|
|
421
|
-
handler,
|
|
422
|
-
policy: { authorize: () => "allow" },
|
|
423
|
-
});
|
|
402
|
+
const registrations = Object.entries(spec.handlers).map(([id, handler]) => registrationFor({ spec, id, handler }));
|
|
403
|
+
assertEveryEntryIsWiredOrDeclared({ spec }, optional);
|
|
404
|
+
return registrations;
|
|
405
|
+
}
|
|
406
|
+
/** One wired tool: every build-time gate first, then the descriptor/handler/policy triple. */
|
|
407
|
+
function registrationFor({ spec, id, handler }) {
|
|
408
|
+
const catalogEntry = spec.catalog.get(id);
|
|
409
|
+
if (!catalogEntry) {
|
|
410
|
+
throw new Error(`tool-registrations: ${spec.domain} catalog has no entry named '${id}' — ${spec.catalogModule} drifted`);
|
|
411
|
+
}
|
|
412
|
+
assertToolIsWirable({ toolId: id, catalogEntry, derivedRisk: spec.derivedRisk }, { handler });
|
|
413
|
+
if (!catalogEntry.inputSchema) {
|
|
414
|
+
throw new Error(`tool-registrations: wired tool '${id}' publishes no inputSchema — add one to its entry in ${spec.catalogModule} so the model gets a contract, or leave the tool unwired`);
|
|
424
415
|
}
|
|
416
|
+
const metadata = catalogEntry.metadata ?? toolMetadataFor({ toolId: id, metadata: spec.metadata });
|
|
417
|
+
const normalizeInput = spec.inputNormalizers?.[id];
|
|
418
|
+
return {
|
|
419
|
+
descriptor: {
|
|
420
|
+
id,
|
|
421
|
+
description: catalogEntry.description,
|
|
422
|
+
inputSchema: catalogEntry.inputSchema,
|
|
423
|
+
...(metadata ? { metadata } : {}),
|
|
424
|
+
// The ONE place a domain's declared risk becomes the descriptor flag every read-only gate
|
|
425
|
+
// reads (`@jini-ai/core`'s `isReadOnlyTool`). Placed here rather than in each domain's
|
|
426
|
+
// `tool-registrations.ts` for the same reason the drift tripwire is: twelve copies of this
|
|
427
|
+
// line is twelve chances for one to say something different. Change what counts as
|
|
428
|
+
// read-only and every domain follows.
|
|
429
|
+
//
|
|
430
|
+
// Safe to derive from the declaration only because `assertToolIsWirable` has ALREADY run
|
|
431
|
+
// two lines above and refused any tool whose declared `sideEffects` disagrees with the
|
|
432
|
+
// wiring layer's own `DERIVED_RISK_BY_TOOL_ID` classification — so by this point the
|
|
433
|
+
// declaration has been independently corroborated, and a catalog entry cannot make itself
|
|
434
|
+
// read-only by editing one word.
|
|
435
|
+
readOnly: catalogEntry.sideEffects === "none",
|
|
436
|
+
sideEffects: catalogEntry.sideEffects,
|
|
437
|
+
},
|
|
438
|
+
handler,
|
|
439
|
+
...(normalizeInput ? { normalizeInput } : {}),
|
|
440
|
+
policy: { authorize: () => "allow" },
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/** The tripwire: a catalog entry is either wired or recorded as deliberately unwired, never silently neither. */
|
|
444
|
+
function assertEveryEntryIsWiredOrDeclared({ spec }, { unwiredToolIds } = {}) {
|
|
425
445
|
for (const id of spec.catalog.keys()) {
|
|
426
446
|
if (id in spec.handlers)
|
|
427
447
|
continue;
|
|
428
|
-
if (
|
|
448
|
+
if (unwiredToolIds?.has(id))
|
|
429
449
|
continue;
|
|
430
450
|
throw new Error(`tool-registrations: ${spec.domain} catalog entry '${id}' is neither wired nor declared unwired — wire a handler for it, or add it to that domain's unwired set with the reason recorded next to it`);
|
|
431
451
|
}
|
|
432
|
-
return registrations;
|
|
433
452
|
}
|
|
434
453
|
/**
|
|
435
454
|
* Folds every domain's risk slice into the one map the assistant-level
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ToolExecutionContext, type ToolHandler } from "../tool-registry.js";
|
|
2
2
|
import type { JsonValue } from "../primitives/index.js";
|
|
3
|
+
import { type AgentSettingWriteRule } from "./agent-write-denylist.js";
|
|
3
4
|
import type { SettingsToolDeps } from "./tool-registrations.js";
|
|
5
|
+
import { type SettingsWriteServiceDeps } from "./write-service.js";
|
|
4
6
|
export type SettingsValueWriteToolId = "settings_set_value" | "settings_clear_value";
|
|
5
7
|
/** Host-only port. This request is never an agent input and only the host's authenticated
|
|
6
8
|
* human transport or a persisted human identity grant may answer it. Model input never authorizes it. */
|
|
@@ -14,6 +16,23 @@ export interface SettingsWriteConfirmation {
|
|
|
14
16
|
value?: JsonValue;
|
|
15
17
|
reason: string;
|
|
16
18
|
}
|
|
19
|
+
/** The effective agent-write key policy: package denials plus host extensions (never agent input),
|
|
20
|
+
* and the host's confirmation policy (replacing the package defaults) plus host extensions. O(r). */
|
|
21
|
+
export declare function agentWriteRules(deps: SettingsToolDeps): {
|
|
22
|
+
denied: AgentSettingWriteRule[];
|
|
23
|
+
confirmation: AgentSettingWriteRule[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Builds `write-service.ts`'s own five-field dep shape from `RouteDeps`.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately duplicates a host's own HTTP admin layer's equivalent field-mapping helper rather
|
|
29
|
+
* than importing it, for the reason `tool-registrations.ts`'s header already gives about
|
|
30
|
+
* `resolveOwnOrOtherPrincipalRead`: this is domain-owned wiring, and importing from a host's HTTP
|
|
31
|
+
* admin layer would invert the ports/adapters direction. It is a field mapping, not logic, so the
|
|
32
|
+
* two copies carry no behavioral drift risk — and `write-service.ts` is the single chokepoint
|
|
33
|
+
* either way, so a divergence here could only produce a missing dependency, which fails loudly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function toWriteDeps(routeDeps: SettingsToolDeps): SettingsWriteServiceDeps;
|
|
17
36
|
/** Builds one value-write handler. Effects: authorize -> raw read -> optional human card ->
|
|
18
37
|
* service write -> result. The service rechecks authorization and performs its normal transaction.
|
|
19
38
|
* @complexity O(value size + rule count) plus bounded repository calls and a human wait.
|