@jini-ai/agent-plugins 0.4.3 → 0.4.5

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5 — 2026-10-12 — retirement waits for usable successors
4
+
5
+ - `retireBundledAgentPlugins()` (and so the boot seeder) keeps a retired bundled plugin until EVERY
6
+ successor is installed at the digest `bundled-digests.json` records for it. Before, a successor
7
+ whose seed had failed still got an activation record and the predecessor was removed — a switch
8
+ with no code behind it. The kept plugin's outcome is `failed` with the reason; the next boot retries.
9
+ - `retiredBundledPlugins` (and the `retired` option) may list several successors for a plugin split
10
+ across them; such outcomes carry `successorIds` and, when retired, per-successor `successors`.
11
+ Single-successor outcomes are unchanged.
12
+
13
+ ## 0.4.4 — 2026-10-12 — Agent Plugins 1.1.0
14
+
15
+ - `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` (and so the installer and MCP
16
+ projection) select the grammar by `$schema` and accept Agent Plugins 1.1.0 identifiers alongside
17
+ 1.0.0. 1.1.0 changes only the version identifiers, so both map to the same validator; 1.0.0
18
+ behavior is unchanged. When a `plugin.json` is supplied, `mcp.json` must target the same version
19
+ (§10).
20
+ - Forward-compatible within major 1 (owner decision): an unknown 1.x (e.g. 1.2.0) is validated with
21
+ the newest known (1.1.0) grammar and reports one warning naming the version. A different major
22
+ (2.x, 0.x) is refused as an unsupported major version; a non-canonical `$schema` is refused with
23
+ the accepted identifiers listed.
24
+
3
25
  ## 0.4.2
4
26
 
5
27
  - 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.
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) for
6
- bundling Agent Skills and MCP servers into one portable directory. Published by a Technical
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
@@ -2,7 +2,7 @@ export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
2
2
  export type { PluginManifest, McpManifest, McpServerEntry } from '../manifest.js';
3
3
  import type { PluginManifestAuthor } from '../manifest.js';
4
4
  export type { PluginManifestAuthor } from '../manifest.js';
