@jini-ai/core 0.4.6 → 0.4.8
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 +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/registration-kit.d.ts +5 -0
- package/dist/registration-kit.js +31 -3
- package/dist/target-scope.d.ts +50 -0
- package/dist/target-scope.js +66 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.4.8 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
- `applyTargetScope`: one optional `site` input on read-only tools; the host resolves the target and a peer handler/principal, writes refuse a foreign target (3786c8f0).
|
|
6
|
+
|
|
7
|
+
## 0.4.7 — 2026-10-16
|
|
8
|
+
|
|
9
|
+
- `requireString`, `requireNumber` and `requireObject` errors list the keys the call actually received and flag unsupported `items[]` batching (a6ac9d7e, c7557838).
|
|
10
|
+
|
|
3
11
|
## 0.4.6 — 2026-10-14
|
|
4
12
|
|
|
5
13
|
- Express 5 accepted: the optional `express` peer is now `^4.21.0 || ^5.0.0` (1442d875), so an Express 5 host no longer hits ERESOLVE.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './naming.js';
|
|
|
21
21
|
export type { AgentToolSideEffect, AgentToolActorClassRule, AgentToolDefinition } from './agent-tools.js';
|
|
22
22
|
export * from "./registration-kit.js";
|
|
23
23
|
export * from "./approval.js";
|
|
24
|
+
export * from './target-scope.js';
|
|
24
25
|
export * from "./native-regexp.js";
|
|
25
26
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, type LegacyToolAuthorizeFn, type AuthorizationPort as ToolPermissionAuthorizationPort, type RequireToolPermissionRequired, type RequireToolPermissionOptional, } from "./tool-permissions.js";
|
|
26
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './contribution-registry.js';
|
|
|
17
17
|
export * from './naming.js';
|
|
18
18
|
export * from "./registration-kit.js";
|
|
19
19
|
export * from "./approval.js";
|
|
20
|
+
export * from './target-scope.js';
|
|
20
21
|
export * from "./native-regexp.js";
|
|
21
22
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, } from "./tool-permissions.js";
|
|
22
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -139,6 +139,11 @@ export declare function indexCatalogById<T extends {
|
|
|
139
139
|
export declare function requireInputRecord(required: {
|
|
140
140
|
input: unknown;
|
|
141
141
|
}, _optional?: Record<string, never>): Record<string, unknown>;
|
|
142
|
+
/**
|
|
143
|
+
* Reads a required non-empty string, with received-key hints when the field is missing.
|
|
144
|
+
* @throws {ToolInputError} If the field is missing, empty, or not a string.
|
|
145
|
+
* @complexity O(k) for k input keys on a missing-field rejection; otherwise O(1).
|
|
146
|
+
*/
|
|
142
147
|
export declare function requireString(required: {
|
|
143
148
|
input: Record<string, unknown>;
|
|
144
149
|
key: string;
|
package/dist/registration-kit.js
CHANGED
|
@@ -110,11 +110,33 @@ export function requireInputRecord(required, _optional = {}) {
|
|
|
110
110
|
throw new ToolInputError({ message: "input must be an object" });
|
|
111
111
|
return candidate.value;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Builds received-key and unsupported-batching hints for a missing required field.
|
|
115
|
+
* @complexity O(k) for k input keys.
|
|
116
|
+
*/
|
|
117
|
+
function missingFieldHint(required, _optional = {}) {
|
|
118
|
+
const { input } = required;
|
|
119
|
+
// Show names, never values, so a wrong-key or batch call can be corrected without exposing content.
|
|
120
|
+
const keys = Object.keys(input);
|
|
121
|
+
let hint = keys.length > 0 ? `; received: ${keys.join(", ")}` : "";
|
|
122
|
+
if (Array.isArray(input.items)) {
|
|
123
|
+
hint += "; this tool takes one input per call — items[] batching is not supported here";
|
|
124
|
+
}
|
|
125
|
+
return hint;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Reads a required non-empty string, with received-key hints when the field is missing.
|
|
129
|
+
* @throws {ToolInputError} If the field is missing, empty, or not a string.
|
|
130
|
+
* @complexity O(k) for k input keys on a missing-field rejection; otherwise O(1).
|
|
131
|
+
*/
|
|
113
132
|
export function requireString(required, _optional = {}) {
|
|
114
133
|
const { input, key } = required;
|
|
115
134
|
const value = input[key];
|
|
116
135
|
if (typeof value !== "string" || value.length === 0) {
|
|
117
|
-
|
|
136
|
+
let message = `'${key}' (non-empty string) is required`;
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
message += missingFieldHint({ input });
|
|
139
|
+
throw new ToolInputError({ message });
|
|
118
140
|
}
|
|
119
141
|
return value;
|
|
120
142
|
}
|
|
@@ -122,7 +144,10 @@ export function requireNumber(requiredArgs, optionalArgs = {}) {
|
|
|
122
144
|
const { input, key } = requiredArgs;
|
|
123
145
|
const value = input[key];
|
|
124
146
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
125
|
-
|
|
147
|
+
let message = `'${key}' (number) is required`;
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
message += missingFieldHint({ input });
|
|
150
|
+
throw new ToolInputError({ message });
|
|
126
151
|
}
|
|
127
152
|
return value;
|
|
128
153
|
}
|
|
@@ -131,7 +156,10 @@ export function requireObject(requiredArgs, optionalArgs = {}) {
|
|
|
131
156
|
const value = input[key];
|
|
132
157
|
const candidate = { value };
|
|
133
158
|
if (!isRecord(candidate) || Array.isArray(candidate.value)) {
|
|
134
|
-
|
|
159
|
+
let message = `'${key}' (object) is required`;
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
message += missingFieldHint({ input });
|
|
162
|
+
throw new ToolInputError({ message });
|
|
135
163
|
}
|
|
136
164
|
return candidate.value;
|
|
137
165
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep the same tool ids plus one optional target input instead of a tool per resource:
|
|
3
|
+
* each tool keeps one permission, approval class, search entry and card. Hosts own target
|
|
4
|
+
* resolution; core knows nothing about their sites, registries or files.
|
|
5
|
+
*
|
|
6
|
+
* Writes refuse foreign targets rather than silently ignoring the unknown key. A handler
|
|
7
|
+
* that ignores unknown keys would otherwise write to the CURRENT target under the wrong label.
|
|
8
|
+
*/
|
|
9
|
+
import type { Principal } from './principal.js';
|
|
10
|
+
import { type ToolHandler, type ToolRegistration } from './tool-registry.js';
|
|
11
|
+
/** A host either keeps the current handler and principal or supplies both for a peer. */
|
|
12
|
+
export type TargetResolution = 'current' | {
|
|
13
|
+
readonly handler: ToolHandler;
|
|
14
|
+
readonly principal: Principal;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Scope read-only registrations through host-supplied target ports and guard other tools
|
|
18
|
+
* against accidentally executing on the current target under a foreign target's name.
|
|
19
|
+
* @param required.registrations - Ordered tool catalog; registrations and schemas are never mutated.
|
|
20
|
+
* @param required.resolveTarget - Resolve a scoped read to the current handler or a peer handler and principal.
|
|
21
|
+
* @param required.isCurrentTarget - Check whether an unscoped tool names the current target.
|
|
22
|
+
* @param optional - Target input name, schema description, refusal text and a narrowing scope predicate.
|
|
23
|
+
* @returns A new array in the same order, preserving tools that already own the target input.
|
|
24
|
+
* @throws ToolInputError from a wrapped handler for a non-string target or a refused foreign target.
|
|
25
|
+
* Host port errors propagate unchanged from the wrapped handler.
|
|
26
|
+
* @complexity O(n) over registrations, plus copying supported schemas' top-level properties.
|
|
27
|
+
* @example applyTargetScope({ registrations, resolveTarget, isCurrentTarget }, { param: 'workspace' });
|
|
28
|
+
*/
|
|
29
|
+
export declare function applyTargetScope(required: {
|
|
30
|
+
readonly registrations: readonly ToolRegistration[];
|
|
31
|
+
/** Called only for scoped tools with a named target. Throw to refuse with the host's message unchanged. */
|
|
32
|
+
resolveTarget(required: {
|
|
33
|
+
target: string;
|
|
34
|
+
toolId: string;
|
|
35
|
+
principal: Principal;
|
|
36
|
+
}): Promise<TargetResolution>;
|
|
37
|
+
/** Called only for unscoped tools (writes and host-excluded reads) with a named target. */
|
|
38
|
+
isCurrentTarget(required: {
|
|
39
|
+
target: string;
|
|
40
|
+
}): boolean | Promise<boolean>;
|
|
41
|
+
}, optional?: {
|
|
42
|
+
readonly param?: string;
|
|
43
|
+
readonly describeParam?: string;
|
|
44
|
+
readonly refuseMessage?: string;
|
|
45
|
+
/** Narrows read-only tools; returning true can never scope a tool that is not read-only. */
|
|
46
|
+
readonly scoped?: (required: {
|
|
47
|
+
registration: ToolRegistration;
|
|
48
|
+
}) => boolean;
|
|
49
|
+
}): ToolRegistration[];
|
|
50
|
+
//# sourceMappingURL=target-scope.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isNonArrayRecord } from './records.js';
|
|
2
|
+
import { isReadOnlyTool, ToolInputError } from './tool-registry.js';
|
|
3
|
+
/** Narrow the shared record predicate to ordinary or null-prototype objects, excluding instances. */
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
if (!isNonArrayRecord({ value }))
|
|
6
|
+
return false;
|
|
7
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8
|
+
return prototype === Object.prototype || prototype === null;
|
|
9
|
+
}
|
|
10
|
+
/** Add an optional input only to supported object schemas; preserve other dialects unchanged. */
|
|
11
|
+
function withTargetParam(schema, param, description) {
|
|
12
|
+
if (schema === undefined)
|
|
13
|
+
return { type: 'object', properties: { [param]: { type: 'string', description } } };
|
|
14
|
+
if (!isPlainObject(schema))
|
|
15
|
+
return schema;
|
|
16
|
+
if (!isPlainObject(schema.properties) && schema.type !== 'object')
|
|
17
|
+
return schema;
|
|
18
|
+
return { ...schema, properties: { ...(isPlainObject(schema.properties) ? schema.properties : {}), [param]: { type: 'string', description } } };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scope read-only registrations through host-supplied target ports and guard other tools
|
|
22
|
+
* against accidentally executing on the current target under a foreign target's name.
|
|
23
|
+
* @param required.registrations - Ordered tool catalog; registrations and schemas are never mutated.
|
|
24
|
+
* @param required.resolveTarget - Resolve a scoped read to the current handler or a peer handler and principal.
|
|
25
|
+
* @param required.isCurrentTarget - Check whether an unscoped tool names the current target.
|
|
26
|
+
* @param optional - Target input name, schema description, refusal text and a narrowing scope predicate.
|
|
27
|
+
* @returns A new array in the same order, preserving tools that already own the target input.
|
|
28
|
+
* @throws ToolInputError from a wrapped handler for a non-string target or a refused foreign target.
|
|
29
|
+
* Host port errors propagate unchanged from the wrapped handler.
|
|
30
|
+
* @complexity O(n) over registrations, plus copying supported schemas' top-level properties.
|
|
31
|
+
* @example applyTargetScope({ registrations, resolveTarget, isCurrentTarget }, { param: 'workspace' });
|
|
32
|
+
*/
|
|
33
|
+
export function applyTargetScope(required, optional = {}) {
|
|
34
|
+
const { param = 'site', describeParam = 'Optional target name. Omit to use the current target.', refuseMessage = 'This tool runs only on the current target.' } = optional;
|
|
35
|
+
return required.registrations.map(registration => {
|
|
36
|
+
const { descriptor } = registration;
|
|
37
|
+
const schema = { value: descriptor.inputSchema };
|
|
38
|
+
const properties = { value: isNonArrayRecord(schema) ? schema.value.properties : undefined };
|
|
39
|
+
if (isNonArrayRecord(properties) && Object.hasOwn(properties.value, param))
|
|
40
|
+
return registration;
|
|
41
|
+
const scoped = isReadOnlyTool({ descriptor }) && (optional.scoped?.({ registration }) ?? true);
|
|
42
|
+
const inputSchema = scoped ? withTargetParam(descriptor.inputSchema, param, describeParam) : descriptor.inputSchema;
|
|
43
|
+
return {
|
|
44
|
+
...registration,
|
|
45
|
+
descriptor: inputSchema === descriptor.inputSchema ? descriptor : { ...descriptor, inputSchema },
|
|
46
|
+
handler: async (ctx, options) => {
|
|
47
|
+
if (!isPlainObject(ctx.input) || !Object.hasOwn(ctx.input, param))
|
|
48
|
+
return registration.handler(ctx, options);
|
|
49
|
+
const target = ctx.input[param];
|
|
50
|
+
if (typeof target !== 'string')
|
|
51
|
+
throw new ToolInputError({ message: `${param} must be a string` });
|
|
52
|
+
const { [param]: _target, ...input } = ctx.input;
|
|
53
|
+
if (!scoped) {
|
|
54
|
+
if (!await required.isCurrentTarget({ target }))
|
|
55
|
+
throw new ToolInputError({ message: refuseMessage });
|
|
56
|
+
return registration.handler({ ...ctx, input }, options);
|
|
57
|
+
}
|
|
58
|
+
const peer = await required.resolveTarget({ target, toolId: descriptor.id, principal: ctx.principal });
|
|
59
|
+
if (peer === 'current')
|
|
60
|
+
return registration.handler({ ...ctx, input }, options);
|
|
61
|
+
return peer.handler({ ...ctx, input, principal: peer.principal }, options);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=target-scope.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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": {
|