@jini-ai/agent-plugins 0.4.0 → 0.4.2

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +19 -0
  3. package/core/persistent-state.d.ts +25 -0
  4. package/core/persistent-state.js +260 -0
  5. package/dist/lifecycle/activation.js +38 -166
  6. package/dist/lifecycle/bundled-digests.d.ts +6 -0
  7. package/dist/lifecycle/bundled-digests.js +156 -0
  8. package/dist/lifecycle/bundled-source-archive.d.ts +3 -0
  9. package/dist/lifecycle/bundled-source-archive.js +74 -0
  10. package/dist/lifecycle/contribution-loader.d.ts +102 -0
  11. package/dist/lifecycle/contribution-loader.js +92 -0
  12. package/dist/lifecycle/fetch-archive.d.ts +1 -2
  13. package/dist/lifecycle/index.d.ts +34 -4
  14. package/dist/lifecycle/index.js +19 -1
  15. package/dist/lifecycle/install-from-url.d.ts +2 -5
  16. package/dist/lifecycle/install-from-url.js +2 -5
  17. package/dist/lifecycle/install.d.ts +57 -2
  18. package/dist/lifecycle/install.js +228 -13
  19. package/dist/lifecycle/layout.d.ts +10 -0
  20. package/dist/lifecycle/layout.js +9 -1
  21. package/dist/lifecycle/manifest.d.ts +3 -9
  22. package/dist/lifecycle/manifest.js +4 -11
  23. package/dist/lifecycle/package-paths.d.ts +2 -0
  24. package/dist/lifecycle/package-paths.js +62 -0
  25. package/dist/lifecycle/persistent-state.d.ts +24 -0
  26. package/dist/lifecycle/persistent-state.js +32 -0
  27. package/dist/lifecycle/ports.d.ts +15 -3
  28. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +14 -2
  29. package/dist/lifecycle/resolve-agent-plugin-refs.js +173 -10
  30. package/dist/lifecycle/retire-bundled.d.ts +50 -1
  31. package/dist/lifecycle/retire-bundled.js +10 -3
  32. package/dist/lifecycle/search.d.ts +63 -0
  33. package/dist/lifecycle/search.js +29 -0
  34. package/dist/lifecycle/seed-bundled.d.ts +18 -1
  35. package/dist/lifecycle/seed-bundled.js +139 -1
  36. package/dist/lifecycle/set-enabled.d.ts +7 -0
  37. package/dist/lifecycle/set-enabled.js +6 -1
  38. package/dist/lifecycle/trusted-plugin-files.d.ts +38 -0
  39. package/dist/lifecycle/trusted-plugin-files.js +19 -1
  40. package/dist/lifecycle/types.d.ts +12 -0
  41. package/dist/lifecycle/uninstall.d.ts +30 -0
  42. package/dist/lifecycle/uninstall.js +232 -4
  43. package/dist/lifecycle/yauzl-archive-reader.d.ts +0 -1
  44. package/dist/lifecycle/yauzl-archive-reader.js +5 -4
  45. package/package.json +34 -9
  46. package/persistent-state.d.ts +25 -0
  47. package/persistent-state.js +2 -0
