@jini-ai/agent-plugins 0.4.1 → 0.4.3
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 +8 -0
- package/core/persistent-state.d.ts +25 -0
- package/core/persistent-state.js +260 -0
- package/dist/lifecycle/activation.js +38 -166
- package/dist/lifecycle/bundled-digests.d.ts +6 -0
- package/dist/lifecycle/bundled-digests.js +160 -0
- package/dist/lifecycle/bundled-source-archive.d.ts +3 -0
- package/dist/lifecycle/bundled-source-archive.js +74 -0
- package/dist/lifecycle/contribution-loader.d.ts +102 -0
- package/dist/lifecycle/contribution-loader.js +92 -0
- package/dist/lifecycle/fetch-archive.d.ts +1 -2
- package/dist/lifecycle/index.d.ts +24 -4
- package/dist/lifecycle/index.js +11 -0
- package/dist/lifecycle/install-from-url.d.ts +2 -5
- package/dist/lifecycle/install-from-url.js +2 -5
- package/dist/lifecycle/install.d.ts +58 -3
- package/dist/lifecycle/install.js +301 -23
- package/dist/lifecycle/installed-package-roots.d.ts +16 -0
- package/dist/lifecycle/installed-package-roots.js +55 -0
- package/dist/lifecycle/layout.js +1 -1
- package/dist/lifecycle/manifest.d.ts +5 -60
- package/dist/lifecycle/manifest.js +12 -20
- package/dist/lifecycle/package-paths.d.ts +2 -0
- package/dist/lifecycle/package-paths.js +62 -0
- package/dist/lifecycle/persistent-state.d.ts +5 -1
- package/dist/lifecycle/persistent-state.js +3 -1
- package/dist/lifecycle/ports.d.ts +20 -2
- package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +13 -1
- package/dist/lifecycle/resolve-agent-plugin-refs.js +150 -40
- package/dist/lifecycle/retire-bundled.d.ts +50 -1
- package/dist/lifecycle/retire-bundled.js +9 -2
- package/dist/lifecycle/search.d.ts +65 -0
- package/dist/lifecycle/search.js +31 -2
- package/dist/lifecycle/seed-bundled.d.ts +18 -1
- package/dist/lifecycle/seed-bundled.js +131 -3
- package/dist/lifecycle/set-enabled.d.ts +7 -0
- package/dist/lifecycle/set-enabled.js +5 -0
- package/dist/lifecycle/trusted-plugin-files.d.ts +38 -0
- package/dist/lifecycle/trusted-plugin-files.js +18 -0
- package/dist/lifecycle/types.d.ts +14 -0
- package/dist/lifecycle/uninstall.d.ts +29 -0
- package/dist/lifecycle/uninstall.js +204 -4
- package/dist/lifecycle/yauzl-archive-reader.d.ts +0 -1
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +3 -2
- package/package.json +22 -8
- package/persistent-state.js +2 -260
- package/ui-ux-design/plugin.json +15 -1
|
@@ -1,14 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `uninstallAgentPlugin()` — permanently removes one workspace's installed Agent Plugin: its
|
|
3
|
+
* on-disk package(s) under `packages/sha256/*` for this `pluginId`, then its activation record.
|
|
4
|
+
* `previewAgentPluginUninstall()` runs the same refusals without removing anything.
|
|
5
|
+
*
|
|
6
|
+
* `features/plugin-runtime/uninstall.ts` already has this exact feature for the OTHER plugin family
|
|
7
|
+
* (the host runtime-plugin format site/runtime plugins) — this is "the same feature, one family over", mirroring its
|
|
8
|
+
* shape: the same `<Domain>NotFoundError`/`<Domain>NotUninstallableError` pair, thrown for the same
|
|
9
|
+
* two reasons (unknown id; nothing an operator can actually remove). Two deliberate divergences from
|
|
10
|
+
* that file, both because the underlying mechanics genuinely differ, not by oversight:
|
|
11
|
+
*
|
|
12
|
+
* 1. **No third `PluginEnabledError`-equivalent precondition.** Plugin-runtime refuses to uninstall a
|
|
13
|
+
* plugin that is enabled in ANY workspace, because its on-disk artifact is INSTANCE-WIDE — shared
|
|
14
|
+
* across every workspace via one `installDir` — so removing it while a DIFFERENT workspace still
|
|
15
|
+
* has it enabled would silently break that workspace's runtime. Agent Plugins have no such
|
|
16
|
+
* hazard: `layout.ts`'s own header (owner decision, tenant-grade isolation, 2026-08-12) states
|
|
17
|
+
* plainly that each workspace gets its own `ws/<workspaceId>/packages/sha256/` tree, and "Uninstall
|
|
18
|
+
* is `rm -rf ws/<workspaceId>/packages/sha256/<digest>/`, full stop" — there is no OTHER workspace
|
|
19
|
+
* for this operation to endanger. Whether the CALLING workspace has this plugin currently enabled
|
|
20
|
+
* is not gated on either: disabling first buys no safety here (unlike plugin-runtime, disabling
|
|
21
|
+
* would not protect anyone else's runtime), and gating on it would only add friction with no
|
|
22
|
+
* corresponding risk removed.
|
|
23
|
+
*
|
|
24
|
+
* 2. **Bundled is refused as UNINSTALLABLE, not as NOT-FOUND — and for a different reason than
|
|
25
|
+
* plugin-runtime's built-ins.** Plugin-runtime's `PluginNotUninstallableError` covers a `"built-in"`
|
|
26
|
+
* discovery record that was never written to `installDir` at all — there is nothing on disk to
|
|
27
|
+
* remove. A bundled Agent Plugin is NOT that case: `seed-bundled.ts` genuinely extracts it onto
|
|
28
|
+
* disk at boot, so it IS `listInstalledPlugins`-visible and this function's own "not found" check
|
|
29
|
+
* would pass. The refusal here is about PERMANENCE, not presence: `recordBundledAgentPluginIfAbsent`
|
|
30
|
+
* re-seeds both the package and its (absent-record-only) disabled activation entry on every boot
|
|
31
|
+
* (`activation.ts`'s header), so deleting the files now would appear to succeed and then silently
|
|
32
|
+
* reappear on the next restart — a worse outcome than refusing outright, because it would look like
|
|
33
|
+
* uninstall lied. `AgentPluginNotUninstallableError`'s message says this plainly and names the
|
|
34
|
+
* levers that DO work: `plugins_set_enabled` with family `agent-plugin` (the assistant's own tool
|
|
35
|
+
* since a1abe2bb, so the model can act on the refusal itself) or the Agent Plugins admin screen's
|
|
36
|
+
* switch. Either one turns a bundled plugin off, even though nothing can make the host stop shipping it.
|
|
37
|
+
*
|
|
38
|
+
* ---------------------------------------------------------------------------
|
|
39
|
+
* Preview, then uninstall — both run the refusals
|
|
40
|
+
* ---------------------------------------------------------------------------
|
|
41
|
+
* `agent_plugins_uninstall` asks a human before it removes anything. A refusal has to come back
|
|
42
|
+
* BEFORE that dialog, or a human is asked to confirm something that cannot happen, so the tool calls
|
|
43
|
+
* `previewAgentPluginUninstall()` first. The preview carries only what a dialog may show (id,
|
|
44
|
+
* versions, digests) — never `packageRoot`, which is a host path. After the answer the tool calls
|
|
45
|
+
* `uninstallAgentPlugin()`, which runs both refusals again against the disk as it is then: the
|
|
46
|
+
* dialog can stay open for minutes. The tool also passes the confirmed preview back in: if the
|
|
47
|
+
* installed digests for the id are no longer exactly the previewed ones (an archive added or removed
|
|
48
|
+
* meanwhile), `uninstallAgentPlugin()` removes nothing and throws `AgentPluginChangedSincePreviewError`,
|
|
49
|
+
* which the tool reports as a result.
|
|
50
|
+
*
|
|
51
|
+
* ---------------------------------------------------------------------------
|
|
52
|
+
* Tombstone vs. delete on the activation record — DELETE, settled
|
|
53
|
+
* ---------------------------------------------------------------------------
|
|
54
|
+
* See `activation.ts`'s `deleteAgentPluginActivation()` for the mechanism; the decision itself
|
|
55
|
+
* belongs here, next to the only call site that makes it. A tombstone (leaving `enabled: false` on
|
|
56
|
+
* disk after the bytes are gone) would invert `activation.ts`'s own stated rule — "absent means
|
|
57
|
+
* active, because an operator's own install IS the consent" — the moment this same plugin id is ever
|
|
58
|
+
* reinstalled: the fresh install would inherit a stale disabled flag about bytes that no longer exist,
|
|
59
|
+
* which is a worse UX than starting over. Deleting the record makes a future reinstall of this id
|
|
60
|
+
* begin exactly where a first-ever install begins.
|
|
61
|
+
*
|
|
62
|
+
* ---------------------------------------------------------------------------
|
|
63
|
+
* Why this module resolves its own workspace layout, rather than accepting one
|
|
64
|
+
* ---------------------------------------------------------------------------
|
|
65
|
+
* Same SECURITY discipline `install.ts`'s `InstallAgentPluginRequired.layout` documents in full: this
|
|
66
|
+
* takes the INSTANCE-level `AgentPluginLayout` plus one `workspaceId`, and calls
|
|
67
|
+
* `layout.forWorkspace(workspaceId)` itself, exactly once per call, so the packages directory it scans
|
|
68
|
+
* and the activation file it edits can never come from two different, hand-stitched sources that
|
|
69
|
+
* quietly disagree about which workspace they belong to.
|
|
70
|
+
*
|
|
71
|
+
* ---------------------------------------------------------------------------
|
|
72
|
+
* Why removal isn't a plain `fs.rm(recursive)`
|
|
73
|
+
* ---------------------------------------------------------------------------
|
|
74
|
+
* `install.ts`'s `freezeTree` chmods every published package directory to `0o555` (no write bit) —
|
|
75
|
+
* that's what makes the package genuinely read-only after install. A plain recursive `rm` on a frozen
|
|
76
|
+
* tree fails EACCES trying to unlink/rmdir inside it (the same fact `__tests__/fixtures/force-remove.ts`
|
|
77
|
+
* exists to work around in tests). `removeFrozenPackageTree` below is that same "restore write
|
|
78
|
+
* permission, then remove" logic as real production code — this is the first production caller that
|
|
79
|
+
* ever needs to remove a package root that install-time freezing already made.
|
|
80
|
+
*
|
|
81
|
+
* ---------------------------------------------------------------------------
|
|
82
|
+
* Why removal stages first — and why it stages NEXT TO the package, not under `staging/`
|
|
83
|
+
* ---------------------------------------------------------------------------
|
|
84
|
+
* Removing a multi-digest plugin is several irreversible `rm`s followed by one activation-record
|
|
85
|
+
* write. Done directly, a failure anywhere after the first `rm` leaves an installation that is
|
|
86
|
+
* half-deleted and an activation record describing bytes that no longer exist — recoverable by
|
|
87
|
+
* nobody. So every package root is first `rename`d aside (cheap, atomic within one filesystem, and
|
|
88
|
+
* reversible), the activation record is deleted, and only then are the renamed trees actually
|
|
89
|
+
* removed. Any failure before that last step puts every renamed tree straight back.
|
|
90
|
+
*
|
|
91
|
+
* Renaming a frozen package root needs its write bit back FIRST. `install.ts`'s own publish comment
|
|
92
|
+
* records the measured fact this depends on: on this filesystem renaming an already-`0o555`
|
|
93
|
+
* directory fails EACCES — and, re-measured here, that holds even when the rename stays inside the
|
|
94
|
+
* same parent, so there is no permission-free way to move a frozen tree. `stageForRemoval` therefore
|
|
95
|
+
* restores the owner write bit on the package ROOT only (never its contents), renames, and — on any
|
|
96
|
+
* failure, including a rollback — puts the exact original mode back, so a failed uninstall leaves a
|
|
97
|
+
* tree that is still frozen. That briefly-writable window is the same "accidental same-process
|
|
98
|
+
* write" surface `freezeTree`'s own header scopes itself to, not a security boundary.
|
|
99
|
+
*
|
|
100
|
+
* The staged name is a SIBLING inside the same `packages/sha256/` directory rather than
|
|
101
|
+
* `<workspaceRoot>/staging/` (which install uses for its own transactions): same parent means the
|
|
102
|
+
* rename is guaranteed to be within one filesystem, and therefore atomic, without depending on a
|
|
103
|
+
* second directory existing. The names begin with a dot and carry a random suffix, so they can never
|
|
104
|
+
* be mistaken for an installed digest (`resolve-agent-plugin-refs.ts` matches `^[a-f0-9]{64}$`) nor
|
|
105
|
+
* collide with a concurrent uninstall. A crash between the rename and the final remove strands a
|
|
106
|
+
* `.uninstalling-*` directory an operator can delete — strictly better than a half-deleted install.
|
|
107
|
+
*/
|
|
108
|
+
/** Prefix for a staged (renamed-aside) package root. Leading dot plus a random suffix: never a
|
|
109
|
+
* valid installed-digest directory name, never colliding with a concurrent uninstall. */
|
|
1
110
|
import { createPersistentStateModule } from "./persistent-state.js";
|
|
2
111
|
import path from "node:path";
|
|
3
112
|
import { createPackagePathsModule } from "./package-paths.js";
|
|
4
113
|
import { createActivationModule } from "./activation.js";
|
|
5
114
|
import { createResolveAgentPluginRefsModule } from "./resolve-agent-plugin-refs.js";
|
|
115
|
+
/** The plugin id has no installed package on disk for this workspace. */
|
|
6
116
|
export class AgentPluginNotFoundError extends Error {
|
|
7
117
|
constructor({ message }, optional = {}) { super(message, optional); }
|
|
8
118
|
}
|
|
119
|
+
/** The plugin is bundled with the host — see this file's header for why that is permanent, not merely
|
|
120
|
+
* "nothing to remove". */
|
|
9
121
|
export class AgentPluginNotUninstallableError extends Error {
|
|
10
122
|
constructor({ message }, optional = {}) { super(message, optional); }
|
|
11
123
|
}
|
|
124
|
+
/** The installed archives for this plugin id are no longer exactly the ones a confirmed preview listed —
|
|
125
|
+
* see `UninstallAgentPluginOptional.confirmedPreview`. Nothing was removed. */
|
|
12
126
|
export class AgentPluginChangedSincePreviewError extends Error {
|
|
13
127
|
constructor({ message }, optional = {}) { super(message, optional); }
|
|
14
128
|
}
|
|
@@ -24,21 +138,55 @@ function buildModule(ports) {
|
|
|
24
138
|
const { deleteAgentPluginActivation, isAgentPluginRecordedAsBundled, resolveAgentPluginActivation } = createActivationModule(ports);
|
|
25
139
|
const { listInstalledPlugins } = createResolveAgentPluginRefsModule(ports);
|
|
26
140
|
const STAGED_DIRNAME_PREFIX = ".uninstalling-";
|
|
141
|
+
/**
|
|
142
|
+
* Reports what uninstalling `pluginId` would remove, without removing anything.
|
|
143
|
+
*
|
|
144
|
+
* @throws {AgentPluginNotFoundError} No installed package for `pluginId` exists in this workspace.
|
|
145
|
+
* @throws {AgentPluginNotUninstallableError} The plugin's activation record has `origin: "bundled"`, or a malformed
|
|
146
|
+
* entry for it (t91 R4).
|
|
147
|
+
* @throws {AgentPluginActivationsUnreadableError} The activation record could not be read at all, so
|
|
148
|
+
* whether it says "bundled" cannot be established — see `activation.ts`'s
|
|
149
|
+
* `isAgentPluginRecordedAsBundled`.
|
|
150
|
+
* @complexity O(d) in this workspace's installed-digest count (one `listInstalledPlugins` walk).
|
|
151
|
+
*/
|
|
27
152
|
async function previewAgentPluginUninstall(required) {
|
|
28
153
|
const { matches } = await resolveUninstallTargets(required);
|
|
29
154
|
const versions = [...new Set(matches.flatMap((plugin) => (plugin.version === undefined ? [] : [plugin.version])))];
|
|
30
155
|
return { pluginId: required.pluginId, versions, archiveDigests: matches.map((plugin) => plugin.archiveDigest) };
|
|
31
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Uninstalls one workspace's installed Agent Plugin: removes every installed archive digest for
|
|
159
|
+
* `pluginId` under this workspace's own `packages/sha256/`, then deletes its activation record.
|
|
160
|
+
*
|
|
161
|
+
* @throws {AgentPluginNotFoundError} No installed package for `pluginId` exists in this workspace.
|
|
162
|
+
* @throws {AgentPluginNotUninstallableError} The plugin's activation record has `origin: "bundled"`, or a malformed
|
|
163
|
+
* entry for it (t91 R4).
|
|
164
|
+
* @throws {AgentPluginActivationsUnreadableError} The activation record could not be read — refused
|
|
165
|
+
* before anything is staged, so nothing is removed.
|
|
166
|
+
* @throws {AgentPluginChangedSincePreviewError} `optional.confirmedPreview` was given and the installed digests differ from it.
|
|
167
|
+
* @throws Whatever the filesystem raised, after every staged tree has been put back — or, if a tree
|
|
168
|
+
* could not be put back, an error naming the staged paths an operator must recover by hand.
|
|
169
|
+
* @complexity O(d) in this workspace's installed-digest count (one `listInstalledPlugins` walk) plus
|
|
170
|
+
* O(f) in the total file count under the matching digest(s) being removed.
|
|
171
|
+
*/
|
|
32
172
|
async function uninstallAgentPlugin(required, optional = {}) {
|
|
33
173
|
const workspaceRoot = required.layout.forWorkspace({ workspaceId: required.workspaceId }).root;
|
|
34
174
|
return createPersistentStateModule(ports).withPluginLock({ workspaceRoot, pluginId: required.pluginId,
|
|
35
175
|
run: () => uninstallAgentPluginUnlocked(required, optional) });
|
|
36
176
|
}
|
|
177
|
+
// Reversible work first — see this file's header, "Why removal stages first". Nothing on this
|
|
178
|
+
// side of the try is destructive: every step up to and including the activation-record delete can
|
|
179
|
+
// be undone by renaming the staged trees back where they came from.
|
|
180
|
+
// Past this point the live state is already settled: the packages are no longer reachable under
|
|
181
|
+
// any installed-digest name and the activation record is gone. These removes are the only
|
|
182
|
+
// irreversible step, and nothing observable depends on them finishing.
|
|
183
|
+
// Remove only empty package ancestors: a concurrently-installed digest must never be deleted.
|
|
184
|
+
// No package may have appeared since this uninstall's preview. Preserve it on a race.
|
|
37
185
|
async function uninstallAgentPluginUnlocked(required, optional = {}) {
|
|
38
186
|
const { workspaceRoot, packagesDir, matches } = await resolveUninstallTargets(required, optional.retiredBundled === true);
|
|
39
187
|
assertUnchangedSincePreview(required.pluginId, optional.confirmedPreview, matches);
|
|
40
188
|
if (optional.deleteMemory && (!optional.confirmedPreview || optional.confirmedPreview.pluginId !== required.pluginId)) {
|
|
41
|
-
throw new Error('Deleting
|
|
189
|
+
throw new Error('Deleting plugin memory requires a confirmed preview for this plugin');
|
|
42
190
|
}
|
|
43
191
|
const staged = [];
|
|
44
192
|
try {
|
|
@@ -66,11 +214,19 @@ function buildModule(ports) {
|
|
|
66
214
|
const pluginRoot = required.layout.forWorkspace({ workspaceId: required.workspaceId }).pluginRootDir({ pluginId: required.pluginId });
|
|
67
215
|
await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, pluginRoot));
|
|
68
216
|
if ((await listInstalledPlugins(workspaceRoot)).some(plugin => plugin.pluginId === required.pluginId))
|
|
69
|
-
throw new Error('Plugin was reinstalled; persistent
|
|
217
|
+
throw new Error('Plugin was reinstalled during uninstall; persistent memory was kept');
|
|
70
218
|
await rm(pluginRoot, { recursive: true, force: true });
|
|
71
219
|
}
|
|
72
220
|
return { pluginId: required.pluginId, removedDigests: matches.map((plugin) => plugin.archiveDigest) };
|
|
73
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Refuses when the digests resolved now are not exactly the set a confirmed preview listed — an archive added
|
|
224
|
+
* OR removed for this id while the dialog was open. Runs in the same call that stages the removal, so it opens
|
|
225
|
+
* no new check-then-act gap of its own.
|
|
226
|
+
*
|
|
227
|
+
* @throws {AgentPluginChangedSincePreviewError} The two digest sets differ.
|
|
228
|
+
* @complexity O(d log d) in the digest count.
|
|
229
|
+
*/
|
|
74
230
|
function assertUnchangedSincePreview(pluginId, confirmedPreview, matches) {
|
|
75
231
|
if (confirmedPreview === undefined)
|
|
76
232
|
return;
|
|
@@ -81,6 +237,14 @@ function buildModule(ports) {
|
|
|
81
237
|
throw new AgentPluginChangedSincePreviewError({ message: `Agent Plugin '${pluginId}' changed after its uninstall was previewed (previewed archive digests: ${previewed.join(", ")}; ` +
|
|
82
238
|
`installed now: ${installedNow.join(", ")}) — nothing was removed` });
|
|
83
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Renames one package root aside, inside its own parent directory, so removing it becomes
|
|
242
|
+
* reversible. Unfreezes the root directory itself first — a frozen directory cannot be renamed at
|
|
243
|
+
* all (see this file's header) — and re-freezes it if the rename fails, so a package this function
|
|
244
|
+
* could not stage is left exactly as it found it.
|
|
245
|
+
*
|
|
246
|
+
* @complexity One `stat`, one or two `chmod`s, one `rename`.
|
|
247
|
+
*/
|
|
84
248
|
async function stageForRemoval(packagesDir, packageRoot) {
|
|
85
249
|
const quarantined = path.join(packagesDir, `${STAGED_DIRNAME_PREFIX}${randomUUID()}`);
|
|
86
250
|
const originalMode = (await stat(packageRoot)).mode & 0o777;
|
|
@@ -94,6 +258,13 @@ function buildModule(ports) {
|
|
|
94
258
|
}
|
|
95
259
|
return { quarantined, original: packageRoot, originalMode };
|
|
96
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Puts every staged tree back, newest first, and returns the error `uninstallAgentPlugin` should
|
|
263
|
+
* throw: `cause` when everything was restored, or — when some tree could not be — an error naming
|
|
264
|
+
* the staged paths, since those directories hold the only remaining copy of that package.
|
|
265
|
+
*
|
|
266
|
+
* @complexity O(s) renames in the staged-tree count.
|
|
267
|
+
*/
|
|
97
268
|
async function restoreStagedTrees(staged, cause) {
|
|
98
269
|
const stranded = [];
|
|
99
270
|
for (const tree of [...staged].reverse()) {
|
|
@@ -111,6 +282,23 @@ function buildModule(ports) {
|
|
|
111
282
|
return new Error(`Agent Plugin uninstall failed (${reason}) and ${stranded.length} package tree(s) could not be put back. ` +
|
|
112
283
|
`They still hold the package bytes and were left in place for recovery: ${stranded.join(", ")}`, { cause });
|
|
113
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Finds `pluginId`'s installed digests in this workspace and applies both refusals.
|
|
287
|
+
*
|
|
288
|
+
* @throws {AgentPluginNotFoundError} No match on disk.
|
|
289
|
+
* @throws {AgentPluginNotUninstallableError} The activation record says `origin: "bundled"`, or holds a malformed
|
|
290
|
+
* entry for this plugin.
|
|
291
|
+
* @throws {AgentPluginActivationsUnreadableError} The activation record exists but could not be
|
|
292
|
+
* read, so whether it says `origin: "bundled"` cannot be established.
|
|
293
|
+
* @complexity O(d) in the installed-digest count.
|
|
294
|
+
*/
|
|
295
|
+
// The one call that turns (instance layout, workspaceId) into real, internally-consistent
|
|
296
|
+
// packages/root paths — see this file's header, "Why this module resolves its own workspace
|
|
297
|
+
// layout". `forWorkspace` itself still throws for a syntactically invalid `workspaceId`.
|
|
298
|
+
// The bundled read above answers `false` for a present-but-malformed entry (a non-object, a non-boolean
|
|
299
|
+
// `enabled`), which says nothing about provenance. The gate reads that entry as `undetermined` and denies; an
|
|
300
|
+
// uninstall does the same rather than remove what may be a bundled plugin the next boot re-seeds (t91 R4). A
|
|
301
|
+
// file that became unreadable between the two reads also lands here, and is refused the same way.
|
|
114
302
|
async function resolveUninstallTargets(required, retiredBundled = false) {
|
|
115
303
|
const { layout, workspaceId, pluginId } = required;
|
|
116
304
|
const workspaceLayout = layout.forWorkspace({ workspaceId });
|
|
@@ -127,17 +315,29 @@ function buildModule(ports) {
|
|
|
127
315
|
}
|
|
128
316
|
return { workspaceRoot: workspaceLayout.root, packagesDir: workspaceLayout.pluginPackagesDir({ pluginId }), matches };
|
|
129
317
|
}
|
|
318
|
+
/** Names the levers that do work for a bundled plugin — see this file's header, item 2. */
|
|
130
319
|
function bundledRefusalMessage(pluginId) {
|
|
131
320
|
return (`Agent Plugin '${pluginId}' is bundled with ${ports.productName} and cannot be uninstalled — it is re-seeded on every boot ` +
|
|
132
321
|
"(seed-bundled.ts), so removing its files now would silently reappear on the next restart. To stop it being " +
|
|
133
|
-
"used, disable it through the host's activation control.");
|
|
322
|
+
(ports.formatBundledUninstallRecovery?.({ pluginId }, {}) ?? "used, disable it through the host's activation control."));
|
|
134
323
|
}
|
|
324
|
+
/** Path-free: says why provenance is unknown and what repairs it. Does not suggest toggling the plugin, which would
|
|
325
|
+
* rewrite a garbled bundled entry as `operator-installed`. */
|
|
135
326
|
function malformedEntryRefusalMessage(pluginId) {
|
|
136
327
|
return (`Agent Plugin '${pluginId}' cannot be uninstalled: its entry in this workspace's activation record is malformed, so ` +
|
|
137
328
|
`whether it is bundled with ${ports.productName} (and would be re-seeded on the next boot) cannot be established. Nothing was ` +
|
|
138
329
|
"removed. Tell the user an operator has to repair that entry in activations.json first; until then this " +
|
|
139
330
|
"plugin's tool calls and plugin-pinned runs are refused.");
|
|
140
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Restores write permission through `root`'s whole tree, then removes it — the production
|
|
334
|
+
* counterpart of `__tests__/fixtures/force-remove.ts` (that file's own header explains why a plain
|
|
335
|
+
* recursive `rm` cannot work here: `install.ts`'s `freezeTree` leaves every directory `0o555`).
|
|
336
|
+
* Deliberately not shared code with that test fixture: the fixture is intentionally test-only and
|
|
337
|
+
* this is the first production need for the same operation, not a refactor opportunity forced by it.
|
|
338
|
+
*
|
|
339
|
+
* @complexity O(f) in the file/directory count under `root`.
|
|
340
|
+
*/
|
|
141
341
|
async function removeFrozenPackageTree(root) {
|
|
142
342
|
await makeTreeWritable(root);
|
|
143
343
|
await rm(root, { recursive: true, force: true });
|
|
@@ -161,7 +361,7 @@ function buildModule(ports) {
|
|
|
161
361
|
}
|
|
162
362
|
}
|
|
163
363
|
}
|
|
164
|
-
return { previewAgentPluginUninstall, uninstallAgentPlugin };
|
|
364
|
+
return { previewAgentPluginUninstall, uninstallAgentPlugin, stageForRemoval, restoreStagedTrees, removeFrozenPackageTree };
|
|
165
365
|
}
|
|
166
366
|
const instances = new WeakMap();
|
|
167
367
|
/** Internal composition; one state/queue instance per injected host context. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Historical lifecycle rationale, carried with the implementation during consumer adoption.
|
|
3
2
|
* @file The real `AgentPluginArchiveReaderPort` adapter, backed by `yauzl`.
|
|
4
3
|
*
|
|
5
4
|
* Library choice (owner-decided, recorded so it is not re-litigated): `yauzl` — streaming,
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/agent-plugins",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
"./lifecycle/yauzl": "node",
|
|
21
21
|
"./manifest": "universal",
|
|
22
22
|
"./persistent-state": "node"
|
|
23
|
+
},
|
|
24
|
+
"isolation": {
|
|
25
|
+
"entries": {
|
|
26
|
+
"./manifest": "lifecycle",
|
|
27
|
+
"./persistent-state": "core"
|
|
28
|
+
},
|
|
29
|
+
"shared": [
|
|
30
|
+
"core/persistent-state.js",
|
|
31
|
+
"persistent-state.js"
|
|
32
|
+
]
|
|
23
33
|
}
|
|
24
34
|
},
|
|
25
35
|
"type": "module",
|
|
@@ -69,7 +79,8 @@
|
|
|
69
79
|
"!dist/**/*.test.d.ts.map",
|
|
70
80
|
"!dist/**/__tests__/**",
|
|
71
81
|
"persistent-state.js",
|
|
72
|
-
"persistent-state.d.ts"
|
|
82
|
+
"persistent-state.d.ts",
|
|
83
|
+
"core"
|
|
73
84
|
],
|
|
74
85
|
"publishConfig": {
|
|
75
86
|
"access": "public",
|
|
@@ -77,20 +88,23 @@
|
|
|
77
88
|
},
|
|
78
89
|
"devDependencies": {
|
|
79
90
|
"@vitest/coverage-v8": "^2.1.9",
|
|
80
|
-
"yazl": "^3.3.1"
|
|
91
|
+
"yazl": "^3.3.1",
|
|
92
|
+
"@jini-ai/core": "^0.4.2",
|
|
93
|
+
"@jini-ai/platform": "^0.5.0"
|
|
81
94
|
},
|
|
82
95
|
"peerDependencies": {
|
|
83
|
-
"yauzl": "^3.4.0"
|
|
96
|
+
"yauzl": "^3.4.0",
|
|
97
|
+
"@jini-ai/core": "^0.4.3",
|
|
98
|
+
"@jini-ai/platform": "^0.5.3"
|
|
84
99
|
},
|
|
85
100
|
"peerDependenciesMeta": {
|
|
86
101
|
"yauzl": {
|
|
87
102
|
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"@jini-ai/platform": {
|
|
105
|
+
"optional": true
|
|
88
106
|
}
|
|
89
107
|
},
|
|
90
|
-
"dependencies": {
|
|
91
|
-
"@jini-ai/core": "^0.4.0",
|
|
92
|
-
"@jini-ai/platform": "^0.5.0"
|
|
93
|
-
},
|
|
94
108
|
"scripts": {
|
|
95
109
|
"build": "tsc -p tsconfig.json",
|
|
96
110
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|