@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
@@ -1,3 +1,4 @@
1
+ import { normalizeToolInput } from './tool-input-normalization.js';
1
2
  /**
2
3
  * The single read-only determination: may this tool run through a surface that has promised its
3
4
  * caller it only reads?
@@ -13,28 +14,31 @@
13
14
  export function isReadOnlyTool({ descriptor }) {
14
15
  return descriptor?.readOnly === true;
15
16
  }
16
- /**
17
- * Thrown by a `ToolHandler` (or a validator it calls, e.g. the kernel's `registration-kit.ts`
18
- * `requireString`/`requireInputRecord`/etc.) to mean "the caller's input was malformed or missing a
19
- * required field" — a fact about the CALL, not the server. `@jini-ai/daemon`'s `ToolExecutor.execute`
20
- * tags a handler rejection's `ToolExecutionResult.errorKind` as `'validation'` when the thrown error
21
- * is `instanceof ToolInputError`, and every HTTP-facing mapping of a `'failed'` execution (today:
22
- * `@jini-ai/http-kit`'s `delegated-tools.ts`) uses that tag to answer with a 4xx instead of folding
23
- * it into the same redacted 500 an actual internal/infrastructure failure gets.
24
- *
25
- * Deliberately a plain marker with no extra fields: the message itself is already caller-actionable
26
- * (`withSchemaOnRejection` appends the tool's own input schema to it), so nothing beyond "this IS one
27
- * of these" needs to survive the `instanceof` check.
17
+ /** Project registration risk for discovery using the same read-only decision as execution.
18
+ * Missing classification stays unknown; discovery never grants execution authority.
19
+ * @returns Explicit readOnly and sideEffects fields for a registry descriptor.
20
+ * @complexity O(1); no effects.
28
21
  */
29
- export class ToolInputError extends Error {
30
- constructor({ message }, optional = {}) {
31
- super(message, optional);
32
- this.name = 'ToolInputError';
33
- }
22
+ export function toolDiscoveryMetadata({ descriptor }, _options = {}) {
23
+ const readOnly = isReadOnlyTool({ descriptor });
24
+ return { readOnly, sideEffects: descriptor?.sideEffects ?? (readOnly ? 'none' : 'unknown') };
34
25
  }
26
+ export { ToolInputError } from './tool-input-errors.js';
35
27
  const registrationsByRegistry = new WeakMap();
28
+ /** Refuse malformed runtime contributions before either registration path changes the registry. */
29
+ function assertAuthorizationPolicy({ registration }, _optional = {}) {
30
+ if (typeof registration.policy?.authorize !== 'function') {
31
+ throw new Error(`ToolRegistry: tool "${registration.descriptor.id}" must declare an authorization policy`);
32
+ }
33
+ }
36
34
  // Static register() remains append-only; scoped host contributions below explicitly own removal.
37
35
  const registryRevisions = new WeakMap();
36
+ /** Advance a core registry's revision. `createToolRegistry` seeds 0 together with the registry's
37
+ * `registrationsByRegistry` entry, and only such registries reach `register()` or a scope's
38
+ * `replace()` (a scope refuses any other registry), so the seeded entry is always present. */
39
+ function bumpRegistryRevision(registry) {
40
+ registryRevisions.set(registry, registryRevisions.get(registry) + 1);
41
+ }
38
42
  /** A host can invalidate its disposable discovery snapshot without exposing handlers. */
39
43
  export function toolRegistryRevision(required, _optional = {}) {
40
44
  return registryRevisions.get(required.registry);
@@ -53,6 +57,7 @@ export function createToolRegistrationScope(required, _optional = {}) {
53
57
  replace({ registrations: next }, _optional = {}) {
54
58
  const ids = new Set();
55
59
  for (const registration of next) {
60
+ assertAuthorizationPolicy({ registration });
56
61
  const id = registration.descriptor.id;
57
62
  if (ids.has(id) || (registrations.has(id) && !owned.has(id)))
58
63
  throw new Error(`ToolRegistry: tool "${id}" is already registered`);
@@ -71,7 +76,7 @@ export function createToolRegistrationScope(required, _optional = {}) {
71
76
  },
72
77
  });
73
78
  owned = ids;
74
- registryRevisions.set(required.registry, (registryRevisions.get(required.registry) ?? 0) + 1);
79
+ bumpRegistryRevision(required.registry);
75
80
  },
76
81
  };
77
82
  }
