@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.
- package/CHANGELOG.md +23 -2
- package/README.md +5 -0
- package/dist/agent-tools.d.ts +1 -1
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +87 -22
- package/dist/batch-tool-inputs.d.ts +147 -0
- package/dist/batch-tool-inputs.js +257 -0
- package/dist/feature-flags.d.ts +21 -0
- package/dist/feature-flags.js +17 -0
- package/dist/fetch/index.d.ts +2 -1
- package/dist/fetch/index.js +1 -0
- package/dist/fetch/loader-read.d.ts +16 -0
- package/dist/fetch/loader-read.js +22 -0
- package/dist/fetch/types.d.ts +12 -2
- package/dist/gated-mutations/gateway.js +32 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -1
- package/dist/object-store.d.ts +115 -0
- package/dist/object-store.js +70 -0
- package/dist/origin-validation.js +64 -57
- package/dist/primitives/http.d.ts +2 -2
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +17 -10
- package/dist/primitives/tasks.d.ts +10 -0
- package/dist/primitives/tasks.js +8 -0
- package/dist/provider-catalog.d.ts +76 -0
- package/dist/provider-catalog.js +6 -0
- package/dist/provider-registry.d.ts +144 -0
- package/dist/provider-registry.js +138 -0
- package/dist/records.d.ts +24 -0
- package/dist/records.js +25 -0
- package/dist/redact.d.ts +32 -0
- package/dist/redact.js +91 -34
- package/dist/registration-kit.d.ts +7 -4
- package/dist/registration-kit.js +81 -62
- package/dist/settings/agent-value-write-tools.d.ts +19 -0
- package/dist/settings/agent-value-write-tools.js +131 -70
- package/dist/settings/ensure-definitions.d.ts +4 -2
- package/dist/settings/ensure-definitions.js +79 -63
- package/dist/settings/errors.d.ts +13 -0
- package/dist/settings/errors.js +15 -0
- package/dist/settings/express/cms-adapter.d.ts +2 -2
- package/dist/settings/express/cms-adapter.js +34 -22
- package/dist/settings/express/http-request.d.ts +33 -0
- package/dist/settings/express/http-request.js +49 -0
- package/dist/settings/express/http.d.ts +2 -2
- package/dist/settings/express/http.js +16 -39
- package/dist/settings/express/stream.d.ts +2 -2
- package/dist/settings/express/stream.js +49 -27
- package/dist/settings/index.d.ts +2 -2
- package/dist/settings/index.js +1 -1
- package/dist/settings/ports.d.ts +7 -1
- package/dist/settings/repo.memory.js +7 -4
- package/dist/settings/settings.js +54 -60
- package/dist/settings/tool-registrations.d.ts +2 -3
- package/dist/settings/tool-registrations.js +64 -39
- package/dist/settings/write-service.d.ts +42 -4
- package/dist/settings/write-service.js +72 -5
- package/dist/target-scope.d.ts +2 -0
- package/dist/target-scope.js +18 -2
- package/dist/text/escape.js +2 -2
- package/dist/text/sanitize-untrusted-text.d.ts +5 -0
- package/dist/text/sanitize-untrusted-text.js +55 -35
- package/dist/tool-exposure.d.ts +79 -0
- package/dist/tool-exposure.js +163 -0
- package/dist/tool-input-errors.d.ts +23 -0
- package/dist/tool-input-errors.js +24 -0
- package/dist/tool-input-normalization.d.ts +37 -0
- package/dist/tool-input-normalization.js +188 -0
- package/dist/tool-metadata-types.d.ts +26 -0
- package/dist/tool-metadata-types.js +2 -0
- package/dist/tool-metadata.d.ts +2 -23
- package/dist/tool-registry.d.ts +38 -22
- package/dist/tool-registry.js +35 -20
- package/package.json +20 -2
- package/plugin-directory.js +72 -33
package/dist/tool-registry.js
CHANGED
|
@@ -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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/plugin-directory.js
CHANGED
|
@@ -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
|
|
19
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|