@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/README.md +5 -0
  3. package/dist/agent-tools.d.ts +1 -1
  4. package/dist/approval.d.ts +15 -0
  5. package/dist/approval.js +87 -22
  6. package/dist/batch-tool-inputs.d.ts +147 -0
  7. package/dist/batch-tool-inputs.js +257 -0
  8. package/dist/feature-flags.d.ts +21 -0
  9. package/dist/feature-flags.js +17 -0
  10. package/dist/fetch/index.d.ts +2 -1
  11. package/dist/fetch/index.js +1 -0
  12. package/dist/fetch/loader-read.d.ts +16 -0
  13. package/dist/fetch/loader-read.js +22 -0
  14. package/dist/fetch/types.d.ts +12 -2
  15. package/dist/gated-mutations/gateway.js +32 -27
  16. package/dist/index.d.ts +8 -1
  17. package/dist/index.js +7 -1
  18. package/dist/object-store.d.ts +115 -0
  19. package/dist/object-store.js +70 -0
  20. package/dist/origin-validation.js +64 -57
  21. package/dist/primitives/http.d.ts +2 -2
  22. package/dist/primitives/index.d.ts +1 -0
  23. package/dist/primitives/index.js +1 -0
  24. package/dist/primitives/paths.js +17 -10
  25. package/dist/primitives/tasks.d.ts +10 -0
  26. package/dist/primitives/tasks.js +8 -0
  27. package/dist/provider-catalog.d.ts +76 -0
  28. package/dist/provider-catalog.js +6 -0
  29. package/dist/provider-registry.d.ts +144 -0
  30. package/dist/provider-registry.js +138 -0
  31. package/dist/records.d.ts +24 -0
  32. package/dist/records.js +25 -0
  33. package/dist/redact.d.ts +32 -0
  34. package/dist/redact.js +91 -34
  35. package/dist/registration-kit.d.ts +7 -4
  36. package/dist/registration-kit.js +81 -62
  37. package/dist/settings/agent-value-write-tools.d.ts +19 -0
  38. package/dist/settings/agent-value-write-tools.js +131 -70
  39. package/dist/settings/ensure-definitions.d.ts +4 -2
  40. package/dist/settings/ensure-definitions.js +79 -63
  41. package/dist/settings/errors.d.ts +13 -0
  42. package/dist/settings/errors.js +15 -0
  43. package/dist/settings/express/cms-adapter.d.ts +2 -2
  44. package/dist/settings/express/cms-adapter.js +34 -22
  45. package/dist/settings/express/http-request.d.ts +33 -0
  46. package/dist/settings/express/http-request.js +49 -0
  47. package/dist/settings/express/http.d.ts +2 -2
  48. package/dist/settings/express/http.js +16 -39
  49. package/dist/settings/express/stream.d.ts +2 -2
  50. package/dist/settings/express/stream.js +49 -27
  51. package/dist/settings/index.d.ts +2 -2
  52. package/dist/settings/index.js +1 -1
  53. package/dist/settings/ports.d.ts +7 -1
  54. package/dist/settings/repo.memory.js +7 -4
  55. package/dist/settings/settings.js +54 -60
  56. package/dist/settings/tool-registrations.d.ts +2 -3
  57. package/dist/settings/tool-registrations.js +64 -39
  58. package/dist/settings/write-service.d.ts +42 -4
  59. package/dist/settings/write-service.js +72 -5
  60. package/dist/target-scope.d.ts +2 -0
  61. package/dist/target-scope.js +18 -2
  62. package/dist/text/escape.js +2 -2
  63. package/dist/text/sanitize-untrusted-text.d.ts +5 -0
  64. package/dist/text/sanitize-untrusted-text.js +55 -35
  65. package/dist/tool-exposure.d.ts +79 -0
  66. package/dist/tool-exposure.js +163 -0
  67. package/dist/tool-input-errors.d.ts +23 -0
  68. package/dist/tool-input-errors.js +24 -0
  69. package/dist/tool-input-normalization.d.ts +37 -0
  70. package/dist/tool-input-normalization.js +188 -0
  71. package/dist/tool-metadata-types.d.ts +26 -0
  72. package/dist/tool-metadata-types.js +2 -0
  73. package/dist/tool-metadata.d.ts +2 -23
  74. package/dist/tool-registry.d.ts +38 -22
  75. package/dist/tool-registry.js +35 -20
  76. package/package.json +20 -2
  77. package/plugin-directory.js +72 -33