@@ -89,11 +94,12 @@ export function createToolRegistry(_required) {
89
94
  const registrations = new Map();
90
95
  const registry = {
91
96
  register(registration) {
97
+ assertAuthorizationPolicy({ registration });
92
98
  if (registrations.has(registration.descriptor.id)) {
93
99
  throw new Error(`ToolRegistry: tool "${registration.descriptor.id}" is already registered`);
94
100
  }
95
101
  registrations.set(registration.descriptor.id, registration);
96
- registryRevisions.set(registry, (registryRevisions.get(registry) ?? 0) + 1);
102
+ bumpRegistryRevision(registry);
97
103
  },
98
104
  has({ toolId }) {
99
105
  return registrations.has(toolId);
@@ -106,6 +112,10 @@ export function createToolRegistry(_required) {
106
112
  registryRevisions.set(registry, 0);
107
113
  return registry;
108
114
  }
115
+ /** Whether a registration has no schema or domain input-normalization hook. O(1), no effects. */
116
+ function isSchemaFreeRegistration({ descriptor, registration }, _options = {}) {
117
+ return descriptor.inputSchema === undefined && registration.invocationSchema === undefined && registration.normalizeInput === undefined;
118
+ }
109
119
  /**
110
120
  * Package-internal escape hatch: resolves `toolId` in `registry` and runs
111
121
  * its authorization gate — the tool's own {@link ToolPolicy}, then an
@@ -133,10 +143,15 @@ export async function authorizeToolInvocation({ registry, toolId, principal, run
133
143
  return undefined;
134
144
  }
135
145
  const { descriptor, handler, policy } = registration;
146
+ // Schema-free tools have nothing to normalize; preserve their authorization/confirmation timing.
147
+ const normalized = isSchemaFreeRegistration({ descriptor, registration })
148
+ ? { input, notes: [] }
149
+ : await normalizeToolInput({ input, registration });
150
+ input = normalized.input;
136
151
  let decision = await policy.authorize({ principal, run, tool: descriptor, input });
137
152
  if (decision === 'allow' && delegate?.onAuthorize) {
138
153
  decision = await delegate.onAuthorize({ principal, run, tool: descriptor, input });
139
154
  }
140
- return decision === 'allow' ? { descriptor, decision, handler } : { descriptor, decision: 'deny' };
155
+ return decision === 'allow' ? { descriptor, decision, handler, input, normalizationNotes: normalized.notes } : { descriptor, decision: 'deny' };
141
156
  }
142
157
  //# sourceMappingURL=tool-registry.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/core",
3
- "version": "0.4.8",
3
+ "version": "0.6.10",
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": {
@@ -18,6 +18,8 @@
18
18
  "./gated-mutations": "universal",
19
19
  "./model-facing-tool-errors": "universal",
20
20
  "./contribution-registry": "universal",
21
+ "./provider-registry": "universal",
22
+ "./object-store": "universal",
21
23
  "./naming": "universal",
22
24
  "./records": "universal",
23
25
  "./primitives": "universal",
@@ -32,7 +34,9 @@
32
34
  "./primitives": "core",
33
35
  "./fetch": "fetch",
34
36
  "./settings": "settings",
35
- "./settings/express": "settings"
37
+ "./settings/express": "settings",
38
+ "./provider-registry": "providers",
39
+ "./object-store": "providers"
36
40
  },
37
41
  "domains": {
38
42
  "fetch": [
@@ -40,6 +44,10 @@
40
44
  ],
41
45
  "settings": [
42
46
  "src/settings"
47
+ ],
48
+ "providers": [
49
+ "src/provider-registry.ts",
50
+ "src/object-store.ts"
43
51
  ]
44
52
  },
45
53
  "shared": [
@@ -98,6 +106,16 @@
98
106
  "import": "./dist/contribution-registry.js",
99
107
  "default": "./dist/contribution-registry.js"
100
108
  },
109
+ "./provider-registry": {
110
+ "types": "./dist/provider-registry.d.ts",
111
+ "import": "./dist/provider-registry.js",
112
+ "default": "./dist/provider-registry.js"
113
+ },
114
+ "./object-store": {
115
+ "types": "./dist/object-store.d.ts",
116
+ "import": "./dist/object-store.js",
117
+ "default": "./dist/object-store.js"
118
+ },
101
119
  "./naming": {
102
120
  "types": "./dist/naming.d.ts",
103
121
  "import": "./dist/naming.js",
@@ -8,6 +8,34 @@ const LEGACY_NAME_PATTERN = /^[a-z0-9.-]+$/;
8
8
  const CAPABILITY_PATTERN = new RegExp(`^${SEGMENT}(?::${SEGMENT})?$`);
9
9
  const object = value => typeof value === 'object' && value !== null && !Array.isArray(value);
10
10
 
11
+ /** A declared id is malformed when it is not a bounded publisher/name string. */
12
+ const malformedId = id => typeof id !== 'string' || id.length > 129 || !ID_PATTERN.test(id);
13
+ const validCapability = p => typeof p === 'string' && p.length <= 129 && CAPABILITY_PATTERN.test(p);
14
+ const legacyIdFor = localName => `unknown/${localName}`;
15
+
16
+ /** The first shape error that stops identity validation before any field is interpreted. */
17
+ function structuralError({ manifest, namespace }) {
18
+ if (!object(manifest)) return 'Plugin manifest must be an object.';
19
+ if (manifest.extensions !== undefined && !object(manifest.extensions)) return 'Plugin extensions must be an object.';
20
+ const extension = manifest.extensions?.[namespace];
21
+ if (extension !== undefined && !object(extension)) return 'Plugin identity extension must be an object.';
22
+ return undefined;
23
+ }
24
+
25
+ /** Errors for a declared id; an absent id is a legacy identity, not an error. */
26
+ function idErrors({ id }) {
27
+ if (id === undefined) return [];
28
+ if (malformedId(id)) return ['Plugin id must have the form publisher/name (lowercase letters, digits, dots and hyphens).'];
29
+ // Only the host may infer a legacy identity; a publisher cannot claim its upgrade exemption.
30
+ return id.startsWith('unknown/') ? ["Plugin publisher 'unknown' is reserved for legacy identities."] : [];
31
+ }
32
+
33
+ /** Errors for a declared provides list; one malformed entry rejects the list rather than being dropped. */
34
+ function providesErrors({ provides }) {
35
+ const valid = provides === undefined || (Array.isArray(provides) && provides.every(validCapability));
36
+ return valid ? [] : ['Plugin provides must be an array of capability names.'];
37
+ }
38
+
11
39
  /** Collects identity errors without mutating the portable manifest. Absent metadata is legacy.
12
40
  * @param {{manifest: unknown, namespace: string}} required
13
41
  * @param {Record<string, never>} [optional]
@@ -15,22 +43,11 @@ const object = value => typeof value === 'object' && value !== null && !Array.is
15
43
  * @complexity O(k + p) in keyword and provides entries; no I/O.
16
44
  */
17
45
  export function validatePluginIdentity({ manifest, namespace }, optional = {}) {
18
- const errors = [];
19
- if (!object(manifest)) return { errors: ['Plugin manifest must be an object.'] };
20
- if (manifest.extensions !== undefined && !object(manifest.extensions)) return { errors: [...errors, 'Plugin extensions must be an object.'] };
46
+ const structural = structuralError({ manifest, namespace });
47
+ if (structural !== undefined) return { errors: [structural] };
21
48
  const extension = manifest.extensions?.[namespace];
22
- if (extension === undefined) return { errors };
23
- if (!object(extension)) return { errors: [...errors, 'Plugin identity extension must be an object.'] };
24
- if (extension.id !== undefined && (typeof extension.id !== 'string' || extension.id.length > 129 || !ID_PATTERN.test(extension.id))) {
25
- errors.push('Plugin id must have the form publisher/name (lowercase letters, digits, dots and hyphens).');
26
- } else if (typeof extension.id === 'string' && extension.id.startsWith('unknown/')) {
27
- // Only the host may infer a legacy identity; a publisher cannot claim its upgrade exemption.
28
- errors.push("Plugin publisher 'unknown' is reserved for legacy identities.");
29
- }
30
- if (extension.provides !== undefined && (!Array.isArray(extension.provides) || !extension.provides.every(p => typeof p === 'string' && p.length <= 129 && CAPABILITY_PATTERN.test(p)))) {
31
- errors.push('Plugin provides must be an array of capability names.');
32
- }
33
- return { errors };
49
+ if (extension === undefined) return { errors: [] };
50
+ return { errors: [...idErrors({ id: extension.id }), ...providesErrors({ provides: extension.provides })] };
34
51
  }
35
52
 
36
53
  /** Resolves one identity; warns on missing ids and refuses malformed declarations.
@@ -45,20 +62,33 @@ export function readPluginIdentity({ manifest, namespace, name }, optional = {})
45
62
  const { errors } = validatePluginIdentity({ manifest, namespace });
46
63
  if (errors.length) throw new TypeError(errors.join(' '));
47
64
  const extension = manifest.extensions?.[namespace];
65
+ const declaredId = extension?.id;
48
66
  const localName = name ?? manifest.name;
49
- if (extension?.id === undefined && (typeof localName !== 'string' || !LEGACY_NAME_PATTERN.test(localName))) {
67
+ const id = declaredId ?? legacyIdentity({ localName });
68
+ const keywords = normalizeKeywords({ raw: manifest.keywords });
69
+ const warnings = declaredId === undefined ? [`Plugin '${localName}' has no publisher-qualified id; using '${id}'.`] : [];
70
+ return { id, keywords, provides: [...(extension?.provides ?? [])], warnings: [...warnings, ...keywordWarnings({ raw: manifest.keywords, keywords })] };
71
+ }
72
+
73
+ /** The host-inferred `unknown/<name>` id, refusing a name the legacy grammar never accepted. */
74
+ function legacyIdentity({ localName }) {
75
+ if (typeof localName !== 'string' || !LEGACY_NAME_PATTERN.test(localName)) {
50
76
  throw new TypeError('A valid plugin name is required for a legacy identity.');
51
77
  }
52
- const id = extension?.id ?? `unknown/${localName}`;
53
- // Keywords are advisory portable metadata: older manifests accepted mixed or non-array values.
54
- // The portable parser's coercion (non-string entries dropped, non-arrays ignored) lives here
55
- // so validation and lookup normalize advisory metadata through the same owner.
56
- const keywords = Array.isArray(manifest.keywords) ? manifest.keywords.filter(k => typeof k === 'string' && k.trim().length > 0) : [];
57
- const warnings = extension?.id === undefined ? [`Plugin '${localName}' has no publisher-qualified id; using '${id}'.`] : [];
58
- if (manifest.keywords !== undefined && (!Array.isArray(manifest.keywords) || keywords.length !== manifest.keywords.length)) {
59
- warnings.push('Plugin keywords must be an array of nonempty strings; invalid keywords were ignored.');
60
- }
61
- return { id, keywords, provides: [...(extension?.provides ?? [])], warnings };
78
+ return legacyIdFor(localName);
79
+ }
80
+
81
+ /** Keywords are advisory portable metadata: older manifests accepted mixed or non-array values.
82
+ * The portable parser's coercion (non-string entries dropped, non-arrays ignored) lives here
83
+ * so validation and lookup normalize advisory metadata through the same owner. */
84
+ function normalizeKeywords({ raw }) {
85
+ return Array.isArray(raw) ? raw.filter(k => typeof k === 'string' && k.trim().length > 0) : [];
86
+ }
87
+
88
+ /** One warning when normalization ignored any declared keyword (or a non-array value). */
89
+ function keywordWarnings({ raw, keywords }) {
90
+ const ignored = raw !== undefined && (!Array.isArray(raw) || keywords.length !== raw.length);
91
+ return ignored ? ['Plugin keywords must be an array of nonempty strings; invalid keywords were ignored.'] : [];
62
92
  }
63
93
 
64
94
  /** Rejects id reuse and publisher changes at an existing storage slot. Legacy metadata may
@@ -72,16 +102,25 @@ export function readPluginIdentity({ manifest, namespace, name }, optional = {})
72
102
  export function assertPluginIdentityAvailable({ candidate, installed }, optional = {}) {
73
103
  for (const plugin of installed) {
74
104
  const sameSlot = plugin.family === candidate.family && plugin.localId === candidate.localId;
75
- if (sameSlot) {
76
- if (plugin.id !== candidate.id && plugin.id !== `unknown/${candidate.localId}`) {
77
- throw new Error(`Plugin '${candidate.localId}' belongs to a different publisher identity.`);
78
- }
79
- } else if (plugin.id === candidate.id && !(plugin.family !== candidate.family && candidate.id === `unknown/${candidate.localId}` && plugin.id === `unknown/${plugin.localId}`)) {
80
- throw new Error(`Duplicate plugin id '${candidate.id}'.`);
81
- }
105
+ const conflict = sameSlot ? sameSlotConflict({ plugin, candidate }) : otherSlotConflict({ plugin, candidate });
106
+ if (conflict !== undefined) throw new Error(conflict);
82
107
  }
83
108
  }
84
109
 
110
+ /** A provider already in the candidate's slot may only be the same identity or its legacy fallback. */
111
+ function sameSlotConflict({ plugin, candidate }) {
112
+ const compatible = plugin.id === candidate.id || plugin.id === legacyIdFor(candidate.localId);
113
+ return compatible ? undefined : `Plugin '${candidate.localId}' belongs to a different publisher identity.`;
114
+ }
115
+
116
+ /** A provider in another slot may not reuse the candidate's id, except equal legacy fallback ids,
117
+ * which are family-scoped and so name distinct plugins in different families. */
118
+ function otherSlotConflict({ plugin, candidate }) {
119
+ if (plugin.id !== candidate.id) return undefined;
120
+ const crossFamilyLegacy = plugin.family !== candidate.family && candidate.id === legacyIdFor(candidate.localId) && plugin.id === legacyIdFor(plugin.localId);
121
+ return crossFamilyLegacy ? undefined : `Duplicate plugin id '${candidate.id}'.`;
122
+ }
123
+
85
124
  /** Restricts an SDK handle at runtime, not just through a TypeScript interface. A host may carry
86
125
  * lifecycle mutation ports, but a plugin's discovery surface exposes only these metadata reads.
87
126
  * @param {{lookup: import('./plugin-directory.js').PluginLookupPort}} required