@@ -1,3 +1,65 @@
1
+ /**
2
+ * @file The per-workspace ledger of WHICH installed digest each bundled Agent Plugin currently
3
+ * ships as — written by `seed-bundled.ts` on every boot, read by the surfaces that have to pick one
4
+ * installed package for a plugin id.
5
+ *
6
+ * ---------------------------------------------------------------------------
7
+ * The gap this closes
8
+ * ---------------------------------------------------------------------------
9
+ * `packages/sha256/<digest>/` is content-addressed and nothing retires an entry, so shipping a NEW
10
+ * VERSION of an already-installed bundled plugin lands it ALONGSIDE the old one. Both consumption
11
+ * surfaces then refuse that plugin outright as ambiguous — `resolve-agent-plugin-refs.ts`'s
12
+ * "refusing to guess which one to use" and `tool-registrations.ts`'s `poisonedPluginIds`. A product
13
+ * upgrade therefore broke every enabled bundled plugin whose bytes it touched, silently, on the next
14
+ * boot. Observed live: `higgsfield-media` (2 digests, enabled, tools gone), `supabase` (3 digests,
15
+ * dormant), and `github` one boot away from the same fate.
16
+ *
17
+ * ---------------------------------------------------------------------------
18
+ * Why a ledger rather than deleting, or than "prefer the newest"
19
+ * ---------------------------------------------------------------------------
20
+ * The ambiguity refusal itself is deliberate and stays exactly as it was — `resolve-agent-plugin-
21
+ * refs.ts`'s header argues at length that silently picking one (the lexicographically-last, the
22
+ * newest mtime, whatever hashed later) would be a WORSE failure mode than an explicit error, because
23
+ * a wrong plugin's content would reach the agent and nothing about the run would look wrong. That
24
+ * reasoning is untouched here. What changes is that for a BUNDLED plugin there is now something
25
+ * authoritative to consult instead of guessing: the build's own seed recorded which digest it
26
+ * published. A digest wins because the host shipped it, never because it sorted first.
27
+ *
28
+ * Three consequences worth stating plainly, because each one is a refusal this file deliberately
29
+ * keeps:
30
+ *
31
+ * 1. **An id with no ledger entry is still ambiguous.** Two digests of a plugin the seeder never
32
+ * seeded — an operator's own installs — remain an explicit error. Nothing here has any authority
33
+ * over a package the host did not put on disk.
34
+ * 2. **A ledger entry that names a digest which is not installed decides nothing.** The named digest
35
+ * has to be present, and be exactly one of the candidates, or the ambiguity stands.
36
+ * 3. **An unreadable ledger reads as an empty one.** That is fail-CLOSED here, unlike
37
+ * `activation.ts`'s own lenient reader: an empty ledger names no winner, so every ambiguity stays
38
+ * an error. This file can only ever resolve an ambiguity, never create one.
39
+ *
40
+ * ---------------------------------------------------------------------------
41
+ * Nothing is deleted, moved, or renamed
42
+ * ---------------------------------------------------------------------------
43
+ * A superseded digest's bytes stay exactly where they are. Retiring by moving directories out of a
44
+ * live `packages/sha256/` at boot was the other candidate design and was rejected: it mutates the
45
+ * operator's package store on a code path whose whole job is supposed to be idempotent, it races
46
+ * anything mid-read, and — since a digest's provenance is NOT recorded per-digest anywhere — a
47
+ * workspace that already held an operator's own install under a bundled plugin's id could have had
48
+ * that operator's bytes moved aside by an inference. Preferring a digest at READ time disturbs
49
+ * nothing and is undone by deleting one small JSON file. The cost is disk: superseded packages
50
+ * accumulate until an operator prunes them, which is their call to make and reversible either way.
51
+ *
52
+ * Architectural role:
53
+ * Owns one small JSON file and one pure selection rule. No knowledge of activation, installation, or
54
+ * tools — `seed-bundled.ts` is the only writer (plus `retire-bundled.ts`, which it calls in the same
55
+ * boot step), and the read side is two call sites that already
56
+ * hold both a workspace root and a list of installed packages.
57
+ */
58
+ /** Where a workspace's bundled-digest ledger lives, beside `activation.ts`'s `activations.json` and
59
+ * for the same reason: it is a fact about this workspace, not about a package. */
60
+ /** The same 64-lowercase-hex shape `resolve-agent-plugin-refs.ts` requires of an installed package
61
+ * directory name. A ledger entry that does not match it could never name a real install. */
62
+ /** The Agent Plugins name grammar, as `activation.ts` and `layout.ts` both already enforce it. */
1
63
  /** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
2
64
  import path from "node:path";
3
65
  export const BUNDLED_DIGESTS_FILENAME = "bundled-digests.json";
@@ -10,6 +72,17 @@ function buildModule(ports) {
10
72
  const SHA256_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
11
73
  const SAFE_PLUGIN_ID_PATTERN = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
12
74
  const EMPTY_LEDGER = new Map();
75
+ /**
76
+ * Reads one workspace's bundled-digest ledger.
77
+ *
78
+ * @param workspaceRoot - `AgentPluginWorkspaceLayout.root`, never an instance-level path.
79
+ * @returns The recorded plugin-id -> digest map, or an EMPTY map for a missing, unreadable,
80
+ * malformed, or wrong-version file. Malformed INDIVIDUAL entries are skipped rather than discarding
81
+ * the whole ledger, matching `activation.ts`'s own per-entry discipline.
82
+ * @throws Nothing. An empty answer is always safe: it names no winner, so every ambiguity this
83
+ * module could have resolved simply stays the explicit error it already was.
84
+ * @complexity One file read plus O(p) in the recorded plugin count.
85
+ */
13
86
  async function readBundledAgentPluginDigests(workspaceRoot) {
14
87
  let raw;
15
88
  try {
@@ -27,6 +100,12 @@ function buildModule(ports) {
27
100
  }
28
101
  return normalizeBundledDigests(parsed);
29
102
  }
103
+ /**
104
+ * Validates an already-`JSON.parse`d ledger value, dropping anything that does not match the shape.
105
+ * Pure, so the parsing rules are assertable without touching a filesystem.
106
+ *
107
+ * @complexity O(p) in the entry count.
108
+ */
30
109
  function normalizeBundledDigests(value) {
31
110
  if (!isPlainObject(value) || value.schemaVersion !== 1 || !isPlainObject(value.plugins))
32
111
  return EMPTY_LEDGER;
@@ -43,6 +122,27 @@ function buildModule(ports) {
43
122
  }
44
123
  return digests;
45
124
  }
125
+ /**
126
+ * Records the digest this boot seeded for each bundled plugin, merged over whatever was already
127
+ * recorded.
128
+ *
129
+ * MERGED, not replaced, for failure isolation (the property `seed-bundled.ts`'s header names): a
130
+ * plugin that failed to seed this boot — an unreadable source directory, a transient I/O fault —
131
+ * must not lose the perfectly good entry an earlier boot wrote for it, which is what a wholesale
132
+ * rewrite of only this boot's successes would do.
133
+ *
134
+ * A plugin id seeded TWICE in one run under two different digests (two bundled source directories
135
+ * declaring the same manifest `name`) is dropped from the ledger entirely, prior entry included:
136
+ * the build itself is ambiguous about what it ships, so there is nothing authoritative to record and
137
+ * the explicit ambiguity refusal downstream is the correct outcome.
138
+ *
139
+ * @param seeded - One entry per successfully seeded plugin. An empty list writes nothing at all —
140
+ * not even an empty file — so a boot that seeded nothing cannot create a workspace directory or
141
+ * truncate an existing ledger.
142
+ * @throws {Error} Any filesystem fault from the write. The caller (`seed-bundled.ts`) captures it
143
+ * into its report rather than failing the boot.
144
+ * @complexity One file read and one atomic write, O(p) in the recorded plugin count.
145
+ */
46
146
  async function recordBundledAgentPluginDigests(required) {
47
147
  if (required.seeded.length === 0)
48
148
  return;
@@ -58,6 +158,15 @@ function buildModule(ports) {
58
158
  const plugins = Object.fromEntries([...merged].map(([pluginId, archiveDigest]) => [pluginId, { archiveDigest, seededAt }]));
59
159
  await writeLedgerAtomically(required.workspaceRoot, plugins);
60
160
  }
161
+ /**
162
+ * This boot's verdict per plugin id: the digest it seeded, or `undefined` when the run seeded the
163
+ * same id under two different digests and therefore has no authoritative answer for it.
164
+ *
165
+ * Split out as a pure step so the "a self-contradictory build records NOTHING" rule is assertable
166
+ * without a filesystem, and so {@link recordBundledAgentPluginDigests} reads as merge-then-write.
167
+ *
168
+ * @complexity O(n) in the seeded-entry count.
169
+ */
61
170
  function resolveThisBootsDigests(seeded) {
62
171
  const thisBoot = new Map();
63
172
  for (const { pluginId, archiveDigest } of seeded) {
@@ -70,6 +179,19 @@ function buildModule(ports) {
70
179
  }
71
180
  return thisBoot;
72
181
  }
182
+ /**
183
+ * Drops one plugin id from the ledger — for `retire-bundled.ts`, once a retired bundled plugin's
184
+ * packages are gone and no build will ever seed it again.
185
+ *
186
+ * Every other entry is kept byte-for-byte, `seededAt` included: this rewrites the raw `plugins` bag
187
+ * rather than the normalized map, so it cannot lose a field a later schema adds. A missing,
188
+ * unreadable, malformed, or wrong-version ledger, or one with no entry for `pluginId`, writes
189
+ * nothing — there is nothing to drop, and a fail-closed reader already treats such a file as empty.
190
+ *
191
+ * @returns Whether an entry was removed.
192
+ * @throws {Error} Any filesystem fault from the write. `retire-bundled.ts` captures it into its report.
193
+ * @complexity One file read and at most one atomic write, O(p) in the recorded plugin count.
194
+ */
73
195
  async function removeBundledAgentPluginDigest(required) {
74
196
  let parsed;
75
197
  try {
@@ -87,6 +209,11 @@ function buildModule(ports) {
87
209
  await writeLedgerAtomically(required.workspaceRoot, remaining);
88
210
  return true;
89
211
  }
212
+ /** Temp-file-then-`rename`, so a reader never observes a half-written ledger and a crash mid-write
213
+ * leaves the previous one intact — the same durability shape `activation.ts`'s
214
+ * `writeActivationsAtomically` uses, minus its cross-process lock: this file has exactly one writer
215
+ * (the boot seeder, retirement included), and two concurrent boots of the SAME build write byte-identical content.
216
+ * @complexity One write and one rename. */
90
217
  async function writeLedgerAtomically(workspaceRoot, plugins) {
91
218
  const finalPath = path.join(workspaceRoot, BUNDLED_DIGESTS_FILENAME);
92
219
  const tempPath = `${finalPath}.tmp-${ports.process.pid}-${randomUUID()}`;
@@ -106,6 +233,24 @@ function buildModule(ports) {
106
233
  throw error;
107
234
  }
108
235
  }
236
+ /**
237
+ * Drops the SUPERSEDED installs of any bundled plugin id from a list of installed packages, leaving
238
+ * every other entry — and every other plugin id — exactly as it was.
239
+ *
240
+ * An id is narrowed only when all three hold: it has more than one installed digest, the ledger
241
+ * records a digest for it, and exactly one of its installed digests is that recorded one. Anything
242
+ * else passes through untouched, so the caller's own ambiguity refusal still fires for every case
243
+ * this module has no authority over (this file's header, consequences 1 and 2).
244
+ *
245
+ * Applied at READ time by the surfaces that must pick one package per plugin id. Deliberately NOT
246
+ * folded into `listInstalledPlugins` itself: `uninstall.ts` needs to see EVERY digest of a plugin to
247
+ * remove them, and a filter hidden inside the shared walk would make superseded bytes unremovable.
248
+ *
249
+ * @returns A list in the input's own order, with superseded entries removed. The input is never
250
+ * mutated.
251
+ * @complexity O(n) in the installed-package count, over one O(n) pre-pass that counts digests per
252
+ * plugin id.
253
+ */
109
254
  function preferBundledAgentPluginDigests(installed, bundled) {
110
255
  if (bundled.size === 0)
111
256
  return installed;
@@ -114,6 +259,12 @@ function buildModule(ports) {
114
259
  return installed;
115
260
  return installed.filter((plugin) => !supersededIds.has(plugin.pluginId) || plugin.archiveDigest === bundled.get(plugin.pluginId));
116
261
  }
262
+ /**
263
+ * The plugin ids whose installed digests the ledger can authoritatively narrow to one — every id
264
+ * that is installed under several digests, exactly one of which is the digest this build seeded.
265
+ *
266
+ * @complexity O(n) in the installed-package count.
267
+ */
117
268
  function idsNarrowableToTheirBundledDigest(installed, bundled) {
118
269
  const digestsByPluginId = new Map();
119
270
  for (const plugin of installed) {
@@ -131,6 +282,11 @@ function buildModule(ports) {
131
282
  }
132
283
  return narrowable;
133
284
  }
285
+ /** Whether `value` is a plain (non-null, non-array) object — the shape both this file's JSON-adjacent
286
+ * values must have before their own fields are worth looking at. Duplicated from `activation.ts`'s
287
+ * private helper of the same name rather than widening that module's public surface, matching this
288
+ * codebase's own precedent for small private formatting/shape helpers (see `resolve-agent-plugin-
289
+ * refs.ts`'s note on `humanize()`). */
134
290
  function isPlainObject(value) {
135
291
  return typeof value === "object" && value !== null && !Array.isArray(value);
136
292
  }
@@ -2,7 +2,10 @@ import type { AgentPluginArchiveReaderPort } from "./install.js";
2
2
  import type { AgentPluginLifecyclePorts } from "./ports.js";
3
3
  export interface PackedAgentPluginArchive {
4
4
  readonly bytes: Uint8Array;
5
+ /** Lowercase hex SHA-256 of {@link bytes} — passed straight to `installAgentPlugin`'s
6
+ * `expectedSha256`, which recomputes and compares it rather than trusting it. */
5
7
  readonly sha256: string;
8
+ /** Package-relative POSIX paths included, sorted — the same order they appear in `bytes`. */
6
9
  readonly files: readonly string[];
7
10
  }
8
11
  /** Internal composition; one state/queue instance per injected host context. */
@@ -1,3 +1,45 @@
1
+ /**
2
+ * @file Turns a bundled Agent Plugin's SOURCE DIRECTORY (tracked in this repo, shipped in the
3
+ * product) into the `{ archive: Uint8Array, expectedSha256, archiveReader }` triple
4
+ * `installAgentPlugin` requires — so a pre-placed package goes through the exact same install path
5
+ * as one downloaded from a URL, rather than around it.
6
+ *
7
+ * ---------------------------------------------------------------------------
8
+ * Why not just copy the directory
9
+ * ---------------------------------------------------------------------------
10
+ * Because `installAgentPlugin` is where every guarantee lives: lexical and symlink zip-slip
11
+ * refusal, entry/file/total-byte caps, atomic staging-then-publish, `chmod 0o555` freezing,
12
+ * content-addressed dedup, and the tenant-scoped `forWorkspace()` resolution that makes it
13
+ * impossible to publish into another workspace's tree. A `cp -R` in a seeder would reimplement
14
+ * none of them, and every one it skipped would be a guarantee that silently applies to
15
+ * operator-installed plugins and not to the one the host ships itself.
16
+ *
17
+ * The obstacle is only that `installAgentPlugin` speaks archives, and a directory is not one.
18
+ * {@link packAgentPluginDirectory} therefore serialises the directory into a tiny, deterministic
19
+ * archive format, and {@link createBundledSourceArchiveReader} reads it back — a round trip, not a
20
+ * bypass. `yauzl`/`yazl` were the obvious alternative and are wrong here: `yazl` is a
21
+ * devDependency used only by dev tooling and test fixtures (`development/scripts/package-agent-plugin.ts`
22
+ * says so in its own header, "must never be imported by production code"), and DEFLATE would make
23
+ * the digest depend on a compressor's version rather than on the bytes.
24
+ *
25
+ * ---------------------------------------------------------------------------
26
+ * The digest is a real content digest
27
+ * ---------------------------------------------------------------------------
28
+ * `installAgentPlugin` verifies `sha256(archive) === expectedSha256` before extracting anything, and
29
+ * uses that digest as the content address. Because this format is byte-deterministic — sorted
30
+ * paths, explicit lengths, no timestamps, no permissions, no compression — the digest is a genuine
31
+ * function of the package's own file contents and names. Two builds of the same source tree produce
32
+ * the same digest, which is what makes seeding idempotent (`install.ts`'s `alreadyPublished` fast
33
+ * path) instead of re-extracting on every boot.
34
+ *
35
+ * Architectural role:
36
+ * One directory walk (the only I/O) plus a pure encoder/decoder pair. `seed-bundled.ts` composes it
37
+ * with `installAgentPlugin`.
38
+ */
39
+ /** Magic + version line. A reader that does not see this refuses rather than guessing. */
40
+ /** Guards a caller that points this at the wrong directory (a `node_modules`, a whole repo). Well
41
+ * above any real plugin; below `install.ts`'s own `maxEntries` of 4096 so the friendlier error
42
+ * comes from here. */
1
43
  /** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
2
44
  import { createHash } from "node:crypto";
3
45
  import path from "node:path";
@@ -6,6 +48,20 @@ function buildModule(ports) {
6
48
  const readFile = ports.filesystem.readFile.bind(ports.filesystem);
7
49
  const MAGIC = ports.bundledArchiveMagic;
8
50
  const MAX_SOURCE_FILES = 2048;
51
+ /**
52
+ * Serialises every regular file under `sourceDir` into one deterministic archive.
53
+ *
54
+ * Symlinks and any other non-regular entry are skipped rather than followed: a bundled package is
55
+ * this repository's own tracked content, so a symlink in it would be a mistake, and following one
56
+ * is the exact vector `install.ts` refuses on the extraction side. Skipping keeps the two sides
57
+ * agreeing instead of producing an archive the installer would then reject.
58
+ *
59
+ * @throws {Error} If the tree holds more than {@link MAX_SOURCE_FILES} files.
60
+ * @throws Propagates `readdir`/`readFile` errors — a bundled package that cannot be read is a build
61
+ * defect, not a runtime condition to degrade around.
62
+ * @complexity O(f) files, O(b) total bytes; both bounded by the cap above and by `install.ts`'s own
63
+ * `LIMITS` on the extraction side.
64
+ */
9
65
  async function packAgentPluginDirectory(sourceDir) {
10
66
  const relativePaths = (await listRegularFiles(sourceDir, "")).sort();
11
67
  if (relativePaths.length > MAX_SOURCE_FILES) {
@@ -24,6 +80,16 @@ function buildModule(ports) {
24
80
  files: relativePaths,
25
81
  };
26
82
  }
83
+ /**
84
+ * Reads the format {@link packAgentPluginDirectory} writes, as an `AgentPluginArchiveReaderPort`.
85
+ *
86
+ * Yields only `kind: "file"` entries — the format has no directory, symlink, or device entries to
87
+ * represent, which is itself a small safety property: the whole class of entry `install.ts` rejects
88
+ * cannot be expressed in an archive this reader produces.
89
+ *
90
+ * @complexity O(n) in the archive's byte length; each file's bytes are yielded once, not copied
91
+ * into an intermediate list.
92
+ */
27
93
  function createBundledSourceArchiveReader() {
28
94
  return {
29
95
  entries({ archive }) {
@@ -64,9 +130,16 @@ function buildModule(ports) {
64
130
  offset = contentEnd;
65
131
  }
66
132
  }
133
+ /** One-shot async iterable over a single chunk — the shape `AgentPluginArchiveEntry.openReadStream`
134
+ * wants, without pulling in a stream implementation for an already-in-memory buffer. */
67
135
  async function* onceAsyncIterable(chunk) {
68
136
  yield chunk;
69
137
  }
138
+ /**
139
+ * Lists every regular file under `dir`, as POSIX-style package-relative paths.
140
+ *
141
+ * @complexity O(f) in filesystem entries under `dir`.
142
+ */
70
143
  async function listRegularFiles(dir, prefix) {
71
144
  const entries = await readdir(dir, { withFileTypes: true });
72
145
  const files = [];
@@ -78,6 +151,7 @@ function buildModule(ports) {
78
151
  else if (entry.isFile()) {
79
152
  files.push(relative);
80
153
  }
154
+ // Symlinks and everything else: skipped. See this function's own doc.
81
155
  }
82
156
  return files;
83
157
  }
@@ -0,0 +1,102 @@
1
+ import type { TrustedPluginPackage, TrustedPluginPackagesQuery, TrustedPluginVerdict } from './trusted-plugin-files.js';
2
+ export type PluginContributionParseResult<T> = {
3
+ readonly ok: true;
4
+ readonly descriptors: readonly T[];
5
+ } | {
6
+ readonly ok: false;
7
+ readonly reason: string;
8
+ };
9
+ export interface PluginContributionResult<T> {
10
+ readonly items: readonly T[];
11
+ readonly refusals: readonly string[];
12
+ }
13
+ export type PluginModuleImportPort = (input: {
14
+ readonly plugin: TrustedPluginPackage;
15
+ readonly modulePath: string;
16
+ }) => Promise<{
17
+ readonly exported: unknown;
18
+ } | string>;
19
+ export interface PluginContributionDefinition<D, T> {
20
+ readonly filename: string;
21
+ readonly contribution: string;
22
+ readonly kind: 'data' | 'executable';
23
+ readonly parse: (input: {
24
+ readonly raw: string;
25
+ }) => PluginContributionParseResult<D>;
26
+ /** A string is the complete refusal line; successful values retain declaration order. */
27
+ readonly load: (input: {
28
+ readonly plugin: TrustedPluginPackage;
29
+ readonly descriptor: D;
30
+ readonly importModule: PluginModuleImportPort;
31
+ }) => T | string | Promise<T | string>;
32
+ /** Undefined propagates the fault; [] deliberately tolerates a missing source manifest. */
33
+ readonly onReadError?: (input: {
34
+ readonly plugin: TrustedPluginPackage;
35
+ readonly error: unknown;
36
+ }) => readonly string[] | undefined;
37
+ }
38
+ export interface LoadedPluginContribution<D, M> {
39
+ readonly descriptor: D;
40
+ readonly pluginId: string;
41
+ readonly module: M;
42
+ }
43
+ export interface PluginContributionPorts {
44
+ readonly findPackages: (input: TrustedPluginPackagesQuery) => Promise<readonly TrustedPluginVerdict[]>;
45
+ readonly readFile: (input: {
46
+ readonly plugin: TrustedPluginPackage;
47
+ readonly filename: string;
48
+ }) => Promise<string>;
49
+ readonly importModule: PluginModuleImportPort;
50
+ }
51
+ export interface LoadPluginContributionsOptional {
52
+ /** Executable definitions always require activation, even if this option is false. */
53
+ readonly requireActive?: boolean;
54
+ readonly orderByPluginId?: boolean;
55
+ readonly onInactive?: TrustedPluginPackagesQuery['onInactive'];
56
+ readonly packageRefusal?: (input: {
57
+ readonly plugin: TrustedPluginPackage;
58
+ }) => string | undefined;
59
+ }
60
+ /** Bind traversal to the lifecycle's existing trust/read/import owner. Parser and error policies
61
+ * remain contribution ports: manifests and refusal contracts do not change.
62
+ * @param required - Trusted-file effects, injectable for direct failure and ordering tests.
63
+ * @returns Installed and explicitly trusted source loaders; reads are fresh on each call.
64
+ * @complexity O(p + d) packages and declarations, plus their file/import I/O; O(d + r) output space.
65
+ */
66
+ export declare function createPluginContributionLoader(required: PluginContributionPorts, _optional?: Record<string, never>): {
67
+ loadPluginContributions: <D, T>(input: {
68
+ readonly workspaceId: string;
69
+ readonly definition: PluginContributionDefinition<D, T>;
70
+ }, options?: LoadPluginContributionsOptional) => Promise<PluginContributionResult<T>>;
71
+ loadPluginContributionsFromSource: <D, T>(input: {
72
+ readonly plugin: TrustedPluginPackage;
73
+ readonly definition: PluginContributionDefinition<D, T>;
74
+ }, _options?: Record<string, never>) => Promise<PluginContributionResult<T>>;
75
+ };
76
+ /** Define executable materialization once; each domain owns export validation and refusal wording.
77
+ * @param required - Existing manifest parser, module path, validator and complete refusal formatter.
78
+ * @param optional - Established read/import recovery policy; absent policies propagate I/O faults.
79
+ * @returns A definition whose installed loader always applies activation before importing code.
80
+ * @complexity O(1) composition; each load performs one contained import and one validation.
81
+ */
82
+ export declare function defineExecutablePluginContribution<D, M>(required: {
83
+ readonly filename: string;
84
+ readonly contribution: string;
85
+ readonly parse: PluginContributionDefinition<D, LoadedPluginContribution<D, M>>['parse'];
86
+ readonly modulePath: (input: {
87
+ readonly descriptor: D;
88
+ }) => string;
89
+ readonly validate: (input: {
90
+ readonly exported: unknown;
91
+ }) => M | string;
92
+ readonly refusal: (input: {
93
+ readonly plugin: TrustedPluginPackage;
94
+ readonly descriptor: D;
95
+ readonly reason: string;
96
+ }) => string;
97
+ }, optional?: {
98
+ readonly onReadError?: PluginContributionDefinition<D, LoadedPluginContribution<D, M>>['onReadError'];
99
+ readonly importRefusalReason?: string;
100
+ readonly importErrorReason?: string;
101
+ }): PluginContributionDefinition<D, LoadedPluginContribution<D, M>>;
102
+ //# sourceMappingURL=contribution-loader.d.ts.map
@@ -0,0 +1,92 @@
1
+ /** Bind traversal to the lifecycle's existing trust/read/import owner. Parser and error policies
2
+ * remain contribution ports: manifests and refusal contracts do not change.
3
+ * @param required - Trusted-file effects, injectable for direct failure and ordering tests.
4
+ * @returns Installed and explicitly trusted source loaders; reads are fresh on each call.
5
+ * @complexity O(p + d) packages and declarations, plus their file/import I/O; O(d + r) output space.
6
+ */
7
+ export function createPluginContributionLoader(required, _optional = {}) {
8
+ /** Source bypass is explicit: only the caller can establish that a source directory is trusted.
9
+ * Read faults propagate unless the definition supplies its established recovery policy. */
10
+ async function loadPluginContributionsFromSource(input, _options = {}) {
11
+ const { plugin, definition } = input;
12
+ let raw;
13
+ try {
14
+ raw = await required.readFile({ plugin, filename: definition.filename });
15
+ }
16
+ catch (error) {
17
+ const refusals = definition.onReadError?.({ plugin, error });
18
+ if (refusals === undefined)
19
+ throw error;
20
+ return { items: [], refusals };
21
+ }
22
+ const parsed = definition.parse({ raw });
23
+ if (!parsed.ok)
24
+ return { items: [], refusals: [`${definition.contribution} from '${plugin.pluginId}' were not loaded: ${definition.filename} is invalid: ${parsed.reason}`] };
25
+ const items = [];
26
+ const refusals = [];
27
+ for (const descriptor of parsed.descriptors) {
28
+ const loaded = await definition.load({ plugin, descriptor, importModule: required.importModule });
29
+ if (typeof loaded === 'string')
30
+ refusals.push(loaded);
31
+ else
32
+ items.push(loaded);
33
+ }
34
+ return { items, refusals };
35
+ }
36
+ /** Discover before loading, then interleave gate and manifest refusals in verdict order.
37
+ * Never fall back to source or bypass the bundled-digest gate for installed packages. */
38
+ async function loadPluginContributions(input, options = {}) {
39
+ const { definition } = input;
40
+ const { packageRefusal, requireActive = true, ...discovery } = options;
41
+ const verdicts = await required.findPackages({
42
+ ...discovery, workspaceId: input.workspaceId, filename: definition.filename,
43
+ contribution: definition.contribution, requireActive: definition.kind === 'executable' || requireActive,
44
+ });
45
+ const items = [];
46
+ const refusals = [];
47
+ for (const verdict of verdicts) {
48
+ if ('refusal' in verdict) {
49
+ refusals.push(verdict.refusal);
50
+ continue;
51
+ }
52
+ const refusal = packageRefusal?.({ plugin: verdict.trusted });
53
+ if (refusal !== undefined) {
54
+ refusals.push(refusal);
55
+ continue;
56
+ }
57
+ const loaded = await loadPluginContributionsFromSource({ plugin: verdict.trusted, definition });
58
+ items.push(...loaded.items);
59
+ refusals.push(...loaded.refusals);
60
+ }
61
+ return { items, refusals };
62
+ }
63
+ return { loadPluginContributions, loadPluginContributionsFromSource };
64
+ }
65
+ /** Define executable materialization once; each domain owns export validation and refusal wording.
66
+ * @param required - Existing manifest parser, module path, validator and complete refusal formatter.
67
+ * @param optional - Established read/import recovery policy; absent policies propagate I/O faults.
68
+ * @returns A definition whose installed loader always applies activation before importing code.
69
+ * @complexity O(1) composition; each load performs one contained import and one validation.
70
+ */
71
+ export function defineExecutablePluginContribution(required, optional = {}) {
72
+ return {
73
+ filename: required.filename, contribution: required.contribution, kind: 'executable', parse: required.parse,
74
+ ...(optional.onReadError ? { onReadError: optional.onReadError } : {}),
75
+ load: async ({ plugin, descriptor, importModule }) => {
76
+ let module;
77
+ try {
78
+ const imported = await importModule({ plugin, modulePath: required.modulePath({ descriptor }) });
79
+ module = typeof imported === 'string' ? optional.importRefusalReason ?? imported : required.validate({ exported: imported.exported });
80
+ }
81
+ catch (error) {
82
+ if (optional.importErrorReason === undefined)
83
+ throw error;
84
+ module = optional.importErrorReason;
85
+ }
86
+ if (typeof module === 'string')
87
+ return required.refusal({ plugin, descriptor, reason: module });
88
+ return { descriptor, pluginId: plugin.pluginId, module };
89
+ },
90
+ };
91
+ }
92
+ //# sourceMappingURL=contribution-loader.js.map
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Historical lifecycle rationale, carried with the implementation during consumer adoption.
3
- * @file The missing front half of the Agent Plugin install pipeline: URL -> verified bytes.
2
+ * @file The network front half of the Agent Plugin install pipeline: URL -> verified bytes.
4
3
  *
5
4
  * `install.ts` deliberately takes `archive: Uint8Array` and never a URL — it is a pure
6
5
  * verify/extract/publish step with no network surface at all. This module is the one place that
@@ -4,12 +4,13 @@ import type { AgentPluginLifecycleRequired, AgentPluginLifecycleOptional } from
4
4
  import type { AgentPluginDeliveryMode } from './resolve-agent-plugin-refs.js';
5
5
  import type { InstalledDigestIdentity } from './bundled-digests.js';
6
6
  export type { SeededAgentPluginOutcome, SeedBundledAgentPluginsRequired, SeedBundledAgentPluginsResult } from './seed-bundled.js';
7
- export type { RetireBundledAgentPluginsRequired, RetireBundledAgentPluginsOptional, RetiredAgentPluginOutcome } from './retire-bundled.js';
7
+ export type { RetireBundledAgentPluginsRequired, RetireBundledAgentPluginsOptional, RetiredAgentPluginOutcome, RetiredAgentPluginSuccessorOutcome } from './retire-bundled.js';
8
8
  export * from './ports.js';
9
9
  export * from './layout.js';
10
10
  export * from './manifest.js';
11
11
  export * from './fetch-archive.js';
12
12
  export * from './mcp-provisioning.js';
13
+ export * from './contribution-loader.js';
13
14
  export { createAgentPluginActivations, ACTIVATIONS_FILENAME, ACTIVATIONS_LOCK_FILENAME, AgentPluginActivationsBusyError, AgentPluginActivationsUnreadableError } from './activation.js';
14
15
  export { BUNDLED_DIGESTS_FILENAME } from './bundled-digests.js';
15
16
  export { AgentPluginInstallError } from './install.js';
@@ -23,7 +24,7 @@ export type { AgentPluginArchiveEntry, AgentPluginArchiveReaderPort, AgentPlugin
23
24
  export type { AgentPluginIntegrity, InstallAgentPluginFromUrlRequired, InstallAgentPluginFromUrlOptional, InstalledAgentPluginFromUrl } from './install-from-url.js';
24
25
  export type { AgentPluginDeliveryMode, ResolveAgentPluginRefsResult } from './resolve-agent-plugin-refs.js';
25
26
  export type { SetAgentPluginEnabledInput, SetAgentPluginEnabledResult } from './set-enabled.js';
26
- export type { UninstallAgentPluginRequired, UninstallAgentPluginOptional, UninstallAgentPluginResult, AgentPluginUninstallPreview } from './uninstall.js';
27
+ export type { UninstallAgentPluginRequired, UninstallAgentPluginOptional, UninstallAgentPluginResult, AgentPluginUninstallPreview, StagedTree } from './uninstall.js';
27
28
  export type { TrustedPluginPackage, TrustedPluginPackagesQuery, TrustedPluginVerdict } from './trusted-plugin-files.js';
28
29
  export type { AgentPluginSearchSkill, AgentPluginSearchCandidate, AgentPluginSearchMatch } from './search.js';
29
30
  /** One host context owns queues and effects. Keep this object and its ports stable for its lifetime.
@@ -67,12 +68,23 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
67
68
  readonly workspaceRoot: string;
68
69
  readonly pluginId: string;
69
70
  }) => Promise<boolean>;
70
- installAgentPlugin: (required: import("./install.js").InstallAgentPluginRequired, _optional?: import("./install.js").InstallAgentPluginOptional) => Promise<import("./types.js").InstalledAgentPlugin>;
71
+ installAgentPlugin: (required: import("./install.js").InstallAgentPluginRequired, optional?: import("./install.js").InstallAgentPluginOptional) => Promise<import("./types.js").InstalledAgentPlugin>;
71
72
  installAgentPluginFromUrl: (required: import("./install-from-url.js").InstallAgentPluginFromUrlRequired, optional?: import("./install-from-url.js").InstallAgentPluginFromUrlOptional) => Promise<import("./install-from-url.js").InstalledAgentPluginFromUrl>;
72
73
  setAgentPluginEnabled: (input: import("./set-enabled.js").SetAgentPluginEnabledInput) => Promise<import("./set-enabled.js").SetAgentPluginEnabledResult>;
73
74
  findTrustedPluginPackages: (input: Omit<Parameters<(query: import("./trusted-plugin-files.js").TrustedPluginPackagesQuery) => Promise<readonly import("./trusted-plugin-files.js").TrustedPluginVerdict[]>>[0], "orderByPluginId" | "onInactive">, optional?: Pick<Parameters<(query: import("./trusted-plugin-files.js").TrustedPluginPackagesQuery) => Promise<readonly import("./trusted-plugin-files.js").TrustedPluginVerdict[]>>[0], "orderByPluginId" | "onInactive">) => Promise<readonly import("./trusted-plugin-files.js").TrustedPluginVerdict[]>;
74
75
  previewAgentPluginUninstall: (required: import("./uninstall.js").UninstallAgentPluginRequired) => Promise<import("./uninstall.js").AgentPluginUninstallPreview>;
75
76
  uninstallAgentPlugin: (required: import("./uninstall.js").UninstallAgentPluginRequired, optional?: import("./uninstall.js").UninstallAgentPluginOptional) => Promise<import("./uninstall.js").UninstallAgentPluginResult>;
77
+ stageForRemoval: (input: {
78
+ readonly packagesDir: string;
79
+ readonly packageRoot: string;
80
+ }, _optional?: Record<string, never>) => Promise<import("./uninstall.js").StagedTree>;
81
+ restoreStagedTrees: (input: {
82
+ readonly staged: Parameters<(staged: readonly import("./uninstall.js").StagedTree[], cause: unknown) => Promise<unknown>>[0];
83
+ readonly cause: unknown;
84
+ }, _optional?: Record<string, never>) => Promise<unknown>;
85
+ removeFrozenPackageTree: (input: {
86
+ readonly root: string;
87
+ }, _optional?: Record<string, never>) => Promise<void>;
76
88
  isAgentPluginActive: (input: {
77
89
  readonly activations: Parameters<(activations: import("./activation.js").AgentPluginActivations, pluginId: string) => boolean>[0];
78
90
  readonly pluginId: string;
@@ -137,8 +149,18 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
137
149
  }, optional?: {
138
150
  readonly deliveryMode?: AgentPluginDeliveryMode;
139
151
  }) => Promise<import("./resolve-agent-plugin-refs.js").ResolveAgentPluginRefsResult>;
152
+ pluginMemory: (input: {
153
+ readonly workspaceId: string;
154
+ readonly pluginId: string;
155
+ }) => import("../../core/persistent-state.js").PluginMemory;
156
+ migratePluginLayout: (input: {
157
+ readonly workspaceId: string;
158
+ }) => Promise<{
159
+ complete: boolean;
160
+ moved: number;
161
+ }>;
140
162
  listInstalledPlugins: (input: {
141
- readonly packagesDir: string;
163
+ readonly workspaceRoot: string;
142
164
  }) => Promise<readonly import("./types.js").InstalledAgentPlugin[]>;
143
165
  isInstalledDigestPresent: (input: {
144
166
  readonly packagesDir: string;
@@ -170,5 +192,13 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
170
192
  readonly sourceDir: string;
171
193
  }) => Promise<import("./bundled-source-archive.js").PackedAgentPluginArchive>;
172
194
  createBundledSourceArchiveReader: (_input: Record<string, never>) => import("./install.js").AgentPluginArchiveReaderPort;
195
+ loadPluginContributions: <D, T>(input: {
196
+ readonly workspaceId: string;
197
+ readonly definition: import("./contribution-loader.js").PluginContributionDefinition<D, T>;
198
+ }, options?: import("./contribution-loader.js").LoadPluginContributionsOptional) => Promise<import("./contribution-loader.js").PluginContributionResult<T>>;
199
+ loadPluginContributionsFromSource: <D, T>(input: {
200
+ readonly plugin: import("./trusted-plugin-files.js").TrustedPluginPackage;
201
+ readonly definition: import("./contribution-loader.js").PluginContributionDefinition<D, T>;
202
+ }, _options?: Record<string, never>) => Promise<import("./contribution-loader.js").PluginContributionResult<T>>;
173
203
  };
174
204
  //# sourceMappingURL=index.d.ts.map