@jini-ai/agent-plugins 0.4.4 → 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,15 @@
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
+
3
13
  ## 0.4.4 — 2026-10-12 — Agent Plugins 1.1.0
4
14
 
5
15
  - `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` (and so the installer and MCP
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/agent-plugins",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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": {