@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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
78
|
-
|
|
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,
|
|
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,
|
|
23
|
+
outcomes.push(await retireOne(required, optional, pluginId, ids));
|
|
22
24
|
}
|
|
23
25
|
catch (error) {
|
|
24
|
-
outcomes.push({ pluginId,
|
|
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,
|
|
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,
|
|
39
|
+
return { pluginId, ...ids, status: "failed", reason: `cannot tell whether '${pluginId}' was enabled: ${verdict.reason}` };
|
|
37
40
|
}
|
|
38
|
-
const
|
|
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,
|
|
44
|
-
return { pluginId,
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
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.
|
|
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": {
|