@jini-ai/agent-plugins 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 +30 -0
- package/README.md +48 -0
- package/dist/admin-surfaces/action-dispatcher.d.ts +37 -0
- package/dist/admin-surfaces/action-dispatcher.js +69 -0
- package/dist/admin-surfaces/action-schema.d.ts +56 -0
- package/dist/admin-surfaces/action-schema.js +167 -0
- package/dist/admin-surfaces/action-tools.d.ts +40 -0
- package/dist/admin-surfaces/action-tools.js +52 -0
- package/dist/admin-surfaces/callback-field-inbox.d.ts +35 -0
- package/dist/admin-surfaces/callback-field-inbox.js +53 -0
- package/dist/admin-surfaces/capability-declarations.d.ts +41 -0
- package/dist/admin-surfaces/capability-declarations.js +102 -0
- package/dist/admin-surfaces/contribution.d.ts +49 -0
- package/dist/admin-surfaces/contribution.js +30 -0
- package/dist/admin-surfaces/delivery-bindings.d.ts +76 -0
- package/dist/admin-surfaces/delivery-bindings.js +99 -0
- package/dist/admin-surfaces/descriptor.d.ts +69 -0
- package/dist/admin-surfaces/descriptor.js +196 -0
- package/dist/admin-surfaces/external-links.d.ts +45 -0
- package/dist/admin-surfaces/external-links.js +70 -0
- package/dist/admin-surfaces/host-validator.d.ts +62 -0
- package/dist/admin-surfaces/host-validator.js +87 -0
- package/dist/admin-surfaces/index.d.ts +23 -0
- package/dist/admin-surfaces/index.js +21 -0
- package/dist/admin-surfaces/protocol.d.ts +103 -0
- package/dist/admin-surfaces/protocol.js +25 -0
- package/dist/admin-surfaces/sandbox.d.ts +78 -0
- package/dist/admin-surfaces/sandbox.js +92 -0
- package/dist/admin-surfaces/settings-value.d.ts +28 -0
- package/dist/admin-surfaces/settings-value.js +45 -0
- package/dist/admin-surfaces/surface-capabilities.d.ts +167 -0
- package/dist/admin-surfaces/surface-capabilities.js +215 -0
- package/dist/admin-surfaces/surface-client.d.ts +116 -0
- package/dist/admin-surfaces/surface-client.js +219 -0
- package/dist/admin-surfaces/surface-handlers.d.ts +111 -0
- package/dist/admin-surfaces/surface-handlers.js +172 -0
- package/dist/admin-surfaces/theme.d.ts +59 -0
- package/dist/admin-surfaces/theme.js +57 -0
- package/dist/folder-zip-node.d.ts +11 -0
- package/dist/folder-zip-node.js +84 -0
- package/dist/folder-zip.d.ts +38 -0
- package/dist/folder-zip.js +134 -0
- package/dist/lifecycle/automatic-use.d.ts +79 -0
- package/dist/lifecycle/automatic-use.js +165 -0
- package/dist/lifecycle/compatibility-metadata.d.ts +19 -0
- package/dist/lifecycle/compatibility-metadata.js +45 -0
- package/dist/lifecycle/compatibility.d.ts +52 -0
- package/dist/lifecycle/compatibility.js +60 -0
- package/dist/lifecycle/fetch-archive.js +26 -24
- package/dist/lifecycle/index.d.ts +14 -2
- package/dist/lifecycle/index.js +8 -1
- package/dist/lifecycle/install-errors.d.ts +14 -0
- package/dist/lifecycle/install-errors.js +9 -0
- package/dist/lifecycle/install.d.ts +2 -13
- package/dist/lifecycle/install.js +12 -123
- package/dist/lifecycle/installed-package-index.d.ts +15 -0
- package/dist/lifecycle/installed-package-index.js +128 -0
- package/dist/lifecycle/installed-package-roots.js +28 -20
- package/dist/lifecycle/manifest.d.ts +14 -0
- package/dist/lifecycle/manifest.js +115 -100
- package/dist/lifecycle/package-paths.js +3 -3
- package/dist/lifecycle/ports.d.ts +1 -1
- package/dist/lifecycle/provider-contribution.d.ts +65 -0
- package/dist/lifecycle/provider-contribution.js +93 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.js +19 -6
- package/dist/lifecycle/retire-bundled.js +20 -18
- package/dist/lifecycle/search.d.ts +6 -1
- package/dist/lifecycle/search.js +45 -7
- package/dist/lifecycle/set-enabled.d.ts +2 -1
- package/dist/lifecycle/set-enabled.js +52 -8
- package/dist/lifecycle/skill-file-reader.js +85 -71
- package/dist/lifecycle/tools/capabilities.d.ts +199 -0
- package/dist/lifecycle/tools/capabilities.js +213 -0
- package/dist/lifecycle/tools/definition.d.ts +32 -0
- package/dist/lifecycle/tools/definition.js +33 -0
- package/dist/lifecycle/tools/index.d.ts +10 -0
- package/dist/lifecycle/tools/index.js +10 -0
- package/dist/lifecycle/tools/manifest.d.ts +33 -0
- package/dist/lifecycle/tools/manifest.js +89 -0
- package/dist/lifecycle/tools/projection.d.ts +90 -0
- package/dist/lifecycle/tools/projection.js +283 -0
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/lifecycle/uninstall.d.ts +3 -1
- package/dist/lifecycle/uninstall.js +51 -27
- package/dist/lifecycle/update-plan.d.ts +18 -0
- package/dist/lifecycle/update-plan.js +22 -0
- package/dist/lifecycle/update-store.d.ts +71 -0
- package/dist/lifecycle/update-store.js +150 -0
- package/dist/lifecycle/update.d.ts +55 -0
- package/dist/lifecycle/update.js +321 -0
- package/dist/manifest.d.ts +2 -1
- package/dist/manifest.js +2 -1
- package/package.json +50 -6
- package/ui-ux-design/plugin.json +8 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle/activation.d.ts.map +0 -1
- package/dist/lifecycle/activation.js.map +0 -1
- package/dist/lifecycle/bundled-digests.d.ts.map +0 -1
- package/dist/lifecycle/bundled-digests.js.map +0 -1
- package/dist/lifecycle/bundled-source-archive.d.ts.map +0 -1
- package/dist/lifecycle/bundled-source-archive.js.map +0 -1
- package/dist/lifecycle/capability-projection.d.ts.map +0 -1
- package/dist/lifecycle/capability-projection.js.map +0 -1
- package/dist/lifecycle/contribution-loader.d.ts.map +0 -1
- package/dist/lifecycle/contribution-loader.js.map +0 -1
- package/dist/lifecycle/fetch-archive.d.ts.map +0 -1
- package/dist/lifecycle/fetch-archive.js.map +0 -1
- package/dist/lifecycle/index.d.ts.map +0 -1
- package/dist/lifecycle/index.js.map +0 -1
- package/dist/lifecycle/install-from-url.d.ts.map +0 -1
- package/dist/lifecycle/install-from-url.js.map +0 -1
- package/dist/lifecycle/install.d.ts.map +0 -1
- package/dist/lifecycle/install.js.map +0 -1
- package/dist/lifecycle/installed-package-roots.d.ts.map +0 -1
- package/dist/lifecycle/installed-package-roots.js.map +0 -1
- package/dist/lifecycle/layout.d.ts.map +0 -1
- package/dist/lifecycle/layout.js.map +0 -1
- package/dist/lifecycle/manifest.d.ts.map +0 -1
- package/dist/lifecycle/manifest.js.map +0 -1
- package/dist/lifecycle/mcp-provisioning.d.ts.map +0 -1
- package/dist/lifecycle/mcp-provisioning.js.map +0 -1
- package/dist/lifecycle/node.d.ts.map +0 -1
- package/dist/lifecycle/node.js.map +0 -1
- package/dist/lifecycle/package-paths.d.ts.map +0 -1
- package/dist/lifecycle/package-paths.js.map +0 -1
- package/dist/lifecycle/persistent-state.d.ts.map +0 -1
- package/dist/lifecycle/persistent-state.js.map +0 -1
- package/dist/lifecycle/ports.d.ts.map +0 -1
- package/dist/lifecycle/ports.js.map +0 -1
- package/dist/lifecycle/resolve-agent-plugin-refs.d.ts.map +0 -1
- package/dist/lifecycle/resolve-agent-plugin-refs.js.map +0 -1
- package/dist/lifecycle/retire-bundled.d.ts.map +0 -1
- package/dist/lifecycle/retire-bundled.js.map +0 -1
- package/dist/lifecycle/search.d.ts.map +0 -1
- package/dist/lifecycle/search.js.map +0 -1
- package/dist/lifecycle/seed-bundled.d.ts.map +0 -1
- package/dist/lifecycle/seed-bundled.js.map +0 -1
- package/dist/lifecycle/set-enabled.d.ts.map +0 -1
- package/dist/lifecycle/set-enabled.js.map +0 -1
- package/dist/lifecycle/skill-file-reader.d.ts.map +0 -1
- package/dist/lifecycle/skill-file-reader.js.map +0 -1
- package/dist/lifecycle/trusted-plugin-files.d.ts.map +0 -1
- package/dist/lifecycle/trusted-plugin-files.js.map +0 -1
- package/dist/lifecycle/types.d.ts.map +0 -1
- package/dist/lifecycle/types.js.map +0 -1
- package/dist/lifecycle/uninstall.d.ts.map +0 -1
- package/dist/lifecycle/uninstall.js.map +0 -1
- package/dist/lifecycle/yauzl-archive-reader.d.ts.map +0 -1
- package/dist/lifecycle/yauzl-archive-reader.js.map +0 -1
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js.map +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Read-side owner of installed package metadata; never constructs a mutation lifecycle. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AgentPluginInstallError } from "./install-errors.js";
|
|
4
|
+
import { readAgentPluginExtension, parseAgentPluginManifest as parseAgentPluginManifestValue } from "./manifest.js";
|
|
5
|
+
/** Indexes manifests and files with the same injected effects used by installation and resolution.
|
|
6
|
+
* @complexity O(f) reads/walk work for f package files; no mutation or executable imports. */
|
|
7
|
+
export function createInstalledPackageIndex(required, _optional = {}) {
|
|
8
|
+
const { ports } = required;
|
|
9
|
+
const readFile = ports.filesystem.readFile.bind(ports.filesystem);
|
|
10
|
+
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
11
|
+
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value }, { identityNamespace: ports.extensionNamespace });
|
|
12
|
+
/** Cosmetic fields are trimmed and capped rather than invalidating the package. The cap stops
|
|
13
|
+
* a hostile package flooding an admin row. Malformed values fall back because these fields are
|
|
14
|
+
* cosmetic: callers title-case the id and show the spec description when these keys are absent.
|
|
15
|
+
* Inner newlines remain paragraph breaks in summaries.
|
|
16
|
+
* @complexity O(n) in the supplied text length; output is capped at maxLength. */
|
|
17
|
+
function readDisplayText(required, _optional = {}) {
|
|
18
|
+
const text = readAgentPluginExtension({ manifest: required.manifest, namespace: ports.extensionNamespace,
|
|
19
|
+
read: ({ value }) => {
|
|
20
|
+
const field = value[required.key];
|
|
21
|
+
return typeof field === 'string' ? field.trim() : undefined;
|
|
22
|
+
} });
|
|
23
|
+
return text ? { [required.key]: text.slice(0, required.maxLength) } : {};
|
|
24
|
+
}
|
|
25
|
+
/** Reads and validates `packageRoot`'s `plugin.json`, refusing a missing, unparseable or invalid one. */
|
|
26
|
+
async function readInstalledManifest({ packageRoot }) {
|
|
27
|
+
let manifestRaw;
|
|
28
|
+
try {
|
|
29
|
+
manifestRaw = await readFile(path.join(packageRoot, "plugin.json"), "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
33
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_MISSING", message: "the archive does not contain a plugin.json at its root" });
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
let manifestValue;
|
|
38
|
+
try {
|
|
39
|
+
manifestValue = JSON.parse(manifestRaw);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: "plugin.json is not valid JSON" }, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
const parsed = parseAgentPluginManifest(manifestValue);
|
|
45
|
+
if (!parsed.ok) {
|
|
46
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
/** Every regular file under `packageRoot` (sorted) and every `skills/<name>/SKILL.md` (sorted by name). */
|
|
51
|
+
async function walkPackageTree({ packageRoot }) {
|
|
52
|
+
const files = [];
|
|
53
|
+
const skills = [];
|
|
54
|
+
async function walk(absolute, prefix) {
|
|
55
|
+
for (const entry of await readdir(absolute, { withFileTypes: true })) {
|
|
56
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
57
|
+
if (entry.isDirectory())
|
|
58
|
+
await walk(path.join(absolute, entry.name), relative);
|
|
59
|
+
else if (entry.isFile())
|
|
60
|
+
files.push(relative);
|
|
61
|
+
// Anything that is neither a directory nor a regular file (a symlink somehow already present,
|
|
62
|
+
// a device node) is skipped rather than indexed — extraction itself never creates one, so
|
|
63
|
+
// encountering one here would mean the filesystem changed out from under an already-published,
|
|
64
|
+
// supposedly-frozen package, which this function has no business acting on either way.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await walk(packageRoot, "");
|
|
68
|
+
for (const relative of files) {
|
|
69
|
+
const match = relative.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
|
70
|
+
if (match)
|
|
71
|
+
skills.push({ name: match[1], skillPath: relative });
|
|
72
|
+
}
|
|
73
|
+
return { files: files.sort(), skills: skills.sort((a, b) => a.name.localeCompare(b.name)) };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reads the (already-extracted-and-trusted, or already-published) `plugin.json` at `packageRoot`
|
|
77
|
+
* and walks the tree to build the `files`/`skills` index. Shared between a fresh install and the
|
|
78
|
+
* content-addressed dedup fast path so both return an identically-shaped result.
|
|
79
|
+
*
|
|
80
|
+
* Exported (2026-08-21) for a second caller outside this module:
|
|
81
|
+
* `resolve-agent-plugin-refs.ts`'s run-time resolution of a pinned `pluginRefId` against the
|
|
82
|
+
* packages already on disk needs the SAME `plugin.json`-parse-plus-tree-walk this function
|
|
83
|
+
* already does correctly (manifest validation via `parseAgentPluginManifest`, sorted `files`), so
|
|
84
|
+
* it re-derives an `InstalledAgentPlugin` per installed digest rather than hand-rolling a second,
|
|
85
|
+
* less-validated reader.
|
|
86
|
+
*/
|
|
87
|
+
// Conditional spreads, not `field: parsed.manifest.field` directly: an `exactOptionalPropertyTypes`-
|
|
88
|
+
// style caller (and this codebase's own convention elsewhere — see `buildToolSource`'s identical
|
|
89
|
+
// `defaultSkillReason` spread in `tool-registrations.ts`) must see an ABSENT key, not a key present
|
|
90
|
+
// with value `undefined`, when the manifest declared none of these — `deepEqual` fixtures across
|
|
91
|
+
// this feature's own tests already assert exact object shape.
|
|
92
|
+
async function indexInstalledRoot({ packageRoot, archiveDigest }, _optional = {}) {
|
|
93
|
+
const parsed = await readInstalledManifest({ packageRoot });
|
|
94
|
+
for (const message of parsed.warnings)
|
|
95
|
+
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
|
|
96
|
+
const { files, skills } = await walkPackageTree({ packageRoot });
|
|
97
|
+
return {
|
|
98
|
+
pluginId: parsed.manifest.name,
|
|
99
|
+
id: parsed.manifest.id,
|
|
100
|
+
provides: parsed.manifest.provides,
|
|
101
|
+
...(parsed.manifest.requires === undefined ? {} : { requires: parsed.manifest.requires }),
|
|
102
|
+
...(parsed.manifest.hostRange === undefined ? {} : { hostRange: parsed.manifest.hostRange }),
|
|
103
|
+
version: parsed.manifest.version,
|
|
104
|
+
...( /**
|
|
105
|
+
* `plugin.json`'s own `description`/`keywords`/`author`/`license` (`manifest.ts`'s
|
|
106
|
+
* `AgentPluginManifest`) — parsed at index time but, before 2026-09-09, discarded immediately
|
|
107
|
+
* after: only `name`/`version` ever reached `InstalledAgentPlugin`. Added for
|
|
108
|
+
* `search_agent_plugin_local` (`tool-registrations.ts`), which needs the description and the
|
|
109
|
+
* author-curated keywords to rank installed plugins against a free-text query — the manifest is
|
|
110
|
+
* already read and parsed here on every call, so carrying these through is zero additional I/O,
|
|
111
|
+
* not a new read path. All four are optional because the spec itself makes them optional (the real
|
|
112
|
+
* installed `ui-ux-design` fixture ships `description` but no `keywords`; a bare-minimum manifest
|
|
113
|
+
* ships neither) — a caller must not assume any of them are present.
|
|
114
|
+
*/parsed.manifest.description !== undefined ? { description: parsed.manifest.description } : {}),
|
|
115
|
+
...(parsed.manifest.keywords !== undefined ? { keywords: parsed.manifest.keywords } : {}),
|
|
116
|
+
...(parsed.manifest.author !== undefined ? { author: parsed.manifest.author } : {}),
|
|
117
|
+
...(parsed.manifest.license !== undefined ? { license: parsed.manifest.license } : {}),
|
|
118
|
+
...readDisplayText({ manifest: parsed.manifest, key: "displayName", maxLength: 64 }),
|
|
119
|
+
...readDisplayText({ manifest: parsed.manifest, key: "summary", maxLength: 1200 }),
|
|
120
|
+
archiveDigest,
|
|
121
|
+
packageRoot,
|
|
122
|
+
files,
|
|
123
|
+
skills,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { indexInstalledRoot };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=installed-package-index.js.map
|
|
@@ -9,12 +9,14 @@ export const INSTALLED_PLUGIN_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
9
9
|
* @complexity O(d) directory/containment operations for d revisions; no executable imports.
|
|
10
10
|
*/
|
|
11
11
|
export async function listInstalledPackageRoots({ ports, workspaceRoot }, _optional = {}) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const effects = {
|
|
13
|
+
readdir: ports.filesystem.readdir.bind(ports.filesystem),
|
|
14
|
+
assertOwnedPath: createPersistentStateModule(ports).assertOwnedPath,
|
|
15
|
+
assertContainedOnDisk: createPackagePathsModule(ports).assertContainedOnDisk,
|
|
16
|
+
};
|
|
15
17
|
let folders;
|
|
16
18
|
try {
|
|
17
|
-
folders = await readdir(workspaceRoot, { withFileTypes: true });
|
|
19
|
+
folders = await effects.readdir(workspaceRoot, { withFileTypes: true });
|
|
18
20
|
}
|
|
19
21
|
catch (error) {
|
|
20
22
|
if (isEnoent(error))
|
|
@@ -25,25 +27,31 @@ export async function listInstalledPackageRoots({ ports, workspaceRoot }, _optio
|
|
|
25
27
|
for (const folder of folders) {
|
|
26
28
|
if (!folder.isDirectory() || !/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(folder.name) || folder.name.length > 64)
|
|
27
29
|
continue;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
roots.push(...await listFolderRevisions({ effects, workspaceRoot, localId: folder.name }));
|
|
31
|
+
}
|
|
32
|
+
return roots;
|
|
33
|
+
}
|
|
34
|
+
/** One plugin folder's contained digest revisions; a folder with no owned package store has none. */
|
|
35
|
+
async function listFolderRevisions({ effects, workspaceRoot, localId }) {
|
|
36
|
+
const packagesDir = path.join(workspaceRoot, localId, 'package', 'sha256');
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
await effects.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, packagesDir) });
|
|
40
|
+
entries = await effects.readdir(packagesDir, { withFileTypes: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const roots = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory() || !INSTALLED_PLUGIN_DIGEST_PATTERN.test(entry.name))
|
|
48
|
+
continue;
|
|
30
49
|
try {
|
|
31
|
-
await
|
|
32
|
-
|
|
50
|
+
const packageRoot = await effects.assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
|
|
51
|
+
roots.push({ localId, archiveDigest: entry.name, packageRoot });
|
|
33
52
|
}
|
|
34
53
|
catch {
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
if (!entry.isDirectory() || !INSTALLED_PLUGIN_DIGEST_PATTERN.test(entry.name))
|
|
39
|
-
continue;
|
|
40
|
-
try {
|
|
41
|
-
const packageRoot = await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
|
|
42
|
-
roots.push({ localId: folder.name, archiveDigest: entry.name, packageRoot });
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
46
|
-
}
|
|
54
|
+
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
return roots;
|
|
@@ -8,6 +8,15 @@ export type AgentPluginServerMetadataReader = (required: {
|
|
|
8
8
|
readonly value: Readonly<Record<string, unknown>>;
|
|
9
9
|
readonly server: Readonly<Record<string, unknown>>;
|
|
10
10
|
}) => Readonly<Record<string, unknown>>;
|
|
11
|
+
/**
|
|
12
|
+
* True when `value` is a valid Agent Plugin id: `plugin.json`'s `name` grammar (§5.5, see
|
|
13
|
+
* `NAME_PATTERN`), 1-64 characters. The one owner of that grammar: a host checks an id taken from a
|
|
14
|
+
* URL or a tool input here before it names anything on disk, instead of restating the pattern.
|
|
15
|
+
* @complexity O(n) in the value's length.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isAgentPluginName({ value }: {
|
|
18
|
+
readonly value: unknown;
|
|
19
|
+
}, _optional?: Record<string, never>): boolean;
|
|
11
20
|
export interface AgentPluginManifest {
|
|
12
21
|
readonly name: string;
|
|
13
22
|
readonly version?: (string) | undefined;
|
|
@@ -19,6 +28,10 @@ export interface AgentPluginManifest {
|
|
|
19
28
|
/** Normalized publisher identity, independent of name/storage key and version. */
|
|
20
29
|
readonly id?: string | undefined;
|
|
21
30
|
readonly provides?: readonly string[] | undefined;
|
|
31
|
+
/** `extensions.<ns>.requires`: advisory capability ids other active plugins should provide. */
|
|
32
|
+
readonly requires?: readonly string[] | undefined;
|
|
33
|
+
/** `extensions.<ns>.hostRange`: semver range over AGENT_PLUGIN_CONTRACT_VERSION; a malformed value is stored as `<invalid>`. */
|
|
34
|
+
readonly hostRange?: string | undefined;
|
|
22
35
|
}
|
|
23
36
|
export type ParseAgentPluginManifestResult = {
|
|
24
37
|
readonly ok: true;
|
|
@@ -30,6 +43,7 @@ export type ParseAgentPluginManifestResult = {
|
|
|
30
43
|
};
|
|
31
44
|
/**
|
|
32
45
|
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
46
|
+
* Hosts inject identityNamespace; the default retains the existing plugin extension format.
|
|
33
47
|
*
|
|
34
48
|
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
35
49
|
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* own "receives a JS value, never a file path or raw bytes" contract. `install.ts` is the only
|
|
55
55
|
* caller that reads the file and hands the parsed value here.
|
|
56
56
|
*/
|
|
57
|
+
import { readAgentPluginCompatibilityMetadata } from "./compatibility-metadata.js";
|
|
57
58
|
import { readPluginIdentity, validatePluginIdentity } from "@jini-ai/core/plugin-directory";
|
|
58
59
|
export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
|
|
59
60
|
/**
|
|
@@ -104,6 +105,15 @@ function readSpecVersion(value, file) {
|
|
|
104
105
|
*/
|
|
105
106
|
const NAME_PATTERN = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
|
|
106
107
|
const MAX_NAME_LENGTH = 64;
|
|
108
|
+
/**
|
|
109
|
+
* True when `value` is a valid Agent Plugin id: `plugin.json`'s `name` grammar (§5.5, see
|
|
110
|
+
* `NAME_PATTERN`), 1-64 characters. The one owner of that grammar: a host checks an id taken from a
|
|
111
|
+
* URL or a tool input here before it names anything on disk, instead of restating the pattern.
|
|
112
|
+
* @complexity O(n) in the value's length.
|
|
113
|
+
*/
|
|
114
|
+
export function isAgentPluginName({ value }, _optional = {}) {
|
|
115
|
+
return typeof value === "string" && value.length <= MAX_NAME_LENGTH && NAME_PATTERN.test(value);
|
|
116
|
+
}
|
|
107
117
|
const KNOWN_MANIFEST_KEYS = new Set(["$schema", "name", "version", "description", "author", "license", "keywords", "extensions"]);
|
|
108
118
|
/**
|
|
109
119
|
* True for a non-null, non-array plain object — the "is this actually a JSON object" gate both
|
|
@@ -112,6 +122,17 @@ const KNOWN_MANIFEST_KEYS = new Set(["$schema", "name", "version", "description"
|
|
|
112
122
|
function isJsonObject(value) {
|
|
113
123
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
114
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* `undefined` for an absent optional field, otherwise whatever `parse` makes of it — `null` meaning
|
|
127
|
+
* present but malformed, which every caller folds into rejecting the whole entry.
|
|
128
|
+
*/
|
|
129
|
+
function parseOptional({ value, parse }) {
|
|
130
|
+
return value === undefined ? undefined : parse(value);
|
|
131
|
+
}
|
|
132
|
+
/** Drops `undefined` members, so an absent optional field stays absent rather than present-as-undefined. */
|
|
133
|
+
function definedFields(fields) {
|
|
134
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
135
|
+
}
|
|
115
136
|
/**
|
|
116
137
|
* `value` narrowed to a `string`, or `undefined` — every optional string field below coerces
|
|
117
138
|
* through this one check instead of repeating its own `typeof ... === "string" ? ... : undefined`
|
|
@@ -125,11 +146,7 @@ function parseManifestAuthor(value) {
|
|
|
125
146
|
return value; // Existing installed manifests may carry a string.
|
|
126
147
|
if (!isJsonObject(value))
|
|
127
148
|
return undefined;
|
|
128
|
-
return {
|
|
129
|
-
...(typeof value.name === 'string' ? { name: value.name } : {}),
|
|
130
|
-
...(typeof value.email === 'string' ? { email: value.email } : {}),
|
|
131
|
-
...(typeof value.url === 'string' ? { url: value.url } : {}),
|
|
132
|
-
};
|
|
149
|
+
return definedFields({ name: coerceOptionalString(value.name), email: coerceOptionalString(value.email), url: coerceOptionalString(value.url) });
|
|
133
150
|
}
|
|
134
151
|
/**
|
|
135
152
|
* Validates `plugin.json`'s `name` field per §5.5's grammar (see `NAME_PATTERN`'s own header for
|
|
@@ -141,25 +158,15 @@ function resolveManifestName(raw) {
|
|
|
141
158
|
const name = coerceOptionalString(raw.name);
|
|
142
159
|
if (name === undefined)
|
|
143
160
|
return { error: "plugin.json 'name' is required and must be a string" };
|
|
144
|
-
if (
|
|
161
|
+
if (!isAgentPluginName({ value: name })) {
|
|
145
162
|
return {
|
|
146
163
|
error: `plugin.json 'name' ('${name}') must be 1-${MAX_NAME_LENGTH} lowercase alphanumeric/hyphen/period characters, start and end alphanumeric, with no consecutive delimiters`,
|
|
147
164
|
};
|
|
148
165
|
}
|
|
149
166
|
return { name };
|
|
150
167
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
*
|
|
154
|
-
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
155
|
-
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
156
|
-
* `plugin-runtime/discovery.ts`).
|
|
157
|
-
* @complexity O(k + n) time and space in the input keys and keyword entries.
|
|
158
|
-
*/
|
|
159
|
-
export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu" } = {}) {
|
|
160
|
-
if (!isJsonObject(value))
|
|
161
|
-
return { ok: false, errors: ["plugin.json must be a JSON object"] };
|
|
162
|
-
const raw = value;
|
|
168
|
+
/** Every `plugin.json` error and warning, in report order: spec version, extensions, name, unknown keys, identity. */
|
|
169
|
+
function collectManifestProblems({ raw, identityNamespace }, { nameError } = {}) {
|
|
163
170
|
const errors = [];
|
|
164
171
|
const warnings = [];
|
|
165
172
|
const specVersion = readSpecVersion(raw.$schema, "plugin");
|
|
@@ -169,19 +176,39 @@ export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu"
|
|
|
169
176
|
warnings.push(specVersion.warning);
|
|
170
177
|
if (raw.extensions !== undefined && !isJsonObject(raw.extensions))
|
|
171
178
|
errors.push("plugin.json extensions must be an object");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
errors.push(nameResult.error);
|
|
179
|
+
if (nameError)
|
|
180
|
+
errors.push(nameError);
|
|
175
181
|
for (const key of Object.keys(raw)) {
|
|
176
182
|
if (!KNOWN_MANIFEST_KEYS.has(key))
|
|
177
183
|
warnings.push(`unrecognized plugin.json field '${key}' (ignored per spec)`);
|
|
178
184
|
}
|
|
179
185
|
errors.push(...validatePluginIdentity({ manifest: raw, namespace: identityNamespace }).errors);
|
|
186
|
+
return { errors, warnings };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
190
|
+
* Hosts inject identityNamespace; the default retains the existing plugin extension format.
|
|
191
|
+
*
|
|
192
|
+
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
193
|
+
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
194
|
+
* `plugin-runtime/discovery.ts`).
|
|
195
|
+
* @complexity O(k + n) time and space in the input keys and keyword entries.
|
|
196
|
+
*/
|
|
197
|
+
export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu" } = {}) {
|
|
198
|
+
if (!isJsonObject(value))
|
|
199
|
+
return { ok: false, errors: ["plugin.json must be a JSON object"] };
|
|
200
|
+
const raw = value;
|
|
201
|
+
const nameResult = resolveManifestName(raw);
|
|
202
|
+
const { errors, warnings } = collectManifestProblems({ raw, identityNamespace }, { nameError: nameResult.error });
|
|
180
203
|
if (errors.length > 0)
|
|
181
204
|
return { ok: false, errors };
|
|
182
205
|
const identity = readPluginIdentity({ manifest: raw, namespace: identityNamespace });
|
|
183
206
|
warnings.push(...identity.warnings);
|
|
207
|
+
const extension = isJsonObject(raw.extensions) && isJsonObject(raw.extensions[identityNamespace]) ? raw.extensions[identityNamespace] : {};
|
|
208
|
+
const { warnings: compatibilityWarnings, ...compatibility } = readAgentPluginCompatibilityMetadata({ extension });
|
|
209
|
+
warnings.push(...compatibilityWarnings);
|
|
184
210
|
const manifest = {
|
|
211
|
+
...compatibility,
|
|
185
212
|
name: nameResult.name,
|
|
186
213
|
id: identity.id,
|
|
187
214
|
provides: identity.provides,
|
|
@@ -223,20 +250,12 @@ function isStringRecord(value) {
|
|
|
223
250
|
function parseStdioServerConfig(raw) {
|
|
224
251
|
if (typeof raw.command !== "string" || raw.command.length === 0)
|
|
225
252
|
return null;
|
|
226
|
-
const { args,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
return null;
|
|
231
|
-
if (cwd !== undefined && typeof cwd !== "string")
|
|
253
|
+
const args = parseOptional({ value: raw.args, parse: (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : null });
|
|
254
|
+
const env = parseOptional({ value: raw.env, parse: (value) => isStringRecord(value) && !RESERVED_STDIO_ENV_KEYS.some((key) => Object.hasOwn(value, key)) ? value : null });
|
|
255
|
+
const cwd = parseOptional({ value: raw.cwd, parse: (value) => typeof value === "string" ? value : null });
|
|
256
|
+
if (args === null || env === null || cwd === null)
|
|
232
257
|
return null;
|
|
233
|
-
return {
|
|
234
|
-
type: "stdio",
|
|
235
|
-
command: raw.command,
|
|
236
|
-
...(args !== undefined ? { args: args } : {}),
|
|
237
|
-
...(env !== undefined ? { env } : {}),
|
|
238
|
-
...(cwd !== undefined ? { cwd } : {}),
|
|
239
|
-
};
|
|
258
|
+
return { type: "stdio", command: raw.command, ...definedFields({ args, env, cwd }) };
|
|
240
259
|
}
|
|
241
260
|
/**
|
|
242
261
|
* Same bound and grammar as `external-mcp-store.ts`'s `MAX_ALLOWED_TOOLS`/`REMOTE_TOOL_NAME_PATTERN`,
|
|
@@ -252,6 +271,10 @@ function parseDefaultToolNames(value) {
|
|
|
252
271
|
return null;
|
|
253
272
|
return value.every((name) => typeof name === "string" && DEFAULT_TOOL_NAME_PATTERN.test(name)) ? value : null;
|
|
254
273
|
}
|
|
274
|
+
/** `read` and `write` default to `[]` when absent. */
|
|
275
|
+
function parseGrantList(value) {
|
|
276
|
+
return value === undefined ? [] : parseDefaultToolNames(value);
|
|
277
|
+
}
|
|
255
278
|
/**
|
|
256
279
|
* Validates bounded sign-in and read lists. `read` and `write` default to `[]`.
|
|
257
280
|
* A provided `allow` bounds both lists; without it only a read declaration is meaningful,
|
|
@@ -263,15 +286,15 @@ function parseDefaultToolNames(value) {
|
|
|
263
286
|
function parseDefaultTools(value) {
|
|
264
287
|
if (!isJsonObject(value))
|
|
265
288
|
return null;
|
|
266
|
-
const allow = value.allow
|
|
267
|
-
const write =
|
|
268
|
-
const read =
|
|
289
|
+
const allow = parseOptional({ value: value.allow, parse: parseDefaultToolNames });
|
|
290
|
+
const write = parseGrantList(value.write);
|
|
291
|
+
const read = parseGrantList(value.read);
|
|
269
292
|
if (allow === null || write === null || read === null)
|
|
270
293
|
return null;
|
|
271
294
|
if (allow === undefined)
|
|
272
295
|
return value.read !== undefined && write.length === 0 ? { write, read } : null;
|
|
273
296
|
const allowed = new Set(allow);
|
|
274
|
-
return write
|
|
297
|
+
return [...write, ...read].every((name) => allowed.has(name)) ? { allow, write, read } : null;
|
|
275
298
|
}
|
|
276
299
|
const MAX_TOKEN_AUTH_URL_LENGTH = 2048;
|
|
277
300
|
function isHttpsUrl(value) {
|
|
@@ -319,18 +342,11 @@ function parseRenamedTools(value) {
|
|
|
319
342
|
function parseTokenAuth(value) {
|
|
320
343
|
if (!isJsonObject(value) || !isHttpsUrl(value.helpUrl) || !isHttpsUrl(value.probeUrl))
|
|
321
344
|
return null;
|
|
322
|
-
const { importFromEnv
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const retiredEnv = value.retiredEnv === undefined ? undefined : parseRetiredEnv(value.retiredEnv);
|
|
326
|
-
if (retiredEnv === null)
|
|
345
|
+
const importFromEnv = parseOptional({ value: value.importFromEnv, parse: (name) => isEnvVarName(name) ? name : null });
|
|
346
|
+
const retiredEnv = parseOptional({ value: value.retiredEnv, parse: parseRetiredEnv });
|
|
347
|
+
if (importFromEnv === null || retiredEnv === null)
|
|
327
348
|
return null;
|
|
328
|
-
return {
|
|
329
|
-
helpUrl: value.helpUrl,
|
|
330
|
-
probeUrl: value.probeUrl,
|
|
331
|
-
...(importFromEnv !== undefined ? { importFromEnv } : {}),
|
|
332
|
-
...(retiredEnv !== undefined ? { retiredEnv } : {}),
|
|
333
|
-
};
|
|
349
|
+
return { helpUrl: value.helpUrl, probeUrl: value.probeUrl, ...definedFields({ importFromEnv, retiredEnv }) };
|
|
334
350
|
}
|
|
335
351
|
/**
|
|
336
352
|
* Validates one `type: "streamable-http"` or `type: "sse"` server entry — the two remote transports
|
|
@@ -345,45 +361,64 @@ function parseTokenAuth(value) {
|
|
|
345
361
|
function parseRemoteServerConfig(type, raw, extension) {
|
|
346
362
|
if (typeof raw.url !== "string" || raw.url.length === 0)
|
|
347
363
|
return null;
|
|
348
|
-
const { headers
|
|
349
|
-
const
|
|
350
|
-
if (headers
|
|
351
|
-
return null;
|
|
352
|
-
if (authMode !== undefined && authMode !== "oauth" && authMode !== "none")
|
|
364
|
+
const headers = parseOptional({ value: raw.headers, parse: (value) => isStringRecord(value) ? value : null });
|
|
365
|
+
const metadata = parseServerMetadata(extension ?? {});
|
|
366
|
+
if (headers === null || metadata === null)
|
|
353
367
|
return null;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const renamedTools = extension
|
|
362
|
-
if (renamedTools === null)
|
|
368
|
+
return { type, url: raw.url, ...definedFields({ headers }), ...metadata };
|
|
369
|
+
}
|
|
370
|
+
/** A remote entry's extension metadata, or `null` when any declared member is malformed. */
|
|
371
|
+
function parseServerMetadata(extension) {
|
|
372
|
+
const authMode = parseOptional({ value: extension.authMode, parse: (mode) => mode === "oauth" || mode === "none" ? mode : null });
|
|
373
|
+
const defaultTools = parseOptional({ value: extension.defaultTools, parse: parseDefaultTools });
|
|
374
|
+
const tokenAuth = parseOptional({ value: extension.tokenAuth, parse: parseTokenAuth });
|
|
375
|
+
const renamedTools = parseOptional({ value: extension.renamedTools, parse: parseRenamedTools });
|
|
376
|
+
if (authMode === null || defaultTools === null || tokenAuth === null || renamedTools === null)
|
|
363
377
|
return null;
|
|
364
|
-
return {
|
|
365
|
-
type,
|
|
366
|
-
url: raw.url,
|
|
367
|
-
...(headers !== undefined ? { headers } : {}),
|
|
368
|
-
...(authMode !== undefined ? { authMode } : {}),
|
|
369
|
-
...(defaultTools !== undefined ? { defaultTools } : {}),
|
|
370
|
-
...(tokenAuth !== undefined ? { tokenAuth } : {}),
|
|
371
|
-
...(renamedTools !== undefined ? { renamedTools } : {}),
|
|
372
|
-
};
|
|
378
|
+
return definedFields({ authMode, defaultTools, tokenAuth, renamedTools });
|
|
373
379
|
}
|
|
374
380
|
/**
|
|
375
|
-
* Dispatches one
|
|
376
|
-
*
|
|
381
|
+
* Dispatches one `mcpServers` object entry to its transport's validator by `type`, or `null` for a
|
|
382
|
+
* `type` outside {@link MCP_SERVER_TRANSPORTS}.
|
|
377
383
|
*/
|
|
378
384
|
function parseMcpServerConfig(value, extension) {
|
|
379
|
-
if (!isJsonObject(value))
|
|
380
|
-
return null;
|
|
381
385
|
if (value.type === "stdio")
|
|
382
386
|
return parseStdioServerConfig(value);
|
|
383
387
|
if (value.type === "streamable-http" || value.type === "sse")
|
|
384
388
|
return parseRemoteServerConfig(value.type, value, extension);
|
|
385
389
|
return null;
|
|
386
390
|
}
|
|
391
|
+
/** One declared server, or `null` for a non-object entry, a malformed extension, or an invalid transport shape. */
|
|
392
|
+
function parseDeclaredServer({ serverId, rawServer, extension }, { readServerMetadata } = {}) {
|
|
393
|
+
// A malformed declared extension excludes the server before the translator can hide it.
|
|
394
|
+
if (!isJsonObject(rawServer) || (extension !== undefined && !isJsonObject(extension)))
|
|
395
|
+
return null;
|
|
396
|
+
const metadata = readServerMetadata?.({ serverId, value: extension ?? {}, server: rawServer }) ?? extension;
|
|
397
|
+
return parseMcpServerConfig(rawServer, metadata);
|
|
398
|
+
}
|
|
399
|
+
/** `mcp.json`'s top-level errors: its own `$schema`, a version mismatch with `plugin.json` (§10), and `mcpServers`' shape. */
|
|
400
|
+
function collectMcpConfigErrors({ raw }, { pluginManifest } = {}) {
|
|
401
|
+
const errors = [];
|
|
402
|
+
// plugin.json already reports an unknown-1.x warning; mcp.json adds none of its own.
|
|
403
|
+
const mcpSpec = readSpecVersion(raw.$schema, "mcp");
|
|
404
|
+
const mcpVersion = mcpSpec.version;
|
|
405
|
+
const pluginVersion = isJsonObject(pluginManifest) ? readSpecVersion(pluginManifest.$schema, "plugin").version : undefined;
|
|
406
|
+
if (mcpSpec.error)
|
|
407
|
+
errors.push(mcpSpec.error);
|
|
408
|
+
else if (pluginVersion !== undefined && pluginVersion !== mcpVersion) {
|
|
409
|
+
errors.push(`mcp.json targets Agent Plugins ${mcpVersion} but plugin.json targets ${pluginVersion}; the versions must match`);
|
|
410
|
+
}
|
|
411
|
+
if (!isJsonObject(raw.mcpServers))
|
|
412
|
+
errors.push("mcp.json 'mcpServers' is required and must be an object");
|
|
413
|
+
return errors;
|
|
414
|
+
}
|
|
415
|
+
/** The per-server metadata `plugin.json` declares under `extensionNamespace`, or `{}` when it declares none. */
|
|
416
|
+
function readExtensionServers({ pluginManifest, extensionNamespace }) {
|
|
417
|
+
const manifest = parseAgentPluginManifest({ value: pluginManifest });
|
|
418
|
+
if (!manifest.ok)
|
|
419
|
+
return {};
|
|
420
|
+
return readAgentPluginExtension({ manifest: manifest.manifest, namespace: extensionNamespace, read: ({ value }) => isJsonObject(value.mcpServers) ? value.mcpServers : {} }) ?? {};
|
|
421
|
+
}
|
|
387
422
|
/**
|
|
388
423
|
* Validates an already-`JSON.parse`d `mcp.json` value against the 1.0.0/1.1.0 top-level shape:
|
|
389
424
|
* `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }`, then validates each declared
|
|
@@ -398,35 +433,15 @@ export function parseAgentPluginMcpConfig({ value, extensionNamespace }, optiona
|
|
|
398
433
|
throw new Error("An extension namespace is required");
|
|
399
434
|
if (!isJsonObject(value))
|
|
400
435
|
return { ok: false, errors: ["mcp.json must be a JSON object"] };
|
|
401
|
-
const
|
|
402
|
-
const errors = [];
|
|
403
|
-
// plugin.json already reports an unknown-1.x warning; mcp.json adds none of its own.
|
|
404
|
-
const mcpSpec = readSpecVersion(raw.$schema, "mcp");
|
|
405
|
-
const mcpVersion = mcpSpec.version;
|
|
406
|
-
const pluginVersion = isJsonObject(optional.pluginManifest) ? readSpecVersion(optional.pluginManifest.$schema, "plugin").version : undefined;
|
|
407
|
-
if (mcpSpec.error)
|
|
408
|
-
errors.push(mcpSpec.error);
|
|
409
|
-
else if (pluginVersion !== undefined && pluginVersion !== mcpVersion) {
|
|
410
|
-
errors.push(`mcp.json targets Agent Plugins ${mcpVersion} but plugin.json targets ${pluginVersion}; the versions must match`);
|
|
411
|
-
}
|
|
412
|
-
const mcpServers = raw.mcpServers;
|
|
413
|
-
if (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers)) {
|
|
414
|
-
errors.push("mcp.json 'mcpServers' is required and must be an object");
|
|
415
|
-
}
|
|
436
|
+
const errors = collectMcpConfigErrors({ raw: value }, { pluginManifest: optional.pluginManifest });
|
|
416
437
|
if (errors.length > 0)
|
|
417
438
|
return { ok: false, errors };
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
const extensionServers = extension ?? {};
|
|
421
|
-
const entries = Object.entries(mcpServers);
|
|
439
|
+
const extensionServers = readExtensionServers({ pluginManifest: optional.pluginManifest, extensionNamespace });
|
|
440
|
+
const entries = Object.entries(value.mcpServers);
|
|
422
441
|
const servers = Object.create(null);
|
|
423
442
|
for (const [serverId, rawServer] of entries) {
|
|
424
443
|
const extension = Object.hasOwn(extensionServers, serverId) ? extensionServers[serverId] : undefined;
|
|
425
|
-
|
|
426
|
-
const metadata = isJsonObject(rawServer) && (extension === undefined || isJsonObject(extension))
|
|
427
|
-
? optional.readServerMetadata?.({ serverId, value: extension ?? {}, server: rawServer }) ?? extension
|
|
428
|
-
: undefined;
|
|
429
|
-
const parsed = extension !== undefined && !isJsonObject(extension) ? null : parseMcpServerConfig(rawServer, metadata);
|
|
444
|
+
const parsed = parseDeclaredServer({ serverId, rawServer, extension }, { readServerMetadata: optional.readServerMetadata });
|
|
430
445
|
if (parsed)
|
|
431
446
|
servers[serverId] = parsed;
|
|
432
447
|
}
|
|
@@ -81,10 +81,10 @@ function buildModule(ports) {
|
|
|
81
81
|
async function assertContainedOnDisk(packageRoot, entryPath) {
|
|
82
82
|
const normalized = normalizePackageEntryPath(entryPath);
|
|
83
83
|
const realRoot = await realpath(packageRoot);
|
|
84
|
+
// No separate lexical-escape check: normalizePackageEntryPath already refused absolute, drive-
|
|
85
|
+
// letter and `..`-leading paths, so resolving its relative output always stays under realRoot,
|
|
86
|
+
// and the filesystem-resolved check below would refuse any escape regardless.
|
|
84
87
|
const lexicalCandidate = path.resolve(realRoot, normalized);
|
|
85
|
-
if (!isWithin(realRoot, lexicalCandidate)) {
|
|
86
|
-
throw new PackagePathViolation({ message: `package entry resolves outside the package root: '${entryPath}'` });
|
|
87
|
-
}
|
|
88
88
|
let realCandidate;
|
|
89
89
|
try {
|
|
90
90
|
realCandidate = await realpath(lexicalCandidate);
|
|
@@ -4,7 +4,7 @@ import type { AgentPluginLayoutPort } from './layout.js';
|
|
|
4
4
|
import type { AgentPluginDeliveryMode, InstalledAgentPlugin } from './types.js';
|
|
5
5
|
/** Native Node filesystem ABI, narrowed to the operations used by lifecycle.
|
|
6
6
|
* Inject instrumented fs/promises effects directly; native handles are never rewrapped. */
|
|
7
|
-
export type FilesystemPort = Pick<typeof import('node:fs/promises'), 'chmod' | 'mkdir' | 'mkdtemp' | 'open' | 'readdir' | 'readFile' | 'rename' | 'rm' | 'rmdir' | 'stat' | 'realpath' | 'unlink'>;
|
|
7
|
+
export type FilesystemPort = Pick<typeof import('node:fs/promises'), 'chmod' | 'mkdir' | 'mkdtemp' | 'open' | 'readdir' | 'readFile' | 'rename' | 'rm' | 'rmdir' | 'stat' | 'lstat' | 'realpath' | 'unlink'>;
|
|
8
8
|
export type AgentPluginFetchPort = (required: {
|
|
9
9
|
readonly url: string;
|
|
10
10
|
}, optional?: RequestInit) => Promise<Response>;
|