@jini-ai/core 0.6.15-rc.2 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -2
  3. package/dist/approval.d.ts +19 -4
  4. package/dist/approval.js +54 -14
  5. package/dist/batch-tool-inputs.d.ts +8 -0
  6. package/dist/batch-tool-inputs.js +63 -8
  7. package/dist/contribution-registry.d.ts +23 -0
  8. package/dist/fetch/cache.d.ts +1 -0
  9. package/dist/fetch/cache.js +15 -7
  10. package/dist/index.d.ts +4 -3
  11. package/dist/index.js +2 -2
  12. package/dist/optional-peer.d.ts +17 -0
  13. package/dist/optional-peer.js +40 -0
  14. package/dist/origin-validation.js +9 -8
  15. package/dist/primitives/index.d.ts +1 -0
  16. package/dist/primitives/index.js +1 -0
  17. package/dist/primitives/paths.js +2 -1
  18. package/dist/primitives/sse.js +4 -3
  19. package/dist/primitives/trim-chars.d.ts +25 -0
  20. package/dist/primitives/trim-chars.js +28 -0
  21. package/dist/redact.d.ts +0 -1
  22. package/dist/redact.js +29 -7
  23. package/dist/registration-kit.js +1 -3
  24. package/dist/settings/agent-tools.js +7 -5
  25. package/dist/settings/agent-value-write-tools.js +7 -3
  26. package/dist/settings/ensure-definitions.js +4 -2
  27. package/dist/settings/express/http-request.js +2 -1
  28. package/dist/settings/express/http.js +12 -4
  29. package/dist/settings/repo.memory.js +5 -2
  30. package/dist/settings/settings.js +5 -1
  31. package/dist/settings/tool-registrations.js +9 -3
  32. package/dist/settings/write-service.js +11 -9
  33. package/dist/target-scope.js +2 -1
  34. package/dist/text/index.d.ts +2 -0
  35. package/dist/text/index.js +2 -0
  36. package/dist/text/line-tail.d.ts +17 -0
  37. package/dist/text/line-tail.js +25 -0
  38. package/dist/text/sanitize-untrusted-text.js +33 -4
  39. package/dist/tool-exposure.js +20 -16
  40. package/dist/tool-input-normalization.d.ts +11 -3
  41. package/dist/tool-input-normalization.js +14 -8
  42. package/dist/tool-metadata-types.d.ts +5 -0
  43. package/dist/tool-registry.d.ts +9 -0
  44. package/package.json +8 -10
@@ -1,4 +1,4 @@
1
- const SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
1
+ const SAFE_NAME = /^[A-Za-z_]\w{0,63}$/;
2
2
  const MAX_NAME_LENGTH = 64;
3
3
  /** A stable, non-security hash keeps transformed names independent of discovery order.
4
4
  * Collisions are still checked against the entire namespace, never trusted as unique. */
