@jini-ai/core 0.6.10 → 0.6.11

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @jini-ai/core
2
2
 
3
+ ## 0.6.11
4
+
5
+ ### Patch Changes
6
+
7
+ - OAuth accepts a parent protected-resource identifier and forwards the refresh resource; settings definitions are filtered; scope selectors are never id shorthand; secret-named URL parameters are masked by default in redaction.
8
+ - Tool inputs apply a sole near-miss and declared input aliases; plugin-load outcomes are reported; `web_fetch` reads SVG/JS as text; `content_read` suggests its resource first; the browser falls back to system Chrome.
9
+
3
10
  ## 0.6.10
4
11
 
5
12
  ### Patch Changes
package/dist/redact.js CHANGED
@@ -181,7 +181,8 @@ function isLuhnValid(digits) {
181
181
  // correlation ID uses uppercase hex and is meant to reach the model and be quoted to an
182
182
  // operator; delegated-tool-calls is the route segment named in delegated tool failures.
183
183
  // Matching the ENTIRE run keeps a secret glued onto either spelling redactable.
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;
184
+ const SECRET_LABELS = 'authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie';
185
+ export const LABELED_SECRET_RE = new RegExp(`\\b(${SECRET_LABELS})\\b\\s*[:=]\\s*\\S+`, 'gi');
185
186
  export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
186
187
  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]+)+)$/;
187
188
  // Runtime markers must survive a second pass; the bounded sanitizer deliberately retains
@@ -198,6 +199,15 @@ const PROVIDER_API_KEY_QUERY = /([?&]key=)([^&\s]+)/gi;
198
199
  // readable for debugging without exposing credentials that authorize access to the resource.
199
200
  const URL_CREDENTIALS_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/\s?#@]+)@/gi;
200
201
  const SIGNED_QUERY_RE = /([?&](?:signature|sig|x-amz-signature|x-goog-signature|access_token|refresh_token)=)([^&#\s,;"']+)/gi;
202
+ // A secret word must END a name segment (split at . _ - or the name's end): api_key, apiKey,
203
+ // authtoken, X-Amz-Signature and custom.Token-value are masked, while author, keyword, design,
204
+ // signup and page stay readable. Names that merely end in a secret word (zipcode, error_code,
205
+ // monkey) are masked too; over-masking a URL value is the cheaper failure. Empty values stay.
206
+ // Match through a trailing fragment; only '&', quotes, parentheses or whitespace end the value.
207
+ // Both name runs are bounded so a long separator-free run costs linear time, not quadratic.
208
+ const URL_SECRET_PARAM_RE = /[?&#;]([\w.-]{0,128}?(?:token|key|secret|signature|sig|code|password|passwd|pwd|auth|authorization|credential)s?(?=[._=-])[\w.-]{0,128})=[^&\s"'<>()]+/gi;
209
+ // A parameter the labeled rule already names (password, client_secret, ...) keeps that category.
210
+ const LABELED_URL_PARAM_NAME_RE = new RegExp(`(?:^|[.-])(?:${SECRET_LABELS})$`, 'i');
201
211
  // Only an entire marker is already masked. A secret glued onto marker-like text still needs
202
212
  // masking; a substring check would preserve credential suffixes in echoed header/query values.
203
213
  function isMaskedValue(value) {
@@ -301,6 +311,9 @@ function redactKeyedCredentials({ text, marker }) {
301
311
  out = out.replace(API_KEY_QUERY, (match, prefix) => isMaskedValue(match.slice(prefix.length)) ? match : `${prefix}${marker('api_key_query')}`);
302
312
  out = out.replace(SHORT_BEARER_RE, () => marker('bearer_token'));
303
313
  out = out.replace(PROVIDER_API_KEY_HEADER, (match, name, value) => isMaskedValue(value) ? match : `${name}: ${marker('api_key_header')}`);
314
+ // Sweep other URL parameters before the general labeled rule can consume harmless
315
+ // neighboring parameters (`password=x&page=2` used to lose `&page=2`).
316
+ out = redactUrlSecretParams({ text: out, marker });
304
317
  return out.replace(MASK_AWARE_LABELED_SECRET_RE, (match, _label, value) => {
305
318
  // Replace only the captured value, retaining labels even when no whitespace separates
306
319
  // them. Glued credential suffixes fail the complete-marker check and remain maskable.
@@ -309,6 +322,16 @@ function redactKeyedCredentials({ text, marker }) {
309
322
  return `${match.slice(0, -value.length)}${marker('labeled_secret')}`;
310
323
  });
311
324
  }
325
+ /** Secret-named URL query/fragment values; values already masked above keep their category. */
326
+ function redactUrlSecretParams({ text, marker }) {
327
+ return text.replace(URL_SECRET_PARAM_RE, (match, name) => {
328
+ const separator = match.indexOf('=') + 1;
329
+ if (isMaskedValue(match.slice(separator)))
330
+ return match;
331
+ const category = LABELED_URL_PARAM_NAME_RE.test(name) ? 'labeled_secret' : 'url_secret_param';
332
+ return `${match.slice(0, separator)}${marker(category)}`;
333
+ });
334
+ }
312
335
  /** Email/IPv4/phone rules, then Luhn-checked card candidates. */
313
336
  function redactPii({ text, marker }) {
314
337
  return applyPatterns({ text, marker, patterns: PII_PATTERNS }).replace(CARD_CANDIDATE, (match) => {
@@ -49,13 +49,16 @@ import { AGENT_WRITABLE_PREFERENCE_IDS, AGENT_WRITABLE_PREFERENCES } from "./age
49
49
  * write entries this file documents but never wires (see file header) carry no schema at all,
50
50
  * since one is never published for a tool the model never sees.
51
51
  *
52
- * No arguments — `settings_list_definitions` takes none; it always enumerates the caller's own
53
- * (boot-wired) workspace's site-owned partition plus the shared platform partition. */
54
- const NO_INPUT_SCHEMA = {
52
+ * Optional filters narrow the caller's own (boot-wired) workspace's site-owned partition plus
53
+ * the shared platform partition; they never select a different workspace. */
54
+ const LIST_DEFINITIONS_SCHEMA = {
55
55
  type: "object",
56
56
  additionalProperties: false,
57
57
  required: [],
58
- properties: {},
58
+ properties: {
59
+ search: { type: "string", description: "Case-insensitive text filter over the full 'namespace.key' name." },
60
+ namespace: { type: "string", description: "Only definitions in this exact namespace." },
61
+ },
59
62
  };
60
63
  /** Shared by `settings_get_effective`/`settings_get_raw`: naming a DIFFERENT principal than the
61
64
  * caller requires `settings.user.read` (checked by the handler, not expressed in this schema). */
@@ -138,10 +141,10 @@ export function getSettingsAgentToolCatalog() {
138
141
  return [
139
142
  {
140
143
  name: "settings_list_definitions",
141
- description: "Lists every active setting definition visible to this workspace (platform core/theme definitions plus this workspace's own site-owned definitions): namespace, key, owner kind, scope bitmask, status, version, schema, agentWritable and confirmationRequired. Use before writing a setting. Metadata only — never a value.",
144
+ description: "Lists active setting definitions visible to this workspace (platform core/theme definitions plus this workspace's own site-owned definitions), optionally filtered by search text and exact namespace: namespace, key, owner kind, scope bitmask, status, version, schema, agentWritable and confirmationRequired. Use before writing a setting. Metadata only — never a value.",
142
145
  sideEffects: "none",
143
146
  authorization: { permission: "settings.read.definitions" },
144
- inputSchema: NO_INPUT_SCHEMA,
147
+ inputSchema: LIST_DEFINITIONS_SCHEMA,
145
148
  },
146
149
  {
147
150
  name: "settings_get_effective",
@@ -1,7 +1,7 @@
1
1
  import { adaptLegacyAuthorize } from "../tool-permissions.js";
2
2
  import { agentWriteRules, buildSettingsValueWriteHandler, toWriteDeps } from "./agent-value-write-tools.js";
3
3
  import { findAgentWriteRule } from "./agent-write-denylist.js";
4
- import { buildDomainRegistrations, indexCatalogById, optionalString, requireInputRecord, requireNoInput, requireString, withSchemaOnRejection } from "../registration-kit.js";
4
+ import { buildDomainRegistrations, indexCatalogById, optionalString, requireInputRecord, requireString, withSchemaOnRejection } from "../registration-kit.js";
5
5
  import { ToolInputError } from "../tool-registry.js";
6
6
  import { requireToolPermission } from "../tool-permissions.js";
7
7
  import { getSettingsAgentToolCatalog } from "./agent-tools.js";
@@ -140,15 +140,21 @@ export function buildSettingsRegistrations(routeDeps) {
140
140
  settings_set_value: buildSettingsValueWriteHandler(routeDeps, "settings_set_value"),
141
141
  settings_clear_value: buildSettingsValueWriteHandler(routeDeps, "settings_clear_value"),
142
142
  settings_list_definitions: async (ctx) => {
143
- requireNoInput({ input: ctx.input });
143
+ const input = requireInputRecord({ input: ctx.input === undefined ? {} : ctx.input });
144
+ const search = optionalString({ input, key: "search" })?.toLowerCase();
145
+ const namespace = optionalString({ input, key: "namespace" });
144
146
  await routeDeps.settingsReady;
145
147
  await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: ctx.principal.id, permission: "settings.read.definitions" }, { entityType: "setting-definition" });
146
148
  const [platformDefs, siteDefs] = await Promise.all([
147
149
  routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: null }),
148
150
  routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: routeDeps.workspaceId }),
149
151
  ]);
152
+ // Filter only authorized workspace rows, before alias lookups; never search stored values.
153
+ const definitions = [...platformDefs, ...siteDefs]
154
+ .filter((def) => namespace === undefined || def.namespace === namespace)
155
+ .filter((def) => !search || `${def.namespace}.${def.key}`.toLowerCase().includes(search));
150
156
  const rules = agentWriteRules(routeDeps);
151
- const data = (await Promise.all([...platformDefs, ...siteDefs].map((def) => describeDefinitionForAgent(routeDeps, rules, def))))
157
+ const data = (await Promise.all(definitions.map((def) => describeDefinitionForAgent(routeDeps, rules, def))))
152
158
  .sort((a, b) => a.namespace.localeCompare(b.namespace) || a.key.localeCompare(b.key));
153
159
  return { data };
154
160
  },
@@ -8,8 +8,8 @@ export interface NormalizedToolInput {
8
8
  export type ToolInputNormalizer = (required: {
9
9
  input: Readonly<Record<string, unknown>>;
10
10
  }, optional?: Record<string, never>) => NormalizedToolInput | Promise<NormalizedToolInput>;
11
- /** Canonicalize one batch layer before merging; preserve values and domain aliases for the
12
- * final normalization. Throws ToolInputError for collisions within this layer, never across
11
+ /** Canonicalize one batch layer (including declared aliases) before merging; leave value conversion
12
+ * and domain hooks for final normalization. Throws ToolInputError for collisions within this layer, never across
13
13
  * layers. Uses the same bounded schema walk as normalizeToolInput.
14
14
  * @example canonicalizeToolInputKeys({ input: { max_chars: '1000' }, schema });
15
15
  */
@@ -20,7 +20,8 @@ export declare function canonicalizeToolInputKeys({ input, schema }: {
20
20
  input: Record<string, unknown>;
21
21
  notes: readonly string[];
22
22
  };
23
- /** Normalize before policy, confirmation and handler validation. Domain hooks own semantic aliases.
23
+ /** Normalize before policy, confirmation and handler validation. Schema properties may declare
24
+ * `aliases: ['search', 'query']`; domain hooks own transformations beyond declared key aliases.
24
25
  * Never changes the caller's object or includes values in notes. Invalid/ambiguous input remains
25
26
  * a ToolInputError; ordinary domain/schema validation still belongs to the registered handler.
26
27
  * @complexity Two bounded schema walks plus the optional domain hook's cost.
@@ -15,11 +15,16 @@ function sameLengthNearMiss({ left, right, index }) {
15
15
  return left.slice(index + 1) === right.slice(index + 1)
16
16
  || (left[index] === right[index + 1] && left[index + 1] === right[index] && left.slice(index + 2) === right.slice(index + 2));
17
17
  }
18
- /** Whether either key is the generic id shorthand for the other's id suffix; hints only. */
18
+ /** Scope selectors name a container, never the resource: guessing `workspaceId` as `id` (or the
19
+ * reverse) would silently drop or invent a scope. Only a declared name or alias may accept one. */
20
+ const SCOPE_SELECTORS = new Set(['workspaceid', 'siteid']);
21
+ /** Whether either key is the generic id shorthand for the other's id suffix; never for a scope selector. */
19
22
  function isIdShorthand({ left, right }, _options = {}) {
23
+ if (SCOPE_SELECTORS.has(left) || SCOPE_SELECTORS.has(right))
24
+ return false;
20
25
  return (left === 'id' && right.endsWith('id')) || (right === 'id' && left.endsWith('id'));
21
26
  }
22
- /** Detect id shorthand or one insertion, deletion, substitution or transposition, for hints only. */
27
+ /** Detect id shorthand or one insertion, deletion, substitution or transposition. */
23
28
  function nearMiss({ left, right }, _options = {}) {
24
29
  if (left.length > 128 || right.length > 128)
25
30
  return false;
@@ -53,6 +58,19 @@ function coerceString({ value, types, path, notes }) {
53
58
  }
54
59
  return coerceNumber({ value, types, path, notes });
55
60
  }
61
+ /** Unwrap only a singleton string array; refuse malformed arrays without exposing values.
62
+ * Keys-only batch layers preserve values until the final merged input is normalized.
63
+ * @complexity O(t) time for t allowed schema types, O(1) space; array contents are not scanned.
64
+ */
65
+ function unwrapStringArray({ value, types, step }) {
66
+ if (step.walk.keysOnly || !Array.isArray(value) || !types.includes('string'))
67
+ return value;
68
+ if (value.length !== 1 || typeof value[0] !== 'string') {
69
+ throw new ToolInputError({ message: `${step.path}: send one value as a string or a one-element string array.` });
70
+ }
71
+ step.walk.notes.push(`${step.path}: one-item string array→string`);
72
+ return value[0];
73
+ }
56
74
  function normalizeArray({ value, schema, step }) {
57
75
  const { path, depth, walk } = step;
58
76
  let list = value;
@@ -65,12 +83,30 @@ function normalizeArray({ value, schema, step }) {
65
83
  return list;
66
84
  return list.map((item, index) => normalizeValue({ value: item, schema: schema.items, step: { path: `${path}[${index}]`, depth: depth + 1, walk } }));
67
85
  }
68
- /** The declared property a supplied key names, or undefined; refuses a key that names two. */
86
+ /** Match a canonical spelling or its property's declared string aliases, ignoring case/separators.
87
+ * Malformed alias annotations confer no alternate names; exact property names still win.
88
+ * @complexity O(a) comparisons for a aliases on this property; no input mutation or effects.
89
+ */
90
+ function matchesDeclaredKey({ key, name, schema }, _options = {}) {
91
+ if (keyShape(name) === keyShape(key))
92
+ return true;
93
+ if (!object(schema) || !Array.isArray(schema.aliases))
94
+ return false;
95
+ return schema.aliases.some(alias => typeof alias === 'string' && keyShape(alias) === keyShape(key));
96
+ }
97
+ /** Exact names, then case/separator variants and declared aliases, then a sole hint; ambiguous hints stay unresolved
98
+ * for the domain hook or closed-schema refusal. Never choose one of several candidates.
99
+ * Hints apply only where a closed schema would refuse the key: an open schema accepts it as data.
100
+ * @complexity O(p + a) comparisons for p properties and a declared aliases; O(p) space.
101
+ */
69
102
  function canonicalKey({ key, location, shape }) {
70
- const candidates = Object.hasOwn(shape.properties, key) ? [key] : shape.names.filter(name => keyShape(name) === keyShape(key));
103
+ const candidates = Object.hasOwn(shape.properties, key) ? [key] : shape.names.filter(name => matchesDeclaredKey({ key, name, schema: shape.properties[name] }));
71
104
  if (candidates.length > 1)
72
105
  throw new ToolInputError({ message: `Ambiguous input '${location}': matches ${candidates.join(', ')}. Use exactly one canonical property name.` });
73
- return candidates[0];
106
+ if (candidates.length === 1 || shape.schema.additionalProperties !== false)
107
+ return candidates[0];
108
+ const hints = shape.names.filter(name => nearMiss({ left: keyShape(key), right: keyShape(name) }));
109
+ return hints.length === 1 ? hints[0] : undefined;
74
110
  }
75
111
  /** Describe the local schema and likely names without choosing a name or exposing input values.
76
112
  * Target and permission fields must be declared in the tool schema before callers can use them.
@@ -111,7 +147,7 @@ function normalizeObject({ value, schema, step }) {
111
147
  return Object.fromEntries(Object.entries(value).map(([key, raw]) => normalizeEntry({ key, raw, shape, sources, step })));
112
148
  }
113
149
  /** Schema-guided copies only: unconstrained data and unsupported schema dialects stay untouched.
114
- * Cost is O(k*p) per object with k supplied keys and p schema properties; recursion is bounded
150
+ * Cost is O(k*(p+a)) comparisons per object with k supplied keys, p properties and a aliases; recursion is bounded
115
151
  * to 64 levels so a hostile input/schema cannot exhaust the execution gate's stack.
116
152
  */
117
153
  function normalizeValue({ value, schema, step }) {
@@ -126,14 +162,15 @@ function normalizeValue({ value, schema, step }) {
126
162
  }
127
163
  if (types.includes('array'))
128
164
  return normalizeArray({ value, schema, step });
129
- return object(value) ? normalizeObject({ value, schema, step }) : value;
165
+ const unwrapped = unwrapStringArray({ value, types, step });
166
+ return object(unwrapped) ? normalizeObject({ value: unwrapped, schema, step }) : unwrapped;
130
167
  }
131
168
  /** A walk from the root of one input. */
132
169
  function walkRoot({ input, schema, notes, validateClosed, keysOnly }) {
133
170
  return normalizeValue({ value: input, schema, step: { path: '', depth: 0, walk: { notes, validateClosed, keysOnly } } });
134
171
  }
135
- /** Canonicalize one batch layer before merging; preserve values and domain aliases for the
136
- * final normalization. Throws ToolInputError for collisions within this layer, never across
172
+ /** Canonicalize one batch layer (including declared aliases) before merging; leave value conversion
173
+ * and domain hooks for final normalization. Throws ToolInputError for collisions within this layer, never across
137
174
  * layers. Uses the same bounded schema walk as normalizeToolInput.
138
175
  * @example canonicalizeToolInputKeys({ input: { max_chars: '1000' }, schema });
139
176
  */
@@ -141,7 +178,8 @@ export function canonicalizeToolInputKeys({ input, schema }, _options = {}) {
141
178
  const notes = [];
142
179
  return { input: walkRoot({ input, schema, notes, validateClosed: false, keysOnly: true }), notes };
143
180
  }
144
- /** Normalize before policy, confirmation and handler validation. Domain hooks own semantic aliases.
181
+ /** Normalize before policy, confirmation and handler validation. Schema properties may declare
182
+ * `aliases: ['search', 'query']`; domain hooks own transformations beyond declared key aliases.
145
183
  * Never changes the caller's object or includes values in notes. Invalid/ambiguous input remains
146
184
  * a ToolInputError; ordinary domain/schema validation still belongs to the registered handler.
147
185
  * @complexity Two bounded schema walks plus the optional domain hook's cost.
@@ -46,6 +46,9 @@ export interface ToolDescriptor {
46
46
  * JSON-Schema-shaped description of this tool's accepted input, carried as `unknown` because the
47
47
  * gate reads common property/type fields for input normalization but leaves validation and
48
48
  * schema dialects to the handler; no validator dependency belongs in every registry consumer.
49
+ * Each property may declare `aliases: readonly string[]` (e.g. contains: { type: 'string',
50
+ * aliases: ['search', 'query'] }). The gate treats these like case variants, rejects ambiguous
51
+ * or conflicting names, and reports the rename in normalization notes.
49
52
  *
50
53
  * It exists because *every* discovery mechanism needs it and nothing else in the kernel can
51
54
  * supply it: a caller that can enumerate `ToolRegistry.list({})` still cannot tell an agent what
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/core",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
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": {