@jini-ai/agent-plugins 0.4.2 → 0.4.4
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 +14 -0
- package/README.md +3 -3
- package/dist/lifecycle/bundled-digests.js +4 -0
- package/dist/lifecycle/install.d.ts +1 -1
- package/dist/lifecycle/install.js +115 -38
- package/dist/lifecycle/installed-package-roots.d.ts +16 -0
- package/dist/lifecycle/installed-package-roots.js +55 -0
- package/dist/lifecycle/manifest.d.ts +11 -59
- package/dist/lifecycle/manifest.js +70 -27
- package/dist/lifecycle/persistent-state.d.ts +4 -0
- package/dist/lifecycle/persistent-state.js +2 -0
- package/dist/lifecycle/ports.d.ts +8 -2
- package/dist/lifecycle/resolve-agent-plugin-refs.js +7 -36
- package/dist/lifecycle/search.d.ts +2 -0
- package/dist/lifecycle/search.js +2 -2
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +3 -2
- package/package.json +5 -8
- package/ui-ux-design/plugin.json +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.4 — 2026-10-12 — Agent Plugins 1.1.0
|
|
4
|
+
|
|
5
|
+
- `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` (and so the installer and MCP
|
|
6
|
+
projection) select the grammar by `$schema` and accept Agent Plugins 1.1.0 identifiers alongside
|
|
7
|
+
1.0.0. 1.1.0 changes only the version identifiers, so both map to the same validator; 1.0.0
|
|
8
|
+
behavior is unchanged. When a `plugin.json` is supplied, `mcp.json` must target the same version
|
|
9
|
+
(§10).
|
|
10
|
+
- Forward-compatible within major 1 (owner decision): an unknown 1.x (e.g. 1.2.0) is validated with
|
|
11
|
+
the newest known (1.1.0) grammar and reports one warning naming the version. A different major
|
|
12
|
+
(2.x, 0.x) is refused as an unsupported major version; a non-canonical `$schema` is refused with
|
|
13
|
+
the accepted identifiers listed.
|
|
14
|
+
|
|
3
15
|
## 0.4.2
|
|
4
16
|
|
|
17
|
+
- Use shared plugin identity metadata with a required `@jini-ai/core` peer at `^0.4.2`. Repair identity-lock publication syntax and isolate unreadable or invalid installed manifests with warnings so one damaged install cannot block healthy installs or bundled seeding.
|
|
18
|
+
|
|
5
19
|
### Patch Changes
|
|
6
20
|
|
|
7
21
|
- Tovu clean-up release (2026-10-08): code Tovu moved into Jini, plus the Jini clean-up (see the commit log since 0.4.1).
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> **Leon, this is you from the past. Do not try to merge `plugins` and `agent-plugins`.**
|
|
4
4
|
|
|
5
|
-
Support for **Agent Plugins**, an open, vendor-neutral spec (v1.0.0, published 2026-08-06
|
|
6
|
-
bundling Agent Skills and MCP servers into one portable
|
|
7
|
-
Steering Committee with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel.
|
|
5
|
+
Support for **Agent Plugins**, an open, vendor-neutral spec (v1.0.0, published 2026-08-06; v1.1.0 and
|
|
6
|
+
later 1.x manifests are also accepted) for bundling Agent Skills and MCP servers into one portable
|
|
7
|
+
directory. Published by a Technical Steering Committee with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel.
|
|
8
8
|
Source: https://developers.googleblog.com/agent-plugins-package-your-skills-tools-and-more/
|
|
9
9
|
|
|
10
10
|
If you are an agent reading this cold: this document is written so you don't need the source
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* ships as — written by `seed-bundled.ts` on every boot, read by the surfaces that have to pick one
|
|
4
4
|
* installed package for a plugin id.
|
|
5
5
|
*
|
|
6
|
+
* Current install policy (frpid, 2026-10-09): successful publication prunes superseded revisions.
|
|
7
|
+
* The rationale below describes retained legacy stores and interrupted cleanup. This ledger still
|
|
8
|
+
* selects the build's authoritative digest there; the ledger itself never deletes package bytes.
|
|
9
|
+
*
|
|
6
10
|
* ---------------------------------------------------------------------------
|
|
7
11
|
* The gap this closes
|
|
8
12
|
* ---------------------------------------------------------------------------
|
|
@@ -5,7 +5,7 @@ export type { InstalledAgentPlugin, InstalledAgentPluginSkill } from "./types.js
|
|
|
5
5
|
/** One extraction/install failure reason. A caller (an admin route, a future marketplace installer)
|
|
6
6
|
* branches on `code` rather than parsing `message` — this codebase's own convention
|
|
7
7
|
* (`PluginLoadError.reason` in `plugin-runtime/loader.ts` is the direct precedent). */
|
|
8
|
-
export type AgentPluginInstallErrorCode = "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
|
|
8
|
+
export type AgentPluginInstallErrorCode = "PLUGIN_ID_CONFLICT" | "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
|
|
9
9
|
export declare class AgentPluginInstallError extends Error {
|
|
10
10
|
readonly code: AgentPluginInstallErrorCode;
|
|
11
11
|
constructor({ code, message }: {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { listInstalledPackageRoots } from "./installed-package-roots.js";
|
|
2
|
+
import { createUninstallModule } from "./uninstall.js";
|
|
3
|
+
import { readPluginIdentity, assertPluginIdentityAvailable } from "@jini-ai/core/plugin-directory";
|
|
1
4
|
/**
|
|
2
5
|
* @file `installAgentPlugin()` — content-addressed, adversarially-hardened extraction of one Agent
|
|
3
6
|
* Plugin (agent-plugins.org) archive into one workspace's own `AgentPluginWorkspaceLayout.packages`.
|
|
@@ -95,7 +98,7 @@ function buildModule(ports) {
|
|
|
95
98
|
const rename = ports.filesystem.rename.bind(ports.filesystem);
|
|
96
99
|
const rm = ports.filesystem.rm.bind(ports.filesystem);
|
|
97
100
|
const stat = ports.filesystem.stat.bind(ports.filesystem);
|
|
98
|
-
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value });
|
|
101
|
+
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value }, { identityNamespace: ports.extensionNamespace });
|
|
99
102
|
const { assertContainedOnDisk, normalizePackageEntryPath } = createPackagePathsModule(ports);
|
|
100
103
|
const LIMITS = {
|
|
101
104
|
maxArchiveBytes: 32 * 1024 * 1024,
|
|
@@ -103,6 +106,63 @@ function buildModule(ports) {
|
|
|
103
106
|
maxFileBytes: 16 * 1024 * 1024,
|
|
104
107
|
maxTotalExtractedBytes: 64 * 1024 * 1024,
|
|
105
108
|
};
|
|
109
|
+
/** Publisher ids are metadata, never filesystem segments. The workspace identity lock
|
|
110
|
+
* serializes installs under different local names competing for one qualified id.
|
|
111
|
+
* @complexity O(d) manifest reads for d installed revisions; no executable imports. */
|
|
112
|
+
async function assertIdentityAvailable(workspaceRoot, candidate) {
|
|
113
|
+
// Installed roots are canonical paths; compare against the same root even when the host
|
|
114
|
+
// supplied an OS alias (macOS /var -> /private/var). Containment still rejects escaped links.
|
|
115
|
+
workspaceRoot = await ports.filesystem.realpath(workspaceRoot);
|
|
116
|
+
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
117
|
+
const manifestPath = path.join(root.packageRoot, 'plugin.json');
|
|
118
|
+
await persistentState.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, manifestPath) });
|
|
119
|
+
let raw;
|
|
120
|
+
let identity;
|
|
121
|
+
try {
|
|
122
|
+
raw = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
123
|
+
identity = readPluginIdentity({ manifest: raw, name: root.localId, namespace: ports.extensionNamespace });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// A damaged unrelated install must not block fresh installs or bundled seeding.
|
|
127
|
+
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message: `Plugin '${root.localId}': installed identity manifest is unreadable or invalid; omitted from identity admission.` });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
assertPluginIdentityAvailable({ candidate: { id: candidate.id, localId: candidate.pluginId, family: 'agent-plugin' },
|
|
132
|
+
installed: [{ ...identity, localId: root.localId, family: 'agent-plugin', version: typeof raw.version === 'string' ? raw.version : null, enabled: false }] });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new AgentPluginInstallError({ code: 'PLUGIN_ID_CONFLICT', message: 'The publisher-qualified plugin id conflicts with an installed package.' });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Retires only this plugin's superseded revisions after accepted publication, under the same
|
|
140
|
+
* identity/plugin locks. Persistent data, learned facts and notes never enter this operation.
|
|
141
|
+
* Reuse uninstall's frozen-tree staging so an interrupted removal is no longer a live digest.
|
|
142
|
+
* There is no previous-revision rollback slot in this lifecycle; failed publication keeps all
|
|
143
|
+
* prior bytes. Cleanup faults warn without turning an accepted install into a failed install.
|
|
144
|
+
* @complexity O(d + f), d workspace revisions, f files in this plugin's superseded packages.
|
|
145
|
+
*/
|
|
146
|
+
async function pruneSupersededRevisions({ workspaceRoot, installed }, _optional = {}) {
|
|
147
|
+
try {
|
|
148
|
+
// Resolve lazily: uninstall's read-side index uses this install module for manifest indexing.
|
|
149
|
+
const removal = createUninstallModule(ports);
|
|
150
|
+
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
151
|
+
if (root.localId !== installed.pluginId || root.archiveDigest === installed.archiveDigest)
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
const staged = await removal.stageForRemoval(path.dirname(root.packageRoot), root.packageRoot);
|
|
155
|
+
await removal.removeFrozenPackageTree(staged.quarantined);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
ports.onEvent?.({ event: 'agent-plugin-revision-prune-warning', message: `Plugin '${installed.pluginId}': a superseded revision could not be pruned.` });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
ports.onEvent?.({ event: 'agent-plugin-revision-prune-warning', message: `Plugin '${installed.pluginId}': superseded revisions could not be scanned for pruning.` });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
106
166
|
/** Exposes `LIMITS.maxArchiveBytes` for a caller outside this module — concretely, a test asserting
|
|
107
167
|
* `@jini-ai/agent-plugins/lifecycle`'s fetch cap constant stays pinned to this one. The local fetch-archive.ts fork was deleted; see the package's fetch module
|
|
108
168
|
* for why it duplicates the number instead of importing it (no runtime dependency on `install.ts`);
|
|
@@ -150,43 +210,56 @@ function buildModule(ports) {
|
|
|
150
210
|
const indexed = await indexInstalledRoot(extractionRoot, digest);
|
|
151
211
|
if (optional.previewOnly)
|
|
152
212
|
return indexed;
|
|
153
|
-
return await persistentState.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
return await persistentState.withIdentityLock({ workspaceRoot: workspaceLayout.root, run: async () => {
|
|
214
|
+
await assertIdentityAvailable(workspaceLayout.root, indexed);
|
|
215
|
+
const publishWithinPluginLock = () => persistentState.withPluginLock({ workspaceRoot: workspaceLayout.root, pluginId: indexed.pluginId, run: async () => {
|
|
216
|
+
// Layout B needs the manifest id before choosing the package destination. Dedup still
|
|
217
|
+
// skips publication and freezing, after one bounded staging extraction.
|
|
218
|
+
// Owner decision 2026-10-04: learned facts begin empty for each site; author guidance stays
|
|
219
|
+
// in package docs. Package extension files never seed learned memory or the user's notes.
|
|
220
|
+
// Content-addressed dedup, scoped to THIS workspace's own tree (`workspaceLayout` was just
|
|
221
|
+
// resolved above, atomically, from this same `workspaceId`): identical bytes were already
|
|
222
|
+
// extracted, verified, and frozen by a prior install of this same workspace's — never
|
|
223
|
+
// re-extracted for a different workspace, by construction, since a different workspace's
|
|
224
|
+
// `workspaceLayout.packages` is a different path entirely. one bounded staging extraction precedes this check.
|
|
225
|
+
// Publish BEFORE freezing permissions, not after: `rename()` moves `extractionRoot` as a single
|
|
226
|
+
// directory-entry operation, and on this filesystem renaming an already-read-only (0o555)
|
|
227
|
+
// directory itself fails EACCES (observed, not merely theoretical — see the regression test this
|
|
228
|
+
// ordering fixed). Freezing after publish also closes a smaller race for free: nothing outside
|
|
229
|
+
// this function can observe the package at its live path while it is still writable, because the
|
|
230
|
+
// live path does not exist until `publish` returns.
|
|
231
|
+
let publishedPlugin;
|
|
232
|
+
const publishPackage = async () => {
|
|
233
|
+
// The manifest identifies the plugin only after bounded staging extraction (Layout B).
|
|
234
|
+
const pluginId = indexed.pluginId;
|
|
235
|
+
const packagesDir = workspaceLayout.pluginPackagesDir({ pluginId });
|
|
236
|
+
await persistentState.assertOwnedPath({ workspaceRoot: workspaceLayout.root, entryPath: path.relative(workspaceLayout.root, packagesDir) });
|
|
237
|
+
await mkdir(packagesDir, { recursive: true, mode: 0o700 });
|
|
238
|
+
const finalRoot = await assertContainedOnDisk(packagesDir, digest);
|
|
239
|
+
for (const directory of [workspaceLayout.pluginDataDir({ pluginId }),
|
|
240
|
+
workspaceLayout.pluginMemoryDir({ pluginId, kind: 'learned' }),
|
|
241
|
+
workspaceLayout.pluginMemoryDir({ pluginId, kind: 'notes' })]) {
|
|
242
|
+
await persistentState.assertOwnedPath({ workspaceRoot: workspaceLayout.root, entryPath: path.relative(workspaceLayout.root, directory) });
|
|
243
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
244
|
+
}
|
|
245
|
+
if (await isRealDirectory(finalRoot)) {
|
|
246
|
+
publishedPlugin = await indexInstalledRoot(finalRoot, digest);
|
|
247
|
+
return publishedPlugin;
|
|
248
|
+
}
|
|
249
|
+
await publish(extractionRoot, finalRoot);
|
|
250
|
+
await freezeTree(finalRoot, executablePaths);
|
|
251
|
+
/** Absolute, read-only (frozen) path to the extracted package root. */
|
|
252
|
+
publishedPlugin = { ...indexed, packageRoot: finalRoot };
|
|
253
|
+
return publishedPlugin;
|
|
254
|
+
};
|
|
255
|
+
const installed = await (optional.publishGuard ? optional.publishGuard({ plugin: indexed, publish: publishPackage }, {}) : publishPackage());
|
|
256
|
+
if (publishedPlugin)
|
|
257
|
+
await pruneSupersededRevisions({ workspaceRoot: workspaceLayout.root, installed: publishedPlugin });
|
|
258
|
+
return installed;
|
|
259
|
+
} });
|
|
260
|
+
if (ports.identityAdmission)
|
|
261
|
+
return ports.identityAdmission({ id: indexed.id, localId: indexed.pluginId, run: publishWithinPluginLock }, {});
|
|
262
|
+
return publishWithinPluginLock();
|
|
190
263
|
} });
|
|
191
264
|
}
|
|
192
265
|
finally {
|
|
@@ -379,6 +452,8 @@ function buildModule(ports) {
|
|
|
379
452
|
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
|
|
380
453
|
}
|
|
381
454
|
const files = [];
|
|
455
|
+
for (const message of parsed.warnings)
|
|
456
|
+
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
|
|
382
457
|
const skills = [];
|
|
383
458
|
async function walk(absolute, prefix) {
|
|
384
459
|
for (const entry of await readdir(absolute, { withFileTypes: true })) {
|
|
@@ -401,6 +476,8 @@ function buildModule(ports) {
|
|
|
401
476
|
await walk(packageRoot, "");
|
|
402
477
|
return {
|
|
403
478
|
pluginId: parsed.manifest.name,
|
|
479
|
+
id: parsed.manifest.id,
|
|
480
|
+
provides: parsed.manifest.provides,
|
|
404
481
|
version: parsed.manifest.version,
|
|
405
482
|
...( /**
|
|
406
483
|
* `plugin.json`'s own `description`/`keywords`/`author`/`license` (`manifest.ts`'s
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentPluginLifecyclePorts } from './ports.js';
|
|
2
|
+
export declare const INSTALLED_PLUGIN_DIGEST_PATTERN: RegExp;
|
|
3
|
+
export interface InstalledPackageRoot {
|
|
4
|
+
readonly localId: string;
|
|
5
|
+
readonly archiveDigest: string;
|
|
6
|
+
readonly packageRoot: string;
|
|
7
|
+
}
|
|
8
|
+
/** Enumerates contained installed revisions; missing stores are empty. A corrupt slot is
|
|
9
|
+
* isolated, matching the full installed-plugin index's existing recovery contract.
|
|
10
|
+
* @complexity O(d) directory/containment operations for d revisions; no executable imports.
|
|
11
|
+
*/
|
|
12
|
+
export declare function listInstalledPackageRoots({ ports, workspaceRoot }: {
|
|
13
|
+
ports: AgentPluginLifecyclePorts;
|
|
14
|
+
workspaceRoot: string;
|
|
15
|
+
}, _optional?: Record<string, never>): Promise<readonly InstalledPackageRoot[]>;
|
|
16
|
+
//# sourceMappingURL=installed-package-roots.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The installed-package walk shared by full indexing and identity admission. Reusing this
|
|
2
|
+
* traversal preserves the same digest grammar, memory-folder exclusion and containment checks. */
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createPersistentStateModule } from './persistent-state.js';
|
|
5
|
+
import { createPackagePathsModule } from './package-paths.js';
|
|
6
|
+
export const INSTALLED_PLUGIN_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
7
|
+
/** Enumerates contained installed revisions; missing stores are empty. A corrupt slot is
|
|
8
|
+
* isolated, matching the full installed-plugin index's existing recovery contract.
|
|
9
|
+
* @complexity O(d) directory/containment operations for d revisions; no executable imports.
|
|
10
|
+
*/
|
|
11
|
+
export async function listInstalledPackageRoots({ ports, workspaceRoot }, _optional = {}) {
|
|
12
|
+
const persistentState = createPersistentStateModule(ports);
|
|
13
|
+
const { assertContainedOnDisk } = createPackagePathsModule(ports);
|
|
14
|
+
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
15
|
+
let folders;
|
|
16
|
+
try {
|
|
17
|
+
folders = await readdir(workspaceRoot, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (isEnoent(error))
|
|
21
|
+
return [];
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const roots = [];
|
|
25
|
+
for (const folder of folders) {
|
|
26
|
+
if (!folder.isDirectory() || !/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(folder.name) || folder.name.length > 64)
|
|
27
|
+
continue;
|
|
28
|
+
const packagesDir = path.join(workspaceRoot, folder.name, 'package', 'sha256');
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
await persistentState.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, packagesDir) });
|
|
32
|
+
entries = await readdir(packagesDir, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
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
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return roots;
|
|
50
|
+
}
|
|
51
|
+
/** Only a genuinely missing store is empty; other filesystem faults propagate unchanged. */
|
|
52
|
+
function isEnoent(error) {
|
|
53
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=installed-package-roots.js.map
|
|
@@ -1,62 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` — the Agent Plugins v1.0.0
|
|
3
|
-
* `plugin.json` / `mcp.json` grammar (agent-plugins.org/specification), verified against the live
|
|
4
|
-
* spec rather than inferred from an example manifest.
|
|
5
|
-
*
|
|
6
|
-
* Deliberately a SEPARATE, smaller validator from `src/features/plugin-runtime/manifest.ts`'s
|
|
7
|
-
* `validateManifest()`. That file validates `.host-plugin`'s own tarball format — `integrity`
|
|
8
|
-
* (per-file SHA-256), `sdkRange`, `tier`, an exactly-three-token capability vocabulary, a single
|
|
9
|
-
* `content.entry.beforeSave` hook. This file validates the OPEN standard's `plugin.json`, which
|
|
10
|
-
* shares none of those fields and defines no execution/trust/capability model at all — see this
|
|
11
|
-
* feature's own header comment in `install.ts` for why the two formats cannot be unified.
|
|
12
|
-
*
|
|
13
|
-
* Pinned v1.0.0 rules from agent-plugins.org/specification:
|
|
14
|
-
* - `plugin.json`'s `name`: 1-64 chars, lowercase alphanumeric/hyphen/period only, must start and
|
|
15
|
-
* end alphanumeric, no `--` or `..` consecutive delimiters (§5.5). Valid: `my-plugin`,
|
|
16
|
-
* `acme.tools`, `lint3r`, `a`. Invalid: `My-Plugin`, `-start`, `has--double`.
|
|
17
|
-
* - `plugin.json`'s `$schema` MUST equal `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`
|
|
18
|
-
* for this pinned version; `mcp.json`'s MUST equal
|
|
19
|
-
* `https://agent-plugins.org/schemas/1.0.0/mcp.schema.json`.
|
|
20
|
-
* - Unknown top-level `plugin.json` fields produce warnings but do not block loading — this module
|
|
21
|
-
* returns them as `warnings`, never as a rejection reason.
|
|
22
|
-
* - `mcp.json`'s top-level shape is `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }` —
|
|
23
|
-
* `mcpServers` MUST be an object whose member names identify servers.
|
|
24
|
-
* - Each server entry MUST carry a `type` discriminator matching one of three CLOSED variants.
|
|
25
|
-
* Launching requires the actual transport config, not just a server id:
|
|
26
|
-
* - `stdio`: requires `command` (non-empty string); optional `args` (string[]), `env`
|
|
27
|
-
* (string-valued object — MUST NOT set `PLUGIN_ROOT`/`PLUGIN_DATA`, which the spec reserves),
|
|
28
|
-
* `cwd` (plugin-relative path).
|
|
29
|
-
* - `streamable-http` / `sse`: requires `url` (non-empty string); optional `headers`
|
|
30
|
-
* (string-valued object).
|
|
31
|
-
* - **The spec defines NO auth-related field on a server entry at all.** Verbatim from
|
|
32
|
-
* `/specification`: "Agent Plugins v1 defines no OAuth configuration or portable
|
|
33
|
-
* credential-reference fields. Authorization discovery, user interaction, and credential storage
|
|
34
|
-
* are client-managed." A plugin therefore cannot declare "this server needs OAuth" using any
|
|
35
|
-
* spec-defined field — the host is the client the spec defers that decision to. This module accepts
|
|
36
|
-
* the host-specific metadata in `plugin.json`'s `extensions.<host-namespace>.mcpServers[serverId]`, keeping
|
|
37
|
-
* `mcp.json` transport entries closed per §7.2.1. Hosts may translate legacy auth and sign-in defaults in MCP
|
|
38
|
-
* entries through readServerMetadata for existing packages, but those cannot supply operator read trust.
|
|
39
|
-
* `authMode: "oauth" | "none"` selects client auth; absent defaults to "none".
|
|
40
|
-
* `defaultTools: { allow?, write?, read? }` names sign-in grants and reviewed read tools.
|
|
41
|
-
* `write` requires `allow` and must be its subset; `read` must be a subset when `allow` is
|
|
42
|
-
* present. A read-only declaration without `allow` leaves sign-in grants unchanged.
|
|
43
|
-
* Remote hints may veto a read declaration but never supply it. A third,
|
|
44
|
-
* `tokenAuth: { helpUrl, probeUrl }`, offers a pasted access token as the sign-in fallback
|
|
45
|
-
* (`access-token-tool.ts`). A fourth, `renamedTools: { oldName: newName }`, carries saved tool
|
|
46
|
-
* selections across a vendor's tool rename (`apply-tool-renames.ts`). A malformed per-server entry
|
|
47
|
-
* (wrong/missing `type`, missing required field) does not fail the whole file — see
|
|
48
|
-
* `ParseAgentPluginMcpConfigResult`'s own doc for the fail-open contract this preserves.
|
|
49
|
-
*
|
|
50
|
-
* Architectural role:
|
|
51
|
-
* Pure, no-I/O parsing over an already-`JSON.parse`d value — mirrors `plugin-runtime/manifest.ts`'s
|
|
52
|
-
* own "receives a JS value, never a file path or raw bytes" contract. `install.ts` is the only
|
|
53
|
-
* caller that reads the file and hands the parsed value here.
|
|
54
|
-
*/
|
|
55
1
|
export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
|
|
56
2
|
export type { PluginManifest, McpManifest, McpServerEntry } from '../manifest.js';
|
|
57
3
|
import type { PluginManifestAuthor } from '../manifest.js';
|
|
58
4
|
export type { PluginManifestAuthor } from '../manifest.js';
|
|
59
|
-
/** Agent Plugins 1.0.0 parsing, with application metadata confined to an explicit namespace. */
|
|
5
|
+
/** Agent Plugins 1.0.0/1.1.0 parsing, with application metadata confined to an explicit namespace. */
|
|
60
6
|
export type AgentPluginServerMetadataReader = (required: {
|
|
61
7
|
readonly serverId: string;
|
|
62
8
|
readonly value: Readonly<Record<string, unknown>>;
|
|
@@ -70,6 +16,9 @@ export interface AgentPluginManifest {
|
|
|
70
16
|
readonly license?: (string) | undefined;
|
|
71
17
|
readonly keywords?: (readonly string[]) | undefined;
|
|
72
18
|
readonly extensions?: (Readonly<Record<string, unknown>>) | undefined;
|
|
19
|
+
/** Normalized publisher identity, independent of name/storage key and version. */
|
|
20
|
+
readonly id?: string | undefined;
|
|
21
|
+
readonly provides?: readonly string[] | undefined;
|
|
73
22
|
}
|
|
74
23
|
export type ParseAgentPluginManifestResult = {
|
|
75
24
|
readonly ok: true;
|
|
@@ -80,7 +29,7 @@ export type ParseAgentPluginManifestResult = {
|
|
|
80
29
|
readonly errors: readonly string[];
|
|
81
30
|
};
|
|
82
31
|
/**
|
|
83
|
-
* Validates an already-`JSON.parse`d `plugin.json` value against the
|
|
32
|
+
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
84
33
|
*
|
|
85
34
|
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
86
35
|
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
@@ -89,9 +38,11 @@ export type ParseAgentPluginManifestResult = {
|
|
|
89
38
|
*/
|
|
90
39
|
export declare function parseAgentPluginManifest({ value }: {
|
|
91
40
|
readonly value: unknown;
|
|
41
|
+
}, { identityNamespace }?: {
|
|
42
|
+
readonly identityNamespace?: string;
|
|
92
43
|
}): ParseAgentPluginManifestResult;
|
|
93
44
|
/**
|
|
94
|
-
* The three closed transport variants `mcp.schema.json`
|
|
45
|
+
* The three closed transport variants `mcp.schema.json` 1.0.0/1.1.0 defines. See this file's header for
|
|
95
46
|
* the verified field shape of each.
|
|
96
47
|
*/
|
|
97
48
|
export declare const MCP_SERVER_TRANSPORTS: readonly ["stdio", "streamable-http", "sse"];
|
|
@@ -183,9 +134,10 @@ export type ParseAgentPluginMcpConfigResult = {
|
|
|
183
134
|
readonly errors: readonly string[];
|
|
184
135
|
};
|
|
185
136
|
/**
|
|
186
|
-
* Validates an already-`JSON.parse`d `mcp.json` value against the
|
|
137
|
+
* Validates an already-`JSON.parse`d `mcp.json` value against the 1.0.0/1.1.0 top-level shape:
|
|
187
138
|
* `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }`, then validates each declared
|
|
188
|
-
* server's own transport shape (see this file's header for the three variants).
|
|
139
|
+
* server's own transport shape (see this file's header for the three variants). When
|
|
140
|
+
* `pluginManifest` declares a supported version, `mcp.json` must declare the same one (§10).
|
|
189
141
|
*
|
|
190
142
|
* @throws Nothing — see {@link parseAgentPluginManifest}.
|
|
191
143
|
* @complexity O(s) in the number of declared servers.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` — the Agent Plugins
|
|
2
|
+
* @file `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` — the Agent Plugins 1.0.0/1.1.0
|
|
3
3
|
* `plugin.json` / `mcp.json` grammar (agent-plugins.org/specification), verified against the live
|
|
4
4
|
* spec rather than inferred from an example manifest.
|
|
5
5
|
*
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
* shares none of those fields and defines no execution/trust/capability model at all — see this
|
|
11
11
|
* feature's own header comment in `install.ts` for why the two formats cannot be unified.
|
|
12
12
|
*
|
|
13
|
-
* Pinned
|
|
13
|
+
* Pinned 1.0.0/1.1.0 rules from agent-plugins.org/specification:
|
|
14
14
|
* - `plugin.json`'s `name`: 1-64 chars, lowercase alphanumeric/hyphen/period only, must start and
|
|
15
15
|
* end alphanumeric, no `--` or `..` consecutive delimiters (§5.5). Valid: `my-plugin`,
|
|
16
16
|
* `acme.tools`, `lint3r`, `a`. Invalid: `My-Plugin`, `-start`, `has--double`.
|
|
17
|
-
* - `plugin.json`'s `$schema` MUST
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* - `plugin.json`'s `$schema` MUST be a recognized canonical identifier
|
|
18
|
+
* (`https://agent-plugins.org/schemas/<version>/plugin.schema.json`); `mcp.json`'s likewise
|
|
19
|
+
* (`.../<version>/mcp.schema.json`), and when both are present their versions MUST match (§10).
|
|
20
|
+
* 1.0.0 and 1.1.0 are recognized; an unknown 1.x is read with the newest known grammar plus one
|
|
21
|
+
* warning; any other major is refused — see `KNOWN_SPEC_VERSIONS`.
|
|
20
22
|
* - Unknown top-level `plugin.json` fields produce warnings but do not block loading — this module
|
|
21
23
|
* returns them as `warnings`, never as a rejection reason.
|
|
22
24
|
* - `mcp.json`'s top-level shape is `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }` —
|
|
@@ -52,13 +54,47 @@
|
|
|
52
54
|
* own "receives a JS value, never a file path or raw bytes" contract. `install.ts` is the only
|
|
53
55
|
* caller that reads the file and hands the parsed value here.
|
|
54
56
|
*/
|
|
57
|
+
import { readPluginIdentity, validatePluginIdentity } from "@jini-ai/core/plugin-directory";
|
|
55
58
|
export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
|
|
56
59
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
60
|
+
* The Agent Plugins versions whose schemas have been diffed against this file's grammar. 1.1.0's
|
|
61
|
+
* spec text and both schemas differ from 1.0.0 only in the version identifiers, so both map to the
|
|
62
|
+
* one grammar below — §5.1 allows mapping explicitly recognized compatible versions to one
|
|
63
|
+
* implementation. Add a version here only after diffing its schemas against this grammar.
|
|
59
64
|
*/
|
|
60
|
-
const
|
|
61
|
-
const
|
|
65
|
+
const KNOWN_SPEC_VERSIONS = ["1.0.0", "1.1.0"];
|
|
66
|
+
const SUPPORTED_MAJOR = "1";
|
|
67
|
+
/**
|
|
68
|
+
* Owner decision (2026-10-12): any other 1.x is accepted with the newest known grammar and one
|
|
69
|
+
* warning, so a minor spec release does not lock out packages that target it. This goes beyond
|
|
70
|
+
* §5.1, which says a client MUST reject a version it does not explicitly recognize. Unknown fields
|
|
71
|
+
* stay tolerated only where the known grammar already allows them (unknown top-level fields warn,
|
|
72
|
+
* `extensions` is opaque). A different major is refused, since majors may break the grammar.
|
|
73
|
+
*/
|
|
74
|
+
const SCHEMA_URL_PATTERN = /^https:\/\/agent-plugins\.org\/schemas\/((\d+)\.\d+\.\d+)\/(plugin|mcp)\.schema\.json$/;
|
|
75
|
+
/** The canonical `$schema` identifier for one spec version's `plugin.json` or `mcp.json`. */
|
|
76
|
+
function schemaUrl(version, file) {
|
|
77
|
+
return `https://agent-plugins.org/schemas/${version}/${file}.schema.json`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reads the spec version `value` names as `file`'s canonical schema identifier: a known version,
|
|
81
|
+
* an unknown 1.x (with its warning), or the error for a different major or a non-canonical value.
|
|
82
|
+
*/
|
|
83
|
+
function readSpecVersion(value, file) {
|
|
84
|
+
const match = typeof value === "string" ? SCHEMA_URL_PATTERN.exec(value) : null;
|
|
85
|
+
if (!match || match[3] !== file) {
|
|
86
|
+
const accepted = KNOWN_SPEC_VERSIONS.map((version) => `'${schemaUrl(version, file)}'`).join(" or ");
|
|
87
|
+
return { error: `${file}.json '$schema' must be ${accepted} (Agent Plugins ${KNOWN_SPEC_VERSIONS.join(", ")}, or another ${SUPPORTED_MAJOR}.x), got '${String(value)}'` };
|
|
88
|
+
}
|
|
89
|
+
const version = match[1];
|
|
90
|
+
if (match[2] !== SUPPORTED_MAJOR) {
|
|
91
|
+
return { error: `${file}.json targets Agent Plugins ${version}, an unsupported major version (supported: ${SUPPORTED_MAJOR}.x)` };
|
|
92
|
+
}
|
|
93
|
+
if (KNOWN_SPEC_VERSIONS.includes(version))
|
|
94
|
+
return { version };
|
|
95
|
+
const newest = KNOWN_SPEC_VERSIONS[KNOWN_SPEC_VERSIONS.length - 1];
|
|
96
|
+
return { version, warning: `${file}.json targets Agent Plugins ${version}, newer than this loader knows; validated with the ${newest} grammar` };
|
|
97
|
+
}
|
|
62
98
|
/**
|
|
63
99
|
* §5.5's grammar as one regex: alnum runs separated by single `-`/`.` delimiters. This shape alone
|
|
64
100
|
* guarantees start/end are alphanumeric (the regex can only start and end on `[a-z0-9]+`) AND that
|
|
@@ -84,13 +120,6 @@ function isJsonObject(value) {
|
|
|
84
120
|
function coerceOptionalString(value) {
|
|
85
121
|
return typeof value === "string" ? value : undefined;
|
|
86
122
|
}
|
|
87
|
-
/**
|
|
88
|
-
* `value` narrowed to a `string[]` (non-string entries dropped), or `undefined` when `value`
|
|
89
|
-
* itself isn't an array — `keywords`'s own coercion.
|
|
90
|
-
*/
|
|
91
|
-
function coerceStringArray(value) {
|
|
92
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : undefined;
|
|
93
|
-
}
|
|
94
123
|
function parseManifestAuthor(value) {
|
|
95
124
|
if (typeof value === 'string')
|
|
96
125
|
return value; // Existing installed manifests may carry a string.
|
|
@@ -120,22 +149,24 @@ function resolveManifestName(raw) {
|
|
|
120
149
|
return { name };
|
|
121
150
|
}
|
|
122
151
|
/**
|
|
123
|
-
* Validates an already-`JSON.parse`d `plugin.json` value against the
|
|
152
|
+
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
124
153
|
*
|
|
125
154
|
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
126
155
|
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
127
156
|
* `plugin-runtime/discovery.ts`).
|
|
128
157
|
* @complexity O(k + n) time and space in the input keys and keyword entries.
|
|
129
158
|
*/
|
|
130
|
-
export function parseAgentPluginManifest({ value }) {
|
|
159
|
+
export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu" } = {}) {
|
|
131
160
|
if (!isJsonObject(value))
|
|
132
161
|
return { ok: false, errors: ["plugin.json must be a JSON object"] };
|
|
133
162
|
const raw = value;
|
|
134
163
|
const errors = [];
|
|
135
164
|
const warnings = [];
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
const specVersion = readSpecVersion(raw.$schema, "plugin");
|
|
166
|
+
if (specVersion.error)
|
|
167
|
+
errors.push(specVersion.error);
|
|
168
|
+
else if (specVersion.warning)
|
|
169
|
+
warnings.push(specVersion.warning);
|
|
139
170
|
if (raw.extensions !== undefined && !isJsonObject(raw.extensions))
|
|
140
171
|
errors.push("plugin.json extensions must be an object");
|
|
141
172
|
const nameResult = resolveManifestName(raw);
|
|
@@ -145,21 +176,26 @@ export function parseAgentPluginManifest({ value }) {
|
|
|
145
176
|
if (!KNOWN_MANIFEST_KEYS.has(key))
|
|
146
177
|
warnings.push(`unrecognized plugin.json field '${key}' (ignored per spec)`);
|
|
147
178
|
}
|
|
179
|
+
errors.push(...validatePluginIdentity({ manifest: raw, namespace: identityNamespace }).errors);
|
|
148
180
|
if (errors.length > 0)
|
|
149
181
|
return { ok: false, errors };
|
|
182
|
+
const identity = readPluginIdentity({ manifest: raw, namespace: identityNamespace });
|
|
183
|
+
warnings.push(...identity.warnings);
|
|
150
184
|
const manifest = {
|
|
151
185
|
name: nameResult.name,
|
|
186
|
+
id: identity.id,
|
|
187
|
+
provides: identity.provides,
|
|
152
188
|
version: coerceOptionalString(raw.version),
|
|
153
189
|
description: coerceOptionalString(raw.description),
|
|
154
190
|
author: parseManifestAuthor(raw.author),
|
|
155
191
|
license: coerceOptionalString(raw.license),
|
|
156
|
-
keywords:
|
|
192
|
+
keywords: raw.keywords === undefined ? undefined : identity.keywords,
|
|
157
193
|
...(isJsonObject(raw.extensions) ? { extensions: raw.extensions } : {}),
|
|
158
194
|
};
|
|
159
195
|
return { ok: true, manifest, warnings };
|
|
160
196
|
}
|
|
161
197
|
/**
|
|
162
|
-
* The three closed transport variants `mcp.schema.json`
|
|
198
|
+
* The three closed transport variants `mcp.schema.json` 1.0.0/1.1.0 defines. See this file's header for
|
|
163
199
|
* the verified field shape of each.
|
|
164
200
|
*/
|
|
165
201
|
export const MCP_SERVER_TRANSPORTS = ["stdio", "streamable-http", "sse"];
|
|
@@ -349,9 +385,10 @@ function parseMcpServerConfig(value, extension) {
|
|
|
349
385
|
return null;
|
|
350
386
|
}
|
|
351
387
|
/**
|
|
352
|
-
* Validates an already-`JSON.parse`d `mcp.json` value against the
|
|
388
|
+
* Validates an already-`JSON.parse`d `mcp.json` value against the 1.0.0/1.1.0 top-level shape:
|
|
353
389
|
* `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }`, then validates each declared
|
|
354
|
-
* server's own transport shape (see this file's header for the three variants).
|
|
390
|
+
* server's own transport shape (see this file's header for the three variants). When
|
|
391
|
+
* `pluginManifest` declares a supported version, `mcp.json` must declare the same one (§10).
|
|
355
392
|
*
|
|
356
393
|
* @throws Nothing — see {@link parseAgentPluginManifest}.
|
|
357
394
|
* @complexity O(s) in the number of declared servers.
|
|
@@ -363,8 +400,14 @@ export function parseAgentPluginMcpConfig({ value, extensionNamespace }, optiona
|
|
|
363
400
|
return { ok: false, errors: ["mcp.json must be a JSON object"] };
|
|
364
401
|
const raw = value;
|
|
365
402
|
const errors = [];
|
|
366
|
-
|
|
367
|
-
|
|
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`);
|
|
368
411
|
}
|
|
369
412
|
const mcpServers = raw.mcpServers;
|
|
370
413
|
if (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers)) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AgentPluginLifecyclePorts } from './ports.js';
|
|
2
2
|
/** Reuses the platform lock with injected effects, never age-stealing a living migration. */
|
|
3
3
|
export declare function createPersistentStateModule(ports: AgentPluginLifecyclePorts, _optional?: Record<string, never>): {
|
|
4
|
+
withIdentityLock: <T>(required: {
|
|
5
|
+
workspaceRoot: string;
|
|
6
|
+
run(): Promise<T>;
|
|
7
|
+
}, _optional?: {}) => Promise<T>;
|
|
4
8
|
withPluginLock: <T>(required: {
|
|
5
9
|
workspaceRoot: string;
|
|
6
10
|
pluginId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createPluginMemory, migratePluginLayout, assertPluginStatePath, withPluginStateLock } from '../../core/persistent-state.js';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { withFileLock } from '@jini-ai/platform/fs/file-lock';
|
|
3
4
|
import { createPackagePathsModule } from './package-paths.js';
|
|
4
5
|
import { parseAgentPluginManifest } from './manifest.js';
|
|
@@ -16,6 +17,7 @@ export function createPersistentStateModule(ports, _optional = {}) {
|
|
|
16
17
|
}),
|
|
17
18
|
};
|
|
18
19
|
return {
|
|
20
|
+
withIdentityLock: (required, _optional = {}) => effects.withLock({ lockPath: path.join(required.workspaceRoot, '.publisher-identity.lock'), run: required.run }),
|
|
19
21
|
withPluginLock: (required, optional = {}) => withPluginStateLock({ ...effects, ...required }, optional),
|
|
20
22
|
memory: (required, optional = {}) => createPluginMemory({ ...effects, ...required }, optional),
|
|
21
23
|
assertOwnedPath: (required, optional = {}) => assertPluginStatePath({ ...effects, ...required }, optional),
|
|
@@ -52,6 +52,12 @@ export interface AgentPluginOutboundGuardPort {
|
|
|
52
52
|
}): Promise<void>;
|
|
53
53
|
}
|
|
54
54
|
export interface AgentPluginLifecyclePorts {
|
|
55
|
+
/** Optional host admission serializes cross-kind identity checking and publication. */
|
|
56
|
+
readonly identityAdmission?: (<T>(required: {
|
|
57
|
+
id: string;
|
|
58
|
+
localId: string;
|
|
59
|
+
run: () => Promise<T>;
|
|
60
|
+
}, optional?: Record<string, never>) => Promise<T>) | undefined;
|
|
55
61
|
readonly onEvent?: ((required: {
|
|
56
62
|
readonly event: string;
|
|
57
63
|
readonly message: string;
|
|
@@ -88,6 +94,6 @@ export interface AgentPluginLifecyclePorts {
|
|
|
88
94
|
readonly outboundGuard: AgentPluginOutboundGuardPort;
|
|
89
95
|
}
|
|
90
96
|
/** Application policy/effects are required; observers and metadata translators are optional. */
|
|
91
|
-
export type AgentPluginLifecycleRequired = Omit<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery'>;
|
|
92
|
-
export type AgentPluginLifecycleOptional = Pick<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery'>;
|
|
97
|
+
export type AgentPluginLifecycleRequired = Omit<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery' | 'identityAdmission'>;
|
|
98
|
+
export type AgentPluginLifecycleOptional = Pick<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery' | 'identityAdmission'>;
|
|
93
99
|
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { listInstalledPackageRoots, INSTALLED_PLUGIN_DIGEST_PATTERN } from "./installed-package-roots.js";
|
|
1
2
|
/**
|
|
2
3
|
* @file Resolves a run's pinned Agent Plugin refs (`pluginRefIds` — `run-start-context.ts`'s
|
|
3
4
|
* decoded `contextRef` field, itself sourced from the composer's `pluginRefId` chips,
|
|
@@ -65,13 +66,11 @@ import { createCapabilityProjectionModule } from "./capability-projection.js";
|
|
|
65
66
|
function buildModule(ports) {
|
|
66
67
|
const persistentState = createPersistentStateModule(ports);
|
|
67
68
|
const { assertContainedOnDisk } = createPackagePathsModule(ports);
|
|
68
|
-
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
69
69
|
const stat = ports.filesystem.stat.bind(ports.filesystem);
|
|
70
70
|
const { resolveAgentPluginActivation } = createActivationModule(ports);
|
|
71
71
|
const { preferBundledAgentPluginDigests, readBundledAgentPluginDigests } = createBundledDigestsModule(ports);
|
|
72
72
|
const { indexInstalledRoot } = createInstallModule(ports);
|
|
73
73
|
const { readInstalledSkillMarkdown } = createCapabilityProjectionModule(ports);
|
|
74
|
-
const SHA256_DIGEST_DIRNAME_PATTERN = /^[a-f0-9]{64}$/;
|
|
75
74
|
/** Reads the delivery mode for this process. An unset or unrecognised value is `inject` — an A/B
|
|
76
75
|
* affordance must never be able to change production behaviour by typo. */
|
|
77
76
|
function resolveAgentPluginDeliveryMode() { return ports.deliveryMode; }
|
|
@@ -147,40 +146,15 @@ function buildModule(ports) {
|
|
|
147
146
|
* re-walking `packages/sha256/*` a second, less-validated way keeps the digest-directory-name check
|
|
148
147
|
* and the per-digest failure isolation in exactly one place. */
|
|
149
148
|
async function listInstalledPlugins(workspaceRoot) {
|
|
150
|
-
let folders;
|
|
151
|
-
try {
|
|
152
|
-
folders = await readdir(workspaceRoot, { withFileTypes: true });
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
if (isEnoent(error))
|
|
156
|
-
return [];
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
149
|
const installed = [];
|
|
160
|
-
for (const
|
|
161
|
-
if (!folder.isDirectory() || !/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(folder.name) || folder.name.length > 64)
|
|
162
|
-
continue;
|
|
163
|
-
const packagesDir = path.join(workspaceRoot, folder.name, "package", "sha256");
|
|
164
|
-
let entries;
|
|
150
|
+
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
165
151
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
152
|
+
const plugin = await indexInstalledRoot(root.packageRoot, root.archiveDigest);
|
|
153
|
+
if (plugin.pluginId === root.localId)
|
|
154
|
+
installed.push(plugin);
|
|
168
155
|
}
|
|
169
156
|
catch {
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
if (!entry.isDirectory() || !SHA256_DIGEST_DIRNAME_PATTERN.test(entry.name))
|
|
174
|
-
continue;
|
|
175
|
-
try {
|
|
176
|
-
const packageRoot = await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
|
|
177
|
-
const plugin = await indexInstalledRoot(packageRoot, entry.name);
|
|
178
|
-
if (plugin.pluginId === folder.name)
|
|
179
|
-
installed.push(plugin);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
183
|
-
}
|
|
157
|
+
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
184
158
|
}
|
|
185
159
|
}
|
|
186
160
|
return installed;
|
|
@@ -208,7 +182,7 @@ function buildModule(ports) {
|
|
|
208
182
|
* @complexity One `stat`.
|
|
209
183
|
*/
|
|
210
184
|
async function isInstalledDigestPresent(packagesDir, archiveDigest) {
|
|
211
|
-
if (!
|
|
185
|
+
if (!INSTALLED_PLUGIN_DIGEST_PATTERN.test(archiveDigest))
|
|
212
186
|
return false;
|
|
213
187
|
try {
|
|
214
188
|
return (await stat(path.join(packagesDir, archiveDigest))).isDirectory();
|
|
@@ -217,9 +191,6 @@ function buildModule(ports) {
|
|
|
217
191
|
return false;
|
|
218
192
|
}
|
|
219
193
|
}
|
|
220
|
-
function isEnoent(error) {
|
|
221
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
222
|
-
}
|
|
223
194
|
/**
|
|
224
195
|
* Resolves one `pluginRefId`, applying the zero/one/many-match rules described in this module's
|
|
225
196
|
* own header.
|
|
@@ -67,6 +67,8 @@ export interface AgentPluginSearchCandidate {
|
|
|
67
67
|
readonly summary?: string | undefined;
|
|
68
68
|
readonly description?: (string) | undefined;
|
|
69
69
|
readonly keywords: readonly string[];
|
|
70
|
+
readonly id?: string | undefined;
|
|
71
|
+
readonly provides?: readonly string[] | undefined;
|
|
70
72
|
/** Whether this workspace currently has the plugin enabled — reflects `activation.ts`'s
|
|
71
73
|
* absent-means-active default, same as everywhere else this codebase reads activation state. */
|
|
72
74
|
readonly enabled: boolean;
|
package/dist/lifecycle/search.js
CHANGED
|
@@ -18,8 +18,8 @@ function buildModule(ports) {
|
|
|
18
18
|
* plain, independently-readable list rather than interleaved with the scoring loop itself. */
|
|
19
19
|
function fieldHaystacks(candidate) {
|
|
20
20
|
return [
|
|
21
|
-
{ text: candidate.pluginId.toLowerCase(), weight: ID_FIELD_WEIGHT },
|
|
22
|
-
{ text: candidate.keywords.join(" ").toLowerCase(), weight: KEYWORD_FIELD_WEIGHT },
|
|
21
|
+
{ text: `${candidate.pluginId} ${candidate.id ?? ""}`.toLowerCase(), weight: ID_FIELD_WEIGHT },
|
|
22
|
+
{ text: [...candidate.keywords, ...(candidate.provides ?? [])].join(" ").toLowerCase(), weight: KEYWORD_FIELD_WEIGHT },
|
|
23
23
|
{ text: (candidate.description ?? "").toLowerCase(), weight: DESCRIPTION_FIELD_WEIGHT },
|
|
24
24
|
{
|
|
25
25
|
text: candidate.skills.map((skill) => `${skill.name} ${skill.summary}`).join(" ").toLowerCase(),
|
|
@@ -6,6 +6,8 @@ export interface InstalledAgentPluginSkill {
|
|
|
6
6
|
readonly skillPath: string;
|
|
7
7
|
}
|
|
8
8
|
export interface InstalledAgentPlugin {
|
|
9
|
+
readonly id?: string | undefined;
|
|
10
|
+
readonly provides?: readonly string[] | undefined;
|
|
9
11
|
readonly pluginId: string;
|
|
10
12
|
readonly version?: (string) | undefined;
|
|
11
13
|
/** the host extension namespace in `plugin.json`: `displayName` — the human-facing title when the id alone reads
|
package/dist/manifest.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface PluginManifestAuthor {
|
|
|
20
20
|
readonly email?: string;
|
|
21
21
|
readonly url?: string;
|
|
22
22
|
}
|
|
23
|
+
export type { PluginIdentityExtension } from "@jini-ai/core/plugin-directory";
|
|
23
24
|
export interface PluginManifest {
|
|
24
25
|
readonly $schema?: string;
|
|
25
26
|
/** 1-64 chars, lowercase alphanumeric/dots/hyphens, no leading/trailing/doubled separators. */
|
|
@@ -60,6 +61,8 @@ export declare function isMcpManifest(value: unknown): value is McpManifest;
|
|
|
60
61
|
/** Object-argument facade for the legacy structural validator. */
|
|
61
62
|
export declare function validatePluginManifest(required: {
|
|
62
63
|
readonly value: unknown;
|
|
64
|
+
}, optional?: {
|
|
65
|
+
readonly identityNamespace?: string;
|
|
63
66
|
}): boolean;
|
|
64
67
|
/** Object-argument facade for the legacy structural validator. */
|
|
65
68
|
export declare function validateMcpManifest(required: {
|
package/dist/manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validatePluginIdentity } from "@jini-ai/core/plugin-directory";
|
|
1
2
|
/**
|
|
2
3
|
* Types for the Agent Plugins 1.0.0 open standard (published by a Technical Steering Committee
|
|
3
4
|
* with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel). A plugin is a
|
|
@@ -28,8 +29,8 @@ export function isMcpManifest(value) {
|
|
|
28
29
|
return typeof servers === 'object' && servers !== null && !Array.isArray(servers);
|
|
29
30
|
}
|
|
30
31
|
/** Object-argument facade for the legacy structural validator. */
|
|
31
|
-
export function validatePluginManifest(required) {
|
|
32
|
-
return isPluginManifest(required.value);
|
|
32
|
+
export function validatePluginManifest(required, optional = {}) {
|
|
33
|
+
return isPluginManifest(required.value) && validatePluginIdentity({ manifest: required.value, namespace: optional.identityNamespace ?? "tovu" }).errors.length === 0;
|
|
33
34
|
}
|
|
34
35
|
/** Object-argument facade for the legacy structural validator. */
|
|
35
36
|
export function validateMcpManifest(required) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/agent-plugins",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Support for Agent Plugins, the open, vendor-neutral spec (v1.0.0, published 2026-08-06
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "Support for Agent Plugins, the open, vendor-neutral spec (v1.0.0, published 2026-08-06; v1.1.0 and later 1.x manifests are also accepted; Technical Steering Committee with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel) for bundling Agent Skills and MCP servers into one portable directory: PluginManifest/McpManifest types + structural validators on the package root, plus the bundled plugins themselves, each on a subpath named for itself (./ui-ux-design). Extracted 2026-08-18 from @jini-ai/plugins, which now only reserves the unrelated, unimplemented ./host format.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -89,21 +89,18 @@
|
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@vitest/coverage-v8": "^2.1.9",
|
|
91
91
|
"yazl": "^3.3.1",
|
|
92
|
-
"@jini-ai/core": "^0.4.
|
|
92
|
+
"@jini-ai/core": "^0.4.2",
|
|
93
93
|
"@jini-ai/platform": "^0.5.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"yauzl": "^3.4.0",
|
|
97
|
-
"@jini-ai/core": "^0.4.
|
|
98
|
-
"@jini-ai/platform": "^0.5.
|
|
97
|
+
"@jini-ai/core": "^0.4.3",
|
|
98
|
+
"@jini-ai/platform": "^0.5.3"
|
|
99
99
|
},
|
|
100
100
|
"peerDependenciesMeta": {
|
|
101
101
|
"yauzl": {
|
|
102
102
|
"optional": true
|
|
103
103
|
},
|
|
104
|
-
"@jini-ai/core": {
|
|
105
|
-
"optional": true
|
|
106
|
-
},
|
|
107
104
|
"@jini-ai/platform": {
|
|
108
105
|
"optional": true
|
|
109
106
|
}
|
package/ui-ux-design/plugin.json
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "ui-ux-design",
|
|
4
|
-
"description": "UI/UX design, interface-design-system, accessibility, and shadcn/ui component skills bundled as one portable Agent Plugin."
|
|
4
|
+
"description": "UI/UX design, interface-design-system, accessibility, and shadcn/ui component skills bundled as one portable Agent Plugin.",
|
|
5
|
+
"extensions": {
|
|
6
|
+
"tovu": {
|
|
7
|
+
"id": "jini/ui-ux-design",
|
|
8
|
+
"provides": [
|
|
9
|
+
"ui-design"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"design",
|
|
15
|
+
"ui",
|
|
16
|
+
"ux",
|
|
17
|
+
"ui-design"
|
|
18
|
+
]
|
|
5
19
|
}
|