5
- /** 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. */
6
6
  export type AgentPluginServerMetadataReader = (required: {
7
7
  readonly serverId: string;
8
8
  readonly value: Readonly<Record<string, unknown>>;
@@ -29,7 +29,7 @@ export type ParseAgentPluginManifestResult = {
29
29
  readonly errors: readonly string[];
30
30
  };
31
31
  /**
32
- * Validates an already-`JSON.parse`d `plugin.json` value against the v1.0.0 grammar.
32
+ * Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
33
33
  *
34
34
  * @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
35
35
  * own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
@@ -42,7 +42,7 @@ export declare function parseAgentPluginManifest({ value }: {
42
42
  readonly identityNamespace?: string;
43
43
  }): ParseAgentPluginManifestResult;
44
44
  /**
45
- * The three closed transport variants `mcp.schema.json` v1.0.0 defines. See this file's header for
45
+ * The three closed transport variants `mcp.schema.json` 1.0.0/1.1.0 defines. See this file's header for
46
46
  * the verified field shape of each.
47
47
  */
48
48
  export declare const MCP_SERVER_TRANSPORTS: readonly ["stdio", "streamable-http", "sse"];
@@ -134,9 +134,10 @@ export type ParseAgentPluginMcpConfigResult = {
134
134
  readonly errors: readonly string[];
135
135
  };
136
136
  /**
137
- * Validates an already-`JSON.parse`d `mcp.json` value against the v1.0.0 top-level shape:
137
+ * Validates an already-`JSON.parse`d `mcp.json` value against the 1.0.0/1.1.0 top-level shape:
138
138
  * `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }`, then validates each declared
139
- * 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).
140
141
  *
141
142
  * @throws Nothing — see {@link parseAgentPluginManifest}.
142
143
  * @complexity O(s) in the number of declared servers.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` — the Agent Plugins v1.0.0
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 v1.0.0 rules from agent-plugins.org/specification:
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 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`.
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>": {...} } }` —
@@ -55,11 +57,44 @@
55
57
  import { readPluginIdentity, validatePluginIdentity } from "@jini-ai/core/plugin-directory";
56
58
  export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
57
59
  /**
58
- * The one plugin.json schema version this loader understands. A manifest declaring any other
59
- * value is rejected outright rather than guessed at: pinning the grammar must survive spec churn.
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.
60
64
  */
61
- const PLUGIN_SCHEMA_1_0_0 = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
62
- const MCP_SCHEMA_1_0_0 = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
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
+ }
63
98
  /**
64
99
  * §5.5's grammar as one regex: alnum runs separated by single `-`/`.` delimiters. This shape alone
65
100
  * guarantees start/end are alphanumeric (the regex can only start and end on `[a-z0-9]+`) AND that
@@ -114,7 +149,7 @@ function resolveManifestName(raw) {
114
149
  return { name };
115
150
  }
116
151
  /**
117
- * Validates an already-`JSON.parse`d `plugin.json` value against the v1.0.0 grammar.
152
+ * Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
118
153
  *
119
154
  * @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
120
155
  * own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
@@ -127,9 +162,11 @@ export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu"
127
162
  const raw = value;
128
163
  const errors = [];
129
164
  const warnings = [];
130
- if (raw.$schema !== PLUGIN_SCHEMA_1_0_0) {
131
- errors.push(`plugin.json '$schema' must be '${PLUGIN_SCHEMA_1_0_0}', got '${String(raw.$schema)}'`);
132
- }
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);
133
170
  if (raw.extensions !== undefined && !isJsonObject(raw.extensions))
134
171
  errors.push("plugin.json extensions must be an object");
135
172
  const nameResult = resolveManifestName(raw);
@@ -158,7 +195,7 @@ export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu"
158
195
  return { ok: true, manifest, warnings };
159
196
  }
160
197
  /**
161
- * The three closed transport variants `mcp.schema.json` v1.0.0 defines. See this file's header for
198
+ * The three closed transport variants `mcp.schema.json` 1.0.0/1.1.0 defines. See this file's header for
162
199
  * the verified field shape of each.
163
200
  */
164
201
  export const MCP_SERVER_TRANSPORTS = ["stdio", "streamable-http", "sse"];
@@ -348,9 +385,10 @@ function parseMcpServerConfig(value, extension) {
348
385
  return null;
349
386
  }
350
387
  /**
351
- * Validates an already-`JSON.parse`d `mcp.json` value against the v1.0.0 top-level shape:
388
+ * Validates an already-`JSON.parse`d `mcp.json` value against the 1.0.0/1.1.0 top-level shape:
352
389
  * `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }`, then validates each declared
353
- * 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).
354
392
  *
355
393
  * @throws Nothing — see {@link parseAgentPluginManifest}.
356
394
  * @complexity O(s) in the number of declared servers.
@@ -362,8 +400,14 @@ export function parseAgentPluginMcpConfig({ value, extensionNamespace }, optiona
362
400
  return { ok: false, errors: ["mcp.json must be a JSON object"] };
363
401
  const raw = value;
364
402
  const errors = [];
365
- if (raw.$schema !== MCP_SCHEMA_1_0_0) {
366
- errors.push(`mcp.json '$schema' must be '${MCP_SCHEMA_1_0_0}', got '${String(raw.$schema)}'`);
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`);
367
411
  }
368
412
  const mcpServers = raw.mcpServers;
369
413
  if (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers)) {
@@ -66,7 +66,8 @@ export interface AgentPluginLifecyclePorts {
66
66
  readonly pluginId: string;
67
67
  }) => string;
68
68
  readonly seededEnabledPluginIds: ReadonlySet<string>;
69
- readonly retiredBundledPlugins: ReadonlyMap<string, string>;
69
+ /** Retired bundled plugin id -> the bundled plugin(s) that absorbed it (`retire-bundled.ts`). */
70
+ readonly retiredBundledPlugins: ReadonlyMap<string, string | readonly string[]>;
70
71
  readonly bundledArchiveMagic: string;
71
72
  readonly filesystem: FilesystemPort;
72
73
  readonly clock: AgentPluginClockPort;
@@ -19,6 +19,14 @@
19
19
  * ---------------------------------------------------------------------------
20
20
  * 1. Nothing installed and no activation record: nothing to do (the second boot after a migration).
21
21
  * A leftover ledger entry is still dropped.
22
+ * 1a. Otherwise EVERY successor must be usable first: installed at the digest `bundled-digests.json`
23
+ * records for it (the build's own seed — the same trust `trusted-plugin-files.ts` demands before
24
+ * core takes a contribution from it), whether it seeded this boot or on an earlier one. If any
25
+ * is not, the retired plugin is KEPT and the outcome is `failed` with the reason; the next boot
26
+ * retries. Without this a successor whose seed failed (a broken `plugin.json`, a full disk) left
27
+ * the retired plugin removed and only an activation record for the successor — a switch with no
28
+ * code behind it (R6, 2026-10-12). A plugin split into several (`deploy` -> `fly`, `render`, ...)
29
+ * is kept until all of them are usable: one working successor must not cost the others' capability.
22
30
  * 2. The retired plugin was ENABLED (explicitly, or by the "absent means active" default while its
23
31
  * package is installed): the successor is switched on — unless an operator explicitly turned the
24
32
  * successor off, which is left exactly as it is and reported in the outcome. No silent override.
@@ -47,13 +55,17 @@ import type { AgentPluginLifecyclePorts } from "./ports.js";
47
55
  /** What happened to the successor. `not-needed`: the retired plugin was off, so the successor was
48
56
  * left as it was. The other three are {@link BundledAgentPluginEnableOutcome}. */
49
57
  export type RetiredAgentPluginSuccessorOutcome = BundledAgentPluginEnableOutcome | "not-needed";
50
- export type RetiredAgentPluginOutcome = {
51
- readonly pluginId: string;
58
+ /** `successorId` is the (first) successor; `successorIds` is present only when the map entry lists
59
+ * several, and then names every one in map order. */
60
+ interface RetiredAgentPluginSuccessors {
52
61
  readonly successorId: string;
62
+ readonly successorIds?: readonly string[] | undefined;
63
+ }
64
+ export type RetiredAgentPluginOutcome = (RetiredAgentPluginSuccessors & {
65
+ readonly pluginId: string;
53
66
  readonly status: "absent";
54
- } | {
67
+ }) | (RetiredAgentPluginSuccessors & {
55
68
  readonly pluginId: string;
56
- readonly successorId: string;
57
69
  readonly status: "retired";
58
70
  /** Every installed archive digest removed; empty when only a record or ledger entry was left. */
59
71
  readonly removedDigests: readonly string[];
@@ -62,24 +74,27 @@ export type RetiredAgentPluginOutcome = {
62
74
  /** `left-disabled-by-operator` is the one case a caller should surface: the retired plugin was
63
75
  * on, its successor was explicitly turned off, and this step did not override that. */
64
76
  readonly successor: RetiredAgentPluginSuccessorOutcome;
65
- } | {
77
+ /** Present only with `successorIds`: each successor's own outcome. `successor` is the first's. */
78
+ readonly successors?: Readonly<Record<string, RetiredAgentPluginSuccessorOutcome>> | undefined;
79
+ }) | (RetiredAgentPluginSuccessors & {
66
80
  readonly pluginId: string;
67
- readonly successorId: string;
68
81
  readonly status: "failed";
69
82
  readonly reason: string;
70
- };
83
+ });
71
84
  export interface RetireBundledAgentPluginsRequired {
72
85
  /** The INSTANCE-level layout, for the same reason `seed-bundled.ts` takes one. */
73
86
  readonly layout: AgentPluginLayout;
74
87
  readonly workspaceId: string;
75
88
  }
76
89
  export interface RetireBundledAgentPluginsOptional {
77
- /** Defaults to {@link RETIRED_BUNDLED_AGENT_PLUGINS}; tests pass their own. */
78
- readonly retired?: (ReadonlyMap<string, string>) | undefined;
90
+ /** Defaults to the host's `retiredBundledPlugins` port; tests pass their own. A value lists
91
+ * several successors when the retired plugin was split across them. */
92
+ readonly retired?: (ReadonlyMap<string, string | readonly string[]>) | undefined;
79
93
  readonly now?: ((required: Record<string, never>) => Date) | undefined;
80
94
  }
81
95
  /** Internal composition; one state/queue instance per injected host context. */
82
96
  export declare function createRetireBundledModule(ports: AgentPluginLifecyclePorts): {
83
97
  retireBundledAgentPlugins: (required: RetireBundledAgentPluginsRequired, optional?: RetireBundledAgentPluginsOptional) => Promise<readonly RetiredAgentPluginOutcome[]>;
84
98
  };
99
+ export {};
85
100
  //# sourceMappingURL=retire-bundled.d.ts.map
@@ -4,7 +4,7 @@ import { createResolveAgentPluginRefsModule } from "./resolve-agent-plugin-refs.
4
4
  import { createUninstallModule } from "./uninstall.js";
5
5
  function buildModule(ports) {
6
6
  const { deleteAgentPluginActivation, enableBundledAgentPluginUnlessOperatorDisabled, readAgentPluginActivations, resolveAgentPluginActivation } = createActivationModule(ports);
7
- const { removeBundledAgentPluginDigest } = createBundledDigestsModule(ports);
7
+ const { readBundledAgentPluginDigests, removeBundledAgentPluginDigest } = createBundledDigestsModule(ports);
8
8
  const { listInstalledPlugins } = createResolveAgentPluginRefsModule(ports);
9
9
  const { uninstallAgentPlugin } = createUninstallModule(ports);
10
10
  /**
@@ -16,41 +16,54 @@ function buildModule(ports) {
16
16
  */
17
17
  async function retireBundledAgentPlugins(required, optional = {}) {
18
18
  const outcomes = [];
19
- for (const [pluginId, successorId] of optional.retired ?? ports.retiredBundledPlugins) {
19
+ for (const [pluginId, successor] of optional.retired ?? ports.retiredBundledPlugins) {
20
+ const successorIds = typeof successor === "string" ? [successor] : successor;
21
+ const ids = { successorId: successorIds[0] ?? "", ...(typeof successor === "string" ? {} : { successorIds }) };
20
22
  try {
21
- outcomes.push(await retireOne(required, optional, pluginId, successorId));
23
+ outcomes.push(await retireOne(required, optional, pluginId, ids));
22
24
  }
23
25
  catch (error) {
24
- outcomes.push({ pluginId, successorId, status: "failed", reason: error instanceof Error ? error.message : String(error) });
26
+ outcomes.push({ pluginId, ...ids, status: "failed", reason: error instanceof Error ? error.message : String(error) });
25
27
  }
26
28
  }
27
29
  return outcomes;
28
30
  }
29
31
  // The successor first: if a later step fails, the operator keeps the capability, and the next boot
30
32
  // (which finds this step already done) retries only what is left.
31
- async function retireOne(required, optional, pluginId, successorId) {
33
+ async function retireOne(required, optional, pluginId, ids) {
32
34
  const workspaceLayout = required.layout.forWorkspace({ workspaceId: required.workspaceId });
33
35
  const workspaceRoot = workspaceLayout.root;
36
+ const successorIds = ids.successorIds ?? [ids.successorId];
34
37
  const verdict = await resolveAgentPluginActivation(workspaceRoot, pluginId);
35
38
  if (verdict.verdict === "undetermined") {
36
- return { pluginId, successorId, status: "failed", reason: `cannot tell whether '${pluginId}' was enabled: ${verdict.reason}` };
39
+ return { pluginId, ...ids, status: "failed", reason: `cannot tell whether '${pluginId}' was enabled: ${verdict.reason}` };
37
40
  }
38
- const installed = (await listInstalledPlugins(workspaceLayout.root)).filter((plugin) => plugin.pluginId === pluginId);
41
+ const allInstalled = await listInstalledPlugins(workspaceLayout.root);
42
+ const installed = allInstalled.filter((plugin) => plugin.pluginId === pluginId);
39
43
  const hasRecord = Object.hasOwn((await readAgentPluginActivations(workspaceRoot)).plugins, pluginId);
40
44
  if (installed.length === 0 && !hasRecord) {
41
45
  const ledgerEntryRemoved = await removeBundledAgentPluginDigest({ workspaceRoot, pluginId });
42
46
  if (!ledgerEntryRemoved)
43
- return { pluginId, successorId, status: "absent" };
44
- return { pluginId, successorId, status: "retired", removedDigests: [], activationRemoved: false, ledgerEntryRemoved, successor: "not-needed" };
47
+ return { pluginId, ...ids, status: "absent" };
48
+ return { pluginId, ...ids, status: "retired", removedDigests: [], activationRemoved: false, ledgerEntryRemoved, successor: "not-needed", ...(ids.successorIds ? { successors: {} } : {}) };
45
49
  }
46
- const successor = verdict.verdict === "active"
47
- ? await enableBundledAgentPluginUnlessOperatorDisabled({
48
- workspaceRoot,
49
- pluginId: successorId,
50
- actor: `system:retire-${pluginId}`,
51
- ...(optional.now !== undefined ? { now: optional.now } : {}),
52
- })
53
- : "not-needed";
50
+ const unusable = await unusableSuccessor(workspaceRoot, allInstalled, successorIds);
51
+ if (unusable !== undefined) {
52
+ return { pluginId, ...ids, status: "failed", reason: `'${pluginId}' is kept until every successor is usable: ${unusable}` };
53
+ }
54
+ const successors = {};
55
+ for (const successorId of successorIds) {
56
+ successors[successorId] =
57
+ verdict.verdict === "active"
58
+ ? await enableBundledAgentPluginUnlessOperatorDisabled({
59
+ workspaceRoot,
60
+ pluginId: successorId,
61
+ actor: `system:retire-${pluginId}`,
62
+ ...(optional.now !== undefined ? { now: optional.now } : {}),
63
+ })
64
+ : "not-needed";
65
+ }
66
+ const successor = successors[ids.successorId] ?? "not-needed";
54
67
  let removedDigests = [];
55
68
  if (installed.length > 0) {
56
69
  removedDigests = (await uninstallAgentPlugin({ layout: required.layout, workspaceId: required.workspaceId, pluginId }, { retiredBundled: true })).removedDigests;
@@ -59,7 +72,22 @@ function buildModule(ports) {
59
72
  await deleteAgentPluginActivation({ workspaceRoot, pluginId });
60
73
  }
61
74
  const ledgerEntryRemoved = await removeBundledAgentPluginDigest({ workspaceRoot, pluginId });
62
- return { pluginId, successorId, status: "retired", removedDigests, activationRemoved: hasRecord, ledgerEntryRemoved, successor };
75
+ return { pluginId, ...ids, status: "retired", removedDigests, activationRemoved: hasRecord, ledgerEntryRemoved, successor, ...(ids.successorIds ? { successors } : {}) };
76
+ }
77
+ /** Why one of `successorIds` cannot take over yet, or `undefined` when every one is installed at the
78
+ * digest the ledger records for it (this file's header, step 1a).
79
+ * @complexity O(s * n) for s successors over n installed packages; both are small bundled counts. */
80
+ async function unusableSuccessor(workspaceRoot, allInstalled, successorIds) {
81
+ const ledger = await readBundledAgentPluginDigests(workspaceRoot);
82
+ for (const successorId of successorIds) {
83
+ const digest = ledger.get(successorId);
84
+ if (digest === undefined)
85
+ return `successor '${successorId}' has never been seeded into this workspace`;
86
+ if (!allInstalled.some((plugin) => plugin.pluginId === successorId && plugin.archiveDigest === digest)) {
87
+ return `successor '${successorId}' is not installed at its seeded digest`;
88
+ }
89
+ }
90
+ return undefined;
63
91
  }
64
92
  return { retireBundledAgentPlugins };
65
93
  }
@@ -115,6 +115,8 @@ function buildModule(ports) {
115
115
  // once its bytes are actually on disk for this workspace.
116
116
  // AFTER seeding and the ledger: a retired plugin's successor is itself bundled, so its record and
117
117
  // its ledger entry exist by now, and the retirement only has to decide whether to switch it on.
118
+ // A successor whose seed FAILED has neither; `retire-bundled.ts` then keeps the retired plugin
119
+ // rather than remove it in favour of a switch with no code behind it (its header, step 1a).
118
120
  async function seedBundledAgentPlugins(required) {
119
121
  const { layout, workspaceId, sourceRoot } = required;
120
122
  const pluginDirNames = await listBundledPluginDirs(sourceRoot);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jini-ai/agent-plugins",
3
- "version": "0.4.3",
4
- "description": "Support for Agent Plugins, the open, vendor-neutral spec (v1.0.0, published 2026-08-06, 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.",
3
+ "version": "0.4.5",
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",