@@ -0,0 +1,163 @@
1
+ const SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
2
+ const MAX_NAME_LENGTH = 64;
3
+ /** A stable, non-security hash keeps transformed names independent of discovery order.
4
+ * Collisions are still checked against the entire namespace, never trusted as unique. */
5
+ function nameHash(id) {
6
+ let hash = 2166136261;
7
+ for (let i = 0; i < id.length; i++)
8
+ hash = Math.imul(hash ^ id.charCodeAt(i), 16777619);
9
+ return (hash >>> 0).toString(16).padStart(8, '0');
10
+ }
11
+ function nameBase(id) {
12
+ if (SAFE_NAME.test(id))
13
+ return id;
14
+ const readable = id.replace(/[^A-Za-z0-9_]/g, '_');
15
+ return `tool_${readable.slice(0, 49)}_${nameHash(id)}`;
16
+ }
17
+ /** O(c) collision probes for c occupied alternatives; suffix space stays inside the wire limit. */
18
+ function assignName(index, id) {
19
+ if (index.forward.has(id))
20
+ return;
21
+ const base = nameBase(id);
22
+ let candidate = base;
23
+ let counter = 1;
24
+ while (index.occupied.has(candidate)) {
25
+ const suffix = `_${counter++}`;
26
+ candidate = `${base.slice(0, MAX_NAME_LENGTH - suffix.length)}${suffix}`;
27
+ }
28
+ index.forward.set(id, candidate);
29
+ index.reverse.set(candidate, id);
30
+ index.occupied.add(candidate);
31
+ }
32
+ /** Reserve native spellings first so a transformed ID cannot steal an existing safe name. */
33
+ function indexNames(index, toolIds) {
34
+ const sorted = [...new Set(toolIds)].sort();
35
+ for (const id of sorted)
36
+ if (SAFE_NAME.test(id))
37
+ assignName(index, id);
38
+ for (const id of sorted)
39
+ if (!SAFE_NAME.test(id))
40
+ assignName(index, id);
41
+ }
42
+ /**
43
+ * Build a deterministic, collision-checked mapping to conservative 64-character wire names.
44
+ * @param required - Registry IDs; duplicates share one mapping.
45
+ * @param optional - Names already occupied by transport-owned tools.
46
+ * @returns Read-only forward/reverse lookups, with no descriptors or execution authority.
47
+ * @complexity O(n log n + l + c) time, O(n + l) space for n IDs, l characters and c collision probes.
48
+ */
49
+ export function createToolNameMap({ toolIds }, { reservedNames = [] } = {}) {
50
+ const index = { forward: new Map(), reverse: new Map(), occupied: new Set(reservedNames) };
51
+ indexNames(index, toolIds);
52
+ return Object.freeze({
53
+ wireNameFor: ({ toolId }, _optional = {}) => index.forward.get(toolId),
54
+ toolIdFor: ({ wireName }, _optional = {}) => index.reverse.get(wireName),
55
+ });
56
+ }
57
+ export class ToolExposureError extends Error {
58
+ code;
59
+ constructor({ code }, options = {}) {
60
+ super(`ToolExposure: ${code}`, options);
61
+ this.name = 'ToolExposureError';
62
+ this.code = code;
63
+ }
64
+ }
65
+ function assertLimit(value) {
66
+ if (!Number.isSafeInteger(value) || value < 0)
67
+ throw new ToolExposureError({ code: 'invalid-limit' });
68
+ }
69
+ /** Freeze detached data only; registry-owned descriptors are never frozen or mutated. */
70
+ function freezeData(value, seen = new Set()) {
71
+ if (value === null || typeof value !== 'object' || Object.isFrozen(value) || seen.has(value))
72
+ return;
73
+ seen.add(value);
74
+ for (const child of Object.values(value))
75
+ freezeData(child, seen);
76
+ Object.freeze(value);
77
+ }
78
+ /** Clone and measure before publication. Invalid data cannot partially change exposure state. */
79
+ function pinDescriptor(descriptor) {
80
+ try {
81
+ const pinned = structuredClone(descriptor);
82
+ const json = JSON.stringify(pinned.inputSchema);
83
+ const bytes = json === undefined ? 0 : new TextEncoder().encode(json).byteLength;
84
+ freezeData(pinned);
85
+ return { descriptor: pinned, bytes };
86
+ }
87
+ catch (cause) {
88
+ throw new ToolExposureError({ code: 'invalid-descriptor' }, { cause });
89
+ }
90
+ }
91
+ /** Count and byte caps reject new entries without evicting any previously callable definition. */
92
+ function limitReason(snapshot, bytes, limits) {
93
+ if (snapshot.tools.length >= limits.maxTools)
94
+ return 'tool-limit';
95
+ if (snapshot.schemaBytes + bytes > limits.maxSchemaBytes)
96
+ return 'schema-byte-limit';
97
+ return undefined;
98
+ }
99
+ /** Stage all changes locally so malformed descriptors leave the last published revision intact. */
100
+ function promoteDescriptors(snapshot, toolIds, descriptors, names, limits) {
101
+ const available = new Map(descriptors.map(descriptor => [descriptor.id, descriptor]));
102
+ const published = new Set(snapshot.tools.map(tool => tool.descriptor.id));
103
+ const tools = [...snapshot.tools];
104
+ const added = [];
105
+ const skipped = [];
106
+ let schemaBytes = snapshot.schemaBytes;
107
+ for (const toolId of new Set(toolIds)) {
108
+ if (published.has(toolId))
109
+ continue;
110
+ const descriptor = available.get(toolId);
111
+ if (!descriptor) {
112
+ skipped.push({ toolId, reason: 'not-found' });
113
+ continue;
114
+ }
115
+ const pinned = pinDescriptor(descriptor);
116
+ const reason = limitReason({ tools, schemaBytes, revision: snapshot.revision }, pinned.bytes, limits);
117
+ if (reason) {
118
+ skipped.push({ toolId, reason });
119
+ continue;
120
+ }
121
+ assignName(names, toolId);
122
+ tools.push({ wireName: names.forward.get(toolId), descriptor: pinned.descriptor });
123
+ schemaBytes += pinned.bytes;
124
+ added.push(toolId);
125
+ }
126
+ let next = snapshot;
127
+ if (added.length > 0)
128
+ next = { revision: snapshot.revision + 1, tools, schemaBytes };
129
+ const result = { snapshot: next, added, skipped };
130
+ freezeData(result);
131
+ return result;
132
+ }
133
+ /**
134
+ * Create one run's bounded, append-only descriptor exposure over the existing registry.
135
+ * @param required - Descriptor source; a filtered registry view may enforce host visibility.
136
+ * @param optional - Count/schema-byte budgets and transport-owned reserved wire names.
137
+ * @returns Immutable snapshots and promotion deltas; no handlers, policies or execution path.
138
+ * @throws ToolExposureError for invalid limits or non-cloneable/non-serializable descriptors.
139
+ * @complexity Promotion O(r log r + s + b + c) time and O(r + s + b) space for r registry IDs,
140
+ * s exposed entries, b promoted descriptor bytes and c name collision probes. No I/O.
141
+ * @remarks Scope mutation is synchronous, so concurrent promise continuations cannot lose updates.
142
+ * Definitions stay pinned until this object is released; execution must recheck current registry policy.
143
+ */
144
+ export function createToolExposure({ registry }, { maxTools = 64, maxSchemaBytes = 262_144, reservedNames = [] } = {}) {
145
+ assertLimit(maxTools);
146
+ assertLimit(maxSchemaBytes);
147
+ const names = { forward: new Map(), reverse: new Map(), occupied: new Set(reservedNames) };
148
+ let snapshot = Object.freeze({ revision: 0, tools: Object.freeze([]), schemaBytes: 0 });
149
+ return Object.freeze({
150
+ promote({ toolIds }, _optional = {}) {
151
+ const descriptors = registry.list({});
152
+ indexNames(names, descriptors.map(descriptor => descriptor.id));
153
+ const result = promoteDescriptors(snapshot, toolIds, descriptors, names, { maxTools, maxSchemaBytes });
154
+ snapshot = result.snapshot;
155
+ return result;
156
+ },
157
+ getSnapshot(_required, _optional = {}) { return snapshot; },
158
+ resolveToolId({ wireName }, _optional = {}) {
159
+ return snapshot.tools.find(tool => tool.wireName === wireName)?.descriptor.id;
160
+ },
161
+ });
162
+ }
163
+ //# sourceMappingURL=tool-exposure.js.map
@@ -0,0 +1,23 @@
1
+ /** @file Dependency-free tool input errors shared by registration and normalization. */
2
+ /**
3
+ * Thrown by a `ToolHandler` (or a validator it calls, e.g. the kernel's `registration-kit.ts`
4
+ * `requireString`/`requireInputRecord`/etc.) to mean "the caller's input was malformed or missing a
5
+ * required field" — a fact about the CALL, not the server. `@jini-ai/daemon`'s `ToolExecutor.execute`
6
+ * tags a handler rejection's `ToolExecutionResult.errorKind` as `'validation'` when the thrown error
7
+ * is `instanceof ToolInputError`, and every HTTP-facing mapping of a `'failed'` execution (today:
8
+ * `@jini-ai/http-kit`'s `delegated-tools.ts`) uses that tag to answer with a 4xx instead of folding
9
+ * it into the same redacted 500 an actual internal/infrastructure failure gets.
10
+ *
11
+ * Deliberately a plain marker with no extra fields: the message itself is already caller-actionable
12
+ * (`withSchemaOnRejection` appends the tool's own input schema to it), so nothing beyond "this IS one
13
+ * of these" needs to survive the `instanceof` check.
14
+ */
15
+ export declare class ToolInputError extends Error {
16
+ constructor({ message }: {
17
+ message: string;
18
+ }, optional?: ErrorOptions);
19
+ }
20
+ /** Keeps caller shape failures distinct from infrastructure/policy failures in the gate. */
21
+ export declare class ToolInputNormalizationError extends ToolInputError {
22
+ }
23
+ //# sourceMappingURL=tool-input-errors.d.ts.map
@@ -0,0 +1,24 @@
1
+ /** @file Dependency-free tool input errors shared by registration and normalization. */
2
+ /**
3
+ * Thrown by a `ToolHandler` (or a validator it calls, e.g. the kernel's `registration-kit.ts`
4
+ * `requireString`/`requireInputRecord`/etc.) to mean "the caller's input was malformed or missing a
5
+ * required field" — a fact about the CALL, not the server. `@jini-ai/daemon`'s `ToolExecutor.execute`
6
+ * tags a handler rejection's `ToolExecutionResult.errorKind` as `'validation'` when the thrown error
7
+ * is `instanceof ToolInputError`, and every HTTP-facing mapping of a `'failed'` execution (today:
8
+ * `@jini-ai/http-kit`'s `delegated-tools.ts`) uses that tag to answer with a 4xx instead of folding
9
+ * it into the same redacted 500 an actual internal/infrastructure failure gets.
10
+ *
11
+ * Deliberately a plain marker with no extra fields: the message itself is already caller-actionable
12
+ * (`withSchemaOnRejection` appends the tool's own input schema to it), so nothing beyond "this IS one
13
+ * of these" needs to survive the `instanceof` check.
14
+ */
15
+ export class ToolInputError extends Error {
16
+ constructor({ message }, optional = {}) {
17
+ super(message, optional);
18
+ this.name = 'ToolInputError';
19
+ }
20
+ }
21
+ /** Keeps caller shape failures distinct from infrastructure/policy failures in the gate. */
22
+ export class ToolInputNormalizationError extends ToolInputError {
23
+ }
24
+ //# sourceMappingURL=tool-input-errors.js.map
@@ -0,0 +1,37 @@
1
+ import type { ToolRegistration } from './tool-registry.js';
2
+ export { ToolInputNormalizationError } from './tool-input-errors.js';
3
+ /** A value-free account of canonicalization; domain hooks use the same contract as the gate. */
4
+ export interface NormalizedToolInput {
5
+ readonly input: unknown;
6
+ readonly notes: readonly string[];
7
+ }
8
+ export type ToolInputNormalizer = (required: {
9
+ input: Readonly<Record<string, unknown>>;
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
13
+ * layers. Uses the same bounded schema walk as normalizeToolInput.
14
+ * @example canonicalizeToolInputKeys({ input: { max_chars: '1000' }, schema });
15
+ */
16
+ export declare function canonicalizeToolInputKeys({ input, schema }: {
17
+ input: Readonly<Record<string, unknown>>;
18
+ schema: unknown;
19
+ }, _options?: Record<string, never>): {
20
+ input: Record<string, unknown>;
21
+ notes: readonly string[];
22
+ };
23
+ /** Normalize before policy, confirmation and handler validation. Domain hooks own semantic aliases.
24
+ * Never changes the caller's object or includes values in notes. Invalid/ambiguous input remains
25
+ * a ToolInputError; ordinary domain/schema validation still belongs to the registered handler.
26
+ * @complexity Two bounded schema walks plus the optional domain hook's cost.
27
+ */
28
+ export declare function normalizeToolInput({ input, registration }: {
29
+ input: unknown;
30
+ registration: Pick<ToolRegistration, 'descriptor' | 'normalizeInput' | 'invocationSchema'>;
31
+ }, _options?: Record<string, never>): Promise<NormalizedToolInput>;
32
+ /** Keep result shapes intact and make rewrite notes visible in plain and MCP tool results. */
33
+ export declare function withNormalizationNotes({ output, notes }: {
34
+ output: unknown;
35
+ notes: readonly string[];
36
+ }, _options?: Record<string, never>): unknown;
37
+ //# sourceMappingURL=tool-input-normalization.d.ts.map
@@ -0,0 +1,188 @@
1
+ import { isNonArrayRecord } from './records.js';
2
+ import { ToolInputError, ToolInputNormalizationError } from './tool-input-errors.js';
3
+ export { ToolInputNormalizationError } from './tool-input-errors.js';
4
+ const object = (value) => isNonArrayRecord({ value });
5
+ const keyShape = (key) => key.replace(/[_-]/g, '').toLowerCase();
6
+ /** Length of the shared prefix of two strings. */
7
+ function commonPrefixLength({ left, right }) {
8
+ let index = 0;
9
+ while (index < left.length && left[index] === right[index])
10
+ index++;
11
+ return index;
12
+ }
13
+ /** Same-length strings that differ by one substitution or one adjacent transposition at `index`. */
14
+ function sameLengthNearMiss({ left, right, index }) {
15
+ return left.slice(index + 1) === right.slice(index + 1)
16
+ || (left[index] === right[index + 1] && left[index + 1] === right[index] && left.slice(index + 2) === right.slice(index + 2));
17
+ }
18
+ /** Whether either key is the generic id shorthand for the other's id suffix; hints only. */
19
+ function isIdShorthand({ left, right }, _options = {}) {
20
+ return (left === 'id' && right.endsWith('id')) || (right === 'id' && left.endsWith('id'));
21
+ }
22
+ /** Detect id shorthand or one insertion, deletion, substitution or transposition, for hints only. */
23
+ function nearMiss({ left, right }, _options = {}) {
24
+ if (left.length > 128 || right.length > 128)
25
+ return false;
26
+ if (isIdShorthand({ left, right }))
27
+ return true;
28
+ if (Math.abs(left.length - right.length) > 1)
29
+ return false;
30
+ const index = commonPrefixLength({ left, right });
31
+ if (left.length === right.length)
32
+ return sameLengthNearMiss({ left, right, index });
33
+ return left.length > right.length ? left.slice(index + 1) === right.slice(index) : left.slice(index) === right.slice(index + 1);
34
+ }
35
+ const NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
36
+ /** Number/integer coercion; returns `value` itself when the string is not a representable number. */
37
+ function coerceNumber({ value, types, path, notes }) {
38
+ const numeric = types.includes('number');
39
+ const integer = types.includes('integer');
40
+ if (!(numeric || integer) || !NUMBER_PATTERN.test(value.trim()))
41
+ return value;
42
+ const number = Number(value);
43
+ if (!Number.isFinite(number) || (integer && !numeric && !Number.isSafeInteger(number)))
44
+ return value;
45
+ notes.push(`${path}: string→${numeric ? 'number' : 'integer'}`);
46
+ return number;
47
+ }
48
+ /** Boolean words first, then numbers; any other string is left for the handler's validator. */
49
+ function coerceString({ value, types, path, notes }) {
50
+ if (types.includes('boolean') && /^(true|false)$/i.test(value.trim())) {
51
+ notes.push(`${path}: string→boolean`);
52
+ return value.trim().toLowerCase() === 'true';
53
+ }
54
+ return coerceNumber({ value, types, path, notes });
55
+ }
56
+ function normalizeArray({ value, schema, step }) {
57
+ const { path, depth, walk } = step;
58
+ let list = value;
59
+ // The single-object shorthand applies to items batching, never arbitrary domain arrays.
60
+ if (!walk.keysOnly && object(value) && path.split('.').at(-1) === 'items') {
61
+ walk.notes.push(`${path}: object→one-item array`);
62
+ list = [value];
63
+ }
64
+ if (!Array.isArray(list))
65
+ return list;
66
+ return list.map((item, index) => normalizeValue({ value: item, schema: schema.items, step: { path: `${path}[${index}]`, depth: depth + 1, walk } }));
67
+ }
68
+ /** The declared property a supplied key names, or undefined; refuses a key that names two. */
69
+ function canonicalKey({ key, location, shape }) {
70
+ const candidates = Object.hasOwn(shape.properties, key) ? [key] : shape.names.filter(name => keyShape(name) === keyShape(key));
71
+ if (candidates.length > 1)
72
+ throw new ToolInputError({ message: `Ambiguous input '${location}': matches ${candidates.join(', ')}. Use exactly one canonical property name.` });
73
+ return candidates[0];
74
+ }
75
+ /** Describe the local schema and likely names without choosing a name or exposing input values.
76
+ * Target and permission fields must be declared in the tool schema before callers can use them.
77
+ * @complexity O(p) time and space for p declared properties; name comparisons are length-bounded.
78
+ */
79
+ function unknownKeyError({ key, location, shape }, _options = {}) {
80
+ const required = new Set(shape.required);
81
+ const names = [...shape.names.filter(name => required.has(name)), ...shape.names.filter(name => !required.has(name))];
82
+ const declared = names.map(name => `${name} (${required.has(name) ? 'required' : 'optional'})`).join(', ') || 'none';
83
+ const hints = names.filter(name => nearMiss({ left: keyShape(key), right: keyShape(name) }));
84
+ const suggestion = hints.length ? ` Did you mean ${hints.map(name => `'${name}'`).join(', ')}?` : '';
85
+ return new ToolInputError({ message: `Unknown input '${location}'. Declared inputs: ${declared}.${suggestion} Resend with the declared name.` });
86
+ }
87
+ /** One key of an object: canonical rename + recursion, closed-schema refusal, or open pass-through. */
88
+ function normalizeEntry({ key, raw, shape, sources, step }) {
89
+ const { path, depth, walk } = step;
90
+ const location = path ? `${path}.${key}` : key;
91
+ const target = canonicalKey({ key, location, shape });
92
+ if (target === undefined) {
93
+ if (walk.validateClosed && shape.schema.additionalProperties === false)
94
+ throw unknownKeyError({ key, location, shape });
95
+ const childSchema = object(shape.schema.additionalProperties) ? shape.schema.additionalProperties : undefined;
96
+ return [key, normalizeValue({ value: raw, schema: childSchema, step: { path: location, depth: depth + 1, walk } })];
97
+ }
98
+ const previous = sources.get(target);
99
+ if (previous !== undefined)
100
+ throw new ToolInputError({ message: `Conflicting inputs '${previous}' and '${key}' both name '${target}'. Send only '${target}'.` });
101
+ sources.set(target, key);
102
+ if (target !== key)
103
+ walk.notes.push(`${location}→${target}`);
104
+ return [target, normalizeValue({ value: raw, schema: shape.properties[target], step: { path: `${path ? `${path}.` : ''}${target}`, depth: depth + 1, walk } })];
105
+ }
106
+ function normalizeObject({ value, schema, step }) {
107
+ const properties = object(schema.properties) ? schema.properties : {};
108
+ const required = Array.isArray(schema.required) ? schema.required.filter((key) => typeof key === 'string') : [];
109
+ const shape = { schema, properties, names: Object.keys(properties), required };
110
+ const sources = new Map();
111
+ return Object.fromEntries(Object.entries(value).map(([key, raw]) => normalizeEntry({ key, raw, shape, sources, step })));
112
+ }
113
+ /** 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
115
+ * to 64 levels so a hostile input/schema cannot exhaust the execution gate's stack.
116
+ */
117
+ function normalizeValue({ value, schema, step }) {
118
+ if (!object(schema))
119
+ return value;
120
+ if (step.depth > 64)
121
+ throw new ToolInputError({ message: 'Tool input exceeds the 64-level normalization limit. Send a shallower object.' });
122
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
123
+ // A string is never an array or object, so it is either coerced here or returned as-is.
124
+ if (typeof value === 'string') {
125
+ return step.walk.keysOnly || types.includes('string') ? value : coerceString({ value, types, path: step.path, notes: step.walk.notes });
126
+ }
127
+ if (types.includes('array'))
128
+ return normalizeArray({ value, schema, step });
129
+ return object(value) ? normalizeObject({ value, schema, step }) : value;
130
+ }
131
+ /** A walk from the root of one input. */
132
+ function walkRoot({ input, schema, notes, validateClosed, keysOnly }) {
133
+ return normalizeValue({ value: input, schema, step: { path: '', depth: 0, walk: { notes, validateClosed, keysOnly } } });
134
+ }
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
137
+ * layers. Uses the same bounded schema walk as normalizeToolInput.
138
+ * @example canonicalizeToolInputKeys({ input: { max_chars: '1000' }, schema });
139
+ */
140
+ export function canonicalizeToolInputKeys({ input, schema }, _options = {}) {
141
+ const notes = [];
142
+ return { input: walkRoot({ input, schema, notes, validateClosed: false, keysOnly: true }), notes };
143
+ }
144
+ /** Normalize before policy, confirmation and handler validation. Domain hooks own semantic aliases.
145
+ * Never changes the caller's object or includes values in notes. Invalid/ambiguous input remains
146
+ * a ToolInputError; ordinary domain/schema validation still belongs to the registered handler.
147
+ * @complexity Two bounded schema walks plus the optional domain hook's cost.
148
+ */
149
+ export async function normalizeToolInput({ input, registration }, _options = {}) {
150
+ try {
151
+ return await normalizeWithinGate({ input, registration });
152
+ }
153
+ catch (error) {
154
+ if (error instanceof ToolInputError)
155
+ throw new ToolInputNormalizationError({ message: error.message });
156
+ throw error;
157
+ }
158
+ }
159
+ const isObjectTool = (schema) => object(schema) && schema.type === 'object';
160
+ /** First pass (open), the optional domain hook, then the final closed-schema pass. */
161
+ async function normalizeWithinGate({ input, registration }) {
162
+ const schema = registration.invocationSchema ?? registration.descriptor.inputSchema;
163
+ if (isObjectTool(schema) && !object(input))
164
+ throw new ToolInputError({ message: `${registration.descriptor.id}: send one input object containing the tool's fields.` });
165
+ const notes = [];
166
+ let normalized = walkRoot({ input, schema, notes, validateClosed: false, keysOnly: false });
167
+ if (registration.normalizeInput && object(normalized)) {
168
+ const result = await registration.normalizeInput({ input: normalized });
169
+ normalized = result.input;
170
+ notes.push(...result.notes);
171
+ }
172
+ if (isObjectTool(schema) && !object(normalized))
173
+ throw new ToolInputError({ message: `${registration.descriptor.id}: normalization must produce one input object.` });
174
+ return { input: walkRoot({ input: normalized, schema, notes, validateClosed: true, keysOnly: false }), notes };
175
+ }
176
+ /** Keep result shapes intact and make rewrite notes visible in plain and MCP tool results. */
177
+ export function withNormalizationNotes({ output, notes }, _options = {}) {
178
+ if (!notes.length)
179
+ return output;
180
+ const text = `normalized: ${notes.join('; ')}`;
181
+ if (typeof output === 'string')
182
+ return `${output}\n${text}`;
183
+ if (object(output))
184
+ return { ...output, normalizationNotes: notes,
185
+ ...(Array.isArray(output.content) ? { content: [...output.content, { type: 'text', text }] } : {}) };
186
+ return { result: output, normalizationNotes: notes };
187
+ }
188
+ //# sourceMappingURL=tool-input-normalization.js.map
@@ -0,0 +1,26 @@
1
+ /** Domain registrations own these declarations; projections never infer risk or authorization. */
2
+ export interface ToolApprovalMetadata {
3
+ readonly class: 'read' | 'edit' | 'trash' | 'delete' | 'restore-over-existing' | 'publish' | 'replace-secret' | 'escalation';
4
+ readonly confirmation: 'direct' | 'policy' | 'plan';
5
+ readonly input?: 'human-form';
6
+ /** Host-owned input classification rule, separate from the declared side effects. */
7
+ readonly rule?: string;
8
+ }
9
+ export interface ToolMetadata<A extends ToolApprovalMetadata = ToolApprovalMetadata> {
10
+ /** Installed guidance capabilities needed by a writer; independent of permission and risk. */
11
+ readonly guidanceCapabilities?: readonly string[];
12
+ readonly search?: {
13
+ readonly keywords?: string;
14
+ readonly queries?: readonly string[];
15
+ };
16
+ readonly approval?: A;
17
+ readonly mcpUi?: {
18
+ readonly redeemable?: boolean;
19
+ readonly exchangeOnly?: boolean;
20
+ readonly secretField?: {
21
+ readonly secret: true;
22
+ };
23
+ };
24
+ }
25
+ export type ToolMetadataById<A extends ToolApprovalMetadata = ToolApprovalMetadata> = Readonly<Record<string, ToolMetadata<A>>>;
26
+ //# sourceMappingURL=tool-metadata-types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool-metadata-types.js.map
@@ -1,26 +1,5 @@
1
- /** Domain registrations own these declarations; projections never infer risk or authorization. */
2
- export interface ToolApprovalMetadata {
3
- readonly class: 'read' | 'edit' | 'trash' | 'delete' | 'restore-over-existing' | 'publish' | 'replace-secret' | 'escalation';
4
- readonly confirmation: 'direct' | 'policy' | 'plan';
5
- readonly input?: 'human-form';
6
- /** Host-owned input classification rule, separate from the declared side effects. */
7
- readonly rule?: string;
8
- }
9
- export interface ToolMetadata<A extends ToolApprovalMetadata = ToolApprovalMetadata> {
10
- readonly search?: {
11
- readonly keywords?: string;
12
- readonly queries?: readonly string[];
13
- };
14
- readonly approval?: A;
15
- readonly mcpUi?: {
16
- readonly redeemable?: boolean;
17
- readonly exchangeOnly?: boolean;
18
- readonly secretField?: {
19
- readonly secret: true;
20
- };
21
- };
22
- }
23
- export type ToolMetadataById<A extends ToolApprovalMetadata = ToolApprovalMetadata> = Readonly<Record<string, ToolMetadata<A>>>;
1
+ import type { ToolApprovalMetadata, ToolMetadata, ToolMetadataById } from './tool-metadata-types.js';
2
+ export type { ToolApprovalMetadata, ToolMetadata, ToolMetadataById } from './tool-metadata-types.js';
24
3
  /** Own-property lookup: inherited object keys can never declare a tool's approval or callback access. */
25
4
  export declare function toolMetadataFor({ toolId, metadata }: {
26
5
  toolId: string;
@@ -26,7 +26,9 @@
26
26
  * `archived provenance ledger` for the 2026-07-29 hardening note.
27
27
  */
28
28
  import type { Principal } from './principal.js';
29
- import type { ToolMetadata } from './tool-metadata.js';
29
+ import type { AgentToolSideEffect } from './agent-tools.js';
30
+ import type { ToolMetadata } from './tool-metadata-types.js';
31
+ import { type ToolInputNormalizer } from './tool-input-normalization.js';
30
32
  /** A structural run reference — anything with a stable `id` satisfies this (e.g. `@jini-ai/protocol`'s `RunStatus`). No import needed to satisfy it; see the module doc. */
31
33
  export interface RunRef {
32
34
  readonly id: string;
@@ -42,8 +44,8 @@ export interface ToolDescriptor {
42
44
  readonly description?: string;
43
45
  /**
44
46
  * JSON-Schema-shaped description of this tool's accepted input, carried as `unknown` because the
45
- * kernel neither parses nor validates it — a schema dialect is a consumer concern, and taking a
46
- * validator dependency here would put one in every package that touches a registry.
47
+ * gate reads common property/type fields for input normalization but leaves validation and
48
+ * schema dialects to the handler; no validator dependency belongs in every registry consumer.
47
49
  *
48
50
  * It exists because *every* discovery mechanism needs it and nothing else in the kernel can
49
51
  * supply it: a caller that can enumerate `ToolRegistry.list({})` still cannot tell an agent what
@@ -74,6 +76,8 @@ export interface ToolDescriptor {
74
76
  * would admit every unclassified write, which is the exact failure the flag exists to prevent.
75
77
  */
76
78
  readonly readOnly?: boolean;
79
+ /** Independently checked domain risk, retained for discovery; never grants permission. */
80
+ readonly sideEffects?: AgentToolSideEffect;
77
81
  }
78
82
  /**
79
83
  * The single read-only determination: may this tool run through a surface that has promised its
@@ -90,6 +94,17 @@ export interface ToolDescriptor {
90
94
  export declare function isReadOnlyTool({ descriptor }: {
91
95
  descriptor: ToolDescriptor | undefined;
92
96
  }): boolean;
97
+ /** Project registration risk for discovery using the same read-only decision as execution.
98
+ * Missing classification stays unknown; discovery never grants execution authority.
99
+ * @returns Explicit readOnly and sideEffects fields for a registry descriptor.
100
+ * @complexity O(1); no effects.
101
+ */
102
+ export declare function toolDiscoveryMetadata({ descriptor }: {
103
+ descriptor: Pick<ToolDescriptor, 'id' | 'readOnly' | 'sideEffects'> | undefined;
104
+ }, _options?: {}): {
105
+ readOnly: boolean;
106
+ sideEffects: AgentToolSideEffect | 'unknown';
107
+ };
93
108
  /**
94
109
  * One human-facing message a handler pushes out mid-call.
95
110
  *
@@ -145,27 +160,21 @@ export interface ToolExecutionContext {
145
160
  /** Optional handler ports supplied by the invoking transport. */
146
161
  export interface ToolExecutionOptions {
147
162
  readonly emitSurface?: SurfaceEmitter;
163
+ /** Invoke through the same executor, retaining this call's principal, run, cancellation and
164
+ * transport gates. Rejects on denial, failed confirmation, validation or execution failure.
165
+ * A composition-owned registry may retain a source registration behind a derived catalog card.
166
+ * Batch wrappers use this port for each final merged input; every supplied registry is gated.
167
+ */
168
+ readonly invokeTool?: (required: {
169
+ toolId: string;
170
+ input: unknown;
171
+ }, optional?: {
172
+ registry?: ToolRegistry;
173
+ }) => Promise<unknown>;
148
174
  }
149
175
  /** Runs the tool's actual side effect. Only ever invoked by `ToolExecutor`, never called directly by a route/agent holding the registry. */
150
176
  export type ToolHandler = (required: ToolExecutionContext, optional?: ToolExecutionOptions) => Promise<unknown>;
151
- /**
152
- * Thrown by a `ToolHandler` (or a validator it calls, e.g. the kernel's `registration-kit.ts`
153
- * `requireString`/`requireInputRecord`/etc.) to mean "the caller's input was malformed or missing a
154
- * required field" — a fact about the CALL, not the server. `@jini-ai/daemon`'s `ToolExecutor.execute`
155
- * tags a handler rejection's `ToolExecutionResult.errorKind` as `'validation'` when the thrown error
156
- * is `instanceof ToolInputError`, and every HTTP-facing mapping of a `'failed'` execution (today:
157
- * `@jini-ai/http-kit`'s `delegated-tools.ts`) uses that tag to answer with a 4xx instead of folding
158
- * it into the same redacted 500 an actual internal/infrastructure failure gets.
159
- *
160
- * Deliberately a plain marker with no extra fields: the message itself is already caller-actionable
161
- * (`withSchemaOnRejection` appends the tool's own input schema to it), so nothing beyond "this IS one
162
- * of these" needs to survive the `instanceof` check.
163
- */
164
- export declare class ToolInputError extends Error {
165
- constructor({ message }: {
166
- message: string;
167
- }, optional?: ErrorOptions);
168
- }
177
+ export { ToolInputError } from './tool-input-errors.js';
169
178
  export type AuthorizationDecision = 'allow' | 'deny';
170
179
  export interface ToolAuthorizationContext {
171
180
  readonly principal: Principal;
@@ -185,13 +194,18 @@ export interface ToolPolicy {
185
194
  authorize(ctx: ToolAuthorizationContext): AuthorizationDecision | Promise<AuthorizationDecision>;
186
195
  }
187
196
  export interface ToolRegistration {
197
+ /** Optional execution-only schema, e.g. authenticated form callbacks. Never published by discovery;
198
+ * the handler must retain callback authentication and authorization. Defaults to descriptor.inputSchema. */
199
+ readonly invocationSchema?: unknown;
200
+ /** Domain-owned aliases, read by the single execution gate before validation/authorization. */
201
+ readonly normalizeInput?: ToolInputNormalizer;
188
202
  readonly descriptor: ToolDescriptor;
189
203
  readonly handler: ToolHandler;
190
204
  readonly policy: ToolPolicy;
191
205
  }
192
206
  /** The public registration/enumeration surface. See the module doc for why `handler`/`policy` never appear here. */
193
207
  export interface ToolRegistry {
194
- /** @throws If `registration.descriptor.id` is already registered — re-registration must be explicit (unregister is not exposed; this registry is append-only by design, matching the kernel's "tools are registered once at composition time" model). */
208
+ /** @throws If the policy has no callable `authorize` or `registration.descriptor.id` is already registered — re-registration must be explicit (unregister is not exposed; this registry is append-only by design, matching the kernel's "tools are registered once at composition time" model). */
195
209
  register(registration: ToolRegistration): void;
196
210
  has(required: {
197
211
  toolId: string;
@@ -242,6 +256,8 @@ export type ToolInvocationAuthorization = {
242
256
  readonly descriptor: ToolDescriptor;
243
257
  readonly decision: 'allow';
244
258
  readonly handler: ToolHandler;
259
+ readonly input: unknown;
260
+ readonly normalizationNotes: readonly string[];
245
261
  } | {
246
262
  readonly descriptor: ToolDescriptor;
247
263
  readonly decision: 'deny';