@@ -11,7 +11,7 @@ function nameHash(id) {
11
11
  function nameBase(id) {
12
12
  if (SAFE_NAME.test(id))
13
13
  return id;
14
- const readable = id.replace(/[^A-Za-z0-9_]/g, '_');
14
+ const readable = id.replace(/\W/g, '_');
15
15
  return `tool_${readable.slice(0, 49)}_${nameHash(id)}`;
16
16
  }
17
17
  /** O(c) collision probes for c occupied alternatives; suffix space stays inside the wire limit. */
@@ -22,7 +22,8 @@ function assignName(index, id) {
22
22
  let candidate = base;
23
23
  let counter = 1;
24
24
  while (index.occupied.has(candidate)) {
25
- const suffix = `_${counter++}`;
25
+ const suffix = `_${counter}`;
26
+ counter += 1;
26
27
  candidate = `${base.slice(0, MAX_NAME_LENGTH - suffix.length)}${suffix}`;
27
28
  }
28
29
  index.forward.set(id, candidate);
@@ -89,6 +90,14 @@ function pinDescriptor(descriptor) {
89
90
  }
90
91
  }
91
92
  /** Count and byte caps reject new entries without evicting any previously callable definition. */
93
+ /** A tool that can join the snapshot (pinned), or why it is skipped. */
94
+ function promotionCandidate(descriptor, snapshot, limits) {
95
+ if (!descriptor)
96
+ return { reason: 'not-found' };
97
+ const pinned = pinDescriptor(descriptor);
98
+ const reason = limitReason(snapshot, pinned.bytes, limits);
99
+ return reason ? { reason } : { pinned };
100
+ }
92
101
  function limitReason(snapshot, bytes, limits) {
93
102
  if (snapshot.tools.length >= limits.maxTools)
94
103
  return 'tool-limit';
@@ -107,21 +116,16 @@ function promoteDescriptors(snapshot, toolIds, descriptors, names, limits) {
107
116
  for (const toolId of new Set(toolIds)) {
108
117
  if (published.has(toolId))
109
118
  continue;
110
- const descriptor = available.get(toolId);
111
- if (!descriptor) {
112
- skipped.push({ toolId, reason: 'not-found' });
113
- continue;
119
+ const candidate = promotionCandidate(available.get(toolId), { tools, schemaBytes, revision: snapshot.revision }, limits);
120
+ if ('reason' in candidate) {
121
+ skipped.push({ toolId, reason: candidate.reason });
114
122
  }
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;
123
+ else {
124
+ assignName(names, toolId);
125
+ tools.push({ wireName: names.forward.get(toolId), descriptor: candidate.pinned.descriptor });
126
+ schemaBytes += candidate.pinned.bytes;
127
+ added.push(toolId);
120
128
  }
121
- assignName(names, toolId);
122
- tools.push({ wireName: names.forward.get(toolId), descriptor: pinned.descriptor });
123
- schemaBytes += pinned.bytes;
124
- added.push(toolId);
125
129
  }
126
130
  let next = snapshot;
127
131
  if (added.length > 0)
@@ -1,4 +1,3 @@
1
- import type { ToolRegistration } from './tool-registry.js';
2
1
  export { ToolInputNormalizationError } from './tool-input-errors.js';
3
2
  /** A value-free account of canonicalization; domain hooks use the same contract as the gate. */
4
3
  export interface NormalizedToolInput {
@@ -8,8 +7,17 @@ export interface NormalizedToolInput {
8
7
  export type ToolInputNormalizer = (required: {
9
8
  input: Readonly<Record<string, unknown>>;
10
9
  }, optional?: Record<string, never>) => NormalizedToolInput | Promise<NormalizedToolInput>;
10
+ /** Only the registration facts consumed by normalization; carries no execution authority. */
11
+ interface NormalizationRegistration {
12
+ readonly descriptor: {
13
+ readonly id: string;
14
+ readonly inputSchema?: unknown;
15
+ };
16
+ readonly invocationSchema?: unknown;
17
+ readonly normalizeInput?: ToolInputNormalizer;
18
+ }
11
19
  /** 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
20
+ * and domain hooks for final normalization. Throws ToolInputError for differing colliding values within this layer, never across
13
21
  * layers. Uses the same bounded schema walk as normalizeToolInput.
14
22
  * @example canonicalizeToolInputKeys({ input: { max_chars: '1000' }, schema });
15
23
  */
@@ -28,7 +36,7 @@ export declare function canonicalizeToolInputKeys({ input, schema }: {
28
36
  */
29
37
  export declare function normalizeToolInput({ input, registration }: {
30
38
  input: unknown;
31
- registration: Pick<ToolRegistration, 'descriptor' | 'normalizeInput' | 'invocationSchema'>;
39
+ registration: NormalizationRegistration;
32
40
  }, _options?: Record<string, never>): Promise<NormalizedToolInput>;
33
41
  /** Keep result shapes intact and make rewrite notes visible in plain and MCP tool results. */
34
42
  export declare function withNormalizationNotes({ output, notes }: {
@@ -117,10 +117,14 @@ function unknownKeyError({ key, location, shape }, _options = {}) {
117
117
  const names = [...shape.names.filter(name => required.has(name)), ...shape.names.filter(name => !required.has(name))];
118
118
  const declared = names.map(name => `${name} (${required.has(name) ? 'required' : 'optional'})`).join(', ') || 'none';
119
119
  const hints = names.filter(name => nearMiss({ left: keyShape(key), right: keyShape(name) }));
120
- const suggestion = hints.length ? ` Did you mean ${hints.map(name => `'${name}'`).join(', ')}?` : '';
120
+ const quotedHints = hints.map(name => `'${name}'`).join(', ');
121
+ const suggestion = hints.length ? ` Did you mean ${quotedHints}?` : '';
121
122
  return new ToolInputError({ message: `Unknown input '${location}'. Declared inputs: ${declared}.${suggestion} Resend with the declared name.` });
122
123
  }
123
- /** One key of an object: canonical rename + recursion, closed-schema refusal, or open pass-through. */
124
+ /** One key of an object: canonical rename + recursion, closed-schema refusal, or open pass-through.
125
+ * Agreeing scalar aliases coalesce after coercion; differing values still fail without disclosure.
126
+ * @complexity The bounded child walk plus O(1) source/value checks.
127
+ */
124
128
  function normalizeEntry({ key, raw, shape, sources, step }) {
125
129
  const { path, depth, walk } = step;
126
130
  const location = path ? `${path}.${key}` : key;
@@ -131,13 +135,15 @@ function normalizeEntry({ key, raw, shape, sources, step }) {
131
135
  const childSchema = object(shape.schema.additionalProperties) ? shape.schema.additionalProperties : undefined;
132
136
  return [key, normalizeValue({ value: raw, schema: childSchema, step: { path: location, depth: depth + 1, walk } })];
133
137
  }
134
- const previous = sources.get(target);
135
- if (previous !== undefined)
136
- throw new ToolInputError({ message: `Conflicting inputs '${previous}' and '${key}' both name '${target}'. Send only '${target}'.` });
137
- sources.set(target, key);
138
138
  if (target !== key)
139
139
  walk.notes.push(`${location}→${target}`);
140
- return [target, normalizeValue({ value: raw, schema: shape.properties[target], step: { path: `${path ? `${path}.` : ''}${target}`, depth: depth + 1, walk } })];
140
+ const parentPrefix = path ? `${path}.` : '';
141
+ const value = normalizeValue({ value: raw, schema: shape.properties[target], step: { path: `${parentPrefix}${target}`, depth: depth + 1, walk } });
142
+ const previous = sources.get(target);
143
+ if (previous !== undefined && !Object.is(previous.value, value))
144
+ throw new ToolInputError({ message: `Conflicting inputs '${previous.key}' and '${key}' both name '${target}'. Send only '${target}'.` });
145
+ sources.set(target, { key, value });
146
+ return [target, value];
141
147
  }
142
148
  function normalizeObject({ value, schema, step }) {
143
149
  const properties = object(schema.properties) ? schema.properties : {};
@@ -170,7 +176,7 @@ function walkRoot({ input, schema, notes, validateClosed, keysOnly }) {
170
176
  return normalizeValue({ value: input, schema, step: { path: '', depth: 0, walk: { notes, validateClosed, keysOnly } } });
171
177
  }
172
178
  /** 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
179
+ * and domain hooks for final normalization. Throws ToolInputError for differing colliding values within this layer, never across
174
180
  * layers. Uses the same bounded schema walk as normalizeToolInput.
175
181
  * @example canonicalizeToolInputKeys({ input: { max_chars: '1000' }, schema });
176
182
  */
@@ -7,6 +7,11 @@ export interface ToolApprovalMetadata {
7
7
  readonly rule?: string;
8
8
  }
9
9
  export interface ToolMetadata<A extends ToolApprovalMetadata = ToolApprovalMetadata> {
10
+ /** Human-facing action name and optional button verb; presentation only, never authority. */
11
+ readonly title?: string;
12
+ /** Plain-language consequence shown before approval; never used to classify or authorize. */
13
+ readonly summary?: string;
14
+ readonly actionVerb?: string;
10
15
  /** Installed guidance capabilities needed by a writer; independent of permission and risk. */
11
16
  readonly guidanceCapabilities?: readonly string[];
12
17
  readonly search?: {
@@ -160,9 +160,18 @@ export interface ToolExecutionContext {
160
160
  readonly input: unknown;
161
161
  readonly signal: AbortSignal;
162
162
  }
163
+ /** Value-free human-dialog outcomes recorded by the executor; never authorization grants. */
164
+ export type ToolApprovalDecision = 'approved' | 'declined' | 'expired' | 'abandoned';
163
165
  /** Optional handler ports supplied by the invoking transport. */
164
166
  export interface ToolExecutionOptions {
165
167
  readonly emitSurface?: SurfaceEmitter;
168
+ /** Reports the human wait to the executor so concurrent writes can be refused. This is
169
+ * notification only; it never grants authority or substitutes for the human dialog. */
170
+ readonly onApprovalStateChange?: (required: {
171
+ pending: boolean;
172
+ /** Bound frontend's human-dialog outcome; observation only, never an authorization grant. */
173
+ decision?: ToolApprovalDecision;
174
+ }, optional?: Record<string, never>) => void;
166
175
  /** Invoke through the same executor, retaining this call's principal, run, cancellation and
167
176
  * transport gates. Rejects on denial, failed confirmation, validation or execution failure.
168
177
  * A composition-owned registry may retain a source registration behind a derived catalog card.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/core",
3
- "version": "0.6.15-rc.2",
3
+ "version": "0.6.16",
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": {
@@ -28,7 +28,8 @@
28
28
  "./settings": "universal",
29
29
  "./settings/express": "node",
30
30
  "./plugin-directory": "universal",
31
- "./filesystem": "universal"
31
+ "./filesystem": "universal",
32
+ "./optional-peer": "universal"
32
33
  },
33
34
  "isolation": {
34
35
  "entries": {
@@ -161,6 +162,11 @@
161
162
  "types": "./dist/filesystem.d.ts",
162
163
  "import": "./dist/filesystem.js",
163
164
  "default": "./dist/filesystem.js"
165
+ },
166
+ "./optional-peer": {
167
+ "types": "./dist/optional-peer.d.ts",
168
+ "import": "./dist/optional-peer.js",
169
+ "default": "./dist/optional-peer.js"
164
170
  }
165
171
  },
166
172
  "files": [
@@ -187,14 +193,6 @@
187
193
  "express": "^4.21.0",
188
194
  "@types/express": "^4.17.21"
189
195
  },
190
- "peerDependencies": {
191
- "express": "^4.21.0 || ^5.0.0"
192
- },
193
- "peerDependenciesMeta": {
194
- "express": {
195
- "optional": true
196
- }
197
- },
198
196
  "scripts": {
199
197
  "build": "tsc -p tsconfig.json",
200
198
  "typecheck": "tsc -p tsconfig.json --noEmit",