@jini-ai/agent-plugins 0.4.1 → 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 (43) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/core/persistent-state.d.ts +25 -0
  3. package/core/persistent-state.js +260 -0
  4. package/dist/lifecycle/activation.js +38 -166
  5. package/dist/lifecycle/bundled-digests.d.ts +6 -0
  6. package/dist/lifecycle/bundled-digests.js +156 -0
  7. package/dist/lifecycle/bundled-source-archive.d.ts +3 -0
  8. package/dist/lifecycle/bundled-source-archive.js +74 -0
  9. package/dist/lifecycle/contribution-loader.d.ts +102 -0
  10. package/dist/lifecycle/contribution-loader.js +92 -0
  11. package/dist/lifecycle/fetch-archive.d.ts +1 -2
  12. package/dist/lifecycle/index.d.ts +24 -4
  13. package/dist/lifecycle/index.js +11 -0
  14. package/dist/lifecycle/install-from-url.d.ts +2 -5
  15. package/dist/lifecycle/install-from-url.js +2 -5
  16. package/dist/lifecycle/install.d.ts +57 -2
  17. package/dist/lifecycle/install.js +222 -21
  18. package/dist/lifecycle/layout.js +1 -1
  19. package/dist/lifecycle/manifest.d.ts +3 -9
  20. package/dist/lifecycle/manifest.js +4 -11
  21. package/dist/lifecycle/package-paths.d.ts +2 -0
  22. package/dist/lifecycle/package-paths.js +62 -0
  23. package/dist/lifecycle/persistent-state.d.ts +1 -1
  24. package/dist/lifecycle/persistent-state.js +1 -1
  25. package/dist/lifecycle/ports.d.ts +14 -2
  26. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +13 -1
  27. package/dist/lifecycle/resolve-agent-plugin-refs.js +143 -4
  28. package/dist/lifecycle/retire-bundled.d.ts +50 -1
  29. package/dist/lifecycle/retire-bundled.js +9 -2
  30. package/dist/lifecycle/search.d.ts +63 -0
  31. package/dist/lifecycle/search.js +29 -0
  32. package/dist/lifecycle/seed-bundled.d.ts +18 -1
  33. package/dist/lifecycle/seed-bundled.js +131 -3
  34. package/dist/lifecycle/set-enabled.d.ts +7 -0
  35. package/dist/lifecycle/set-enabled.js +5 -0
  36. package/dist/lifecycle/trusted-plugin-files.d.ts +38 -0
  37. package/dist/lifecycle/trusted-plugin-files.js +18 -0
  38. package/dist/lifecycle/types.d.ts +12 -0
  39. package/dist/lifecycle/uninstall.d.ts +29 -0
  40. package/dist/lifecycle/uninstall.js +204 -4
  41. package/dist/lifecycle/yauzl-archive-reader.d.ts +0 -1
  42. package/package.json +25 -8
  43. package/persistent-state.js +2 -260
@@ -1,10 +1,13 @@
1
1
  import type { AgentPluginLayout } from "./layout.js";
2
- import { type RetiredAgentPluginOutcome } from "./retire-bundled.js";
2
+ import type { RetiredAgentPluginOutcome } from "./retire-bundled.js";
3
3
  import type { AgentPluginLifecyclePorts } from "./ports.js";
4
4
  export type SeededAgentPluginOutcome = {
5
5
  readonly pluginId: string;
6
6
  readonly status: "seeded";
7
7
  readonly archiveDigest: string;
8
+ /** True when this boot wrote the initial record (disabled, or enabled for
9
+ * {@link BUNDLED_AGENT_PLUGINS_SEEDED_ENABLED}); false when a decision already existed and was
10
+ * preserved. */
8
11
  readonly activationRecorded: boolean;
9
12
  } | {
10
13
  readonly pluginId: string;
@@ -12,12 +15,26 @@ export type SeededAgentPluginOutcome = {
12
15
  readonly reason: string;
13
16
  };
14
17
  export interface SeedBundledAgentPluginsResult {
18
+ /** Directory holding one subdirectory per bundled plugin. `deps.ts`'s `bundledAgentPluginsDir()`
19
+ * in production. */
15
20
  readonly sourceRoot: string;
16
21
  readonly outcomes: readonly SeededAgentPluginOutcome[];
22
+ /** Why this boot could not record which digest it published for each plugin
23
+ * (`bundled-digests.ts`), or absent when it did. Reported rather than thrown, for the same
24
+ * failure-isolation reason every per-plugin failure is: a workspace whose ledger cannot be
25
+ * written still has every package installed and every activation decision intact — it only falls
26
+ * back to refusing a multi-digest plugin id, which is exactly the behaviour that predates the
27
+ * ledger. */
17
28
  readonly ledgerFailure?: (string) | undefined;
29
+ /** One outcome per retired bundled plugin (`retire-bundled.ts`), run after seeding and the ledger so
30
+ * each successor's own record already exists. Empty when the activation pre-flight refused. */
18
31
  readonly retirements: readonly RetiredAgentPluginOutcome[];
19
32
  }
20
33
  export interface SeedBundledAgentPluginsRequired {
34
+ /** The INSTANCE-level layout (`resolveAgentPluginLayout()`), not a pre-resolved workspace one —
35
+ * `installAgentPlugin` resolves `forWorkspace(workspaceId)` itself, atomically, and this
36
+ * function passes both through unchanged so that guarantee is not weakened in transit. See
37
+ * `install.ts`'s SECURITY note on `InstallAgentPluginRequired.layout`. */
21
38
  readonly layout: AgentPluginLayout;
22
39
  readonly workspaceId: string;
23
40
  readonly sourceRoot: string;
@@ -1,10 +1,88 @@
1
+ /**
2
+ * @file `seedBundledAgentPlugins()` — installs the Agent Plugins that ship WITH the host into a
3
+ * workspace's own package store, and records each one INACTIVE until an operator says otherwise
4
+ * (except the few core paths depend on, `BUNDLED_AGENT_PLUGINS_SEEDED_ENABLED`, which seed active).
5
+ *
6
+ * ---------------------------------------------------------------------------
7
+ * "Bundled but inactive", concretely
8
+ * ---------------------------------------------------------------------------
9
+ * Bundled: the package's real files are tracked in this repository under `content/agent-plugins/<id>/`
10
+ * and copied into `dist/` by `npm run build`, exactly like `content/themes/`. A fresh install has them
11
+ * without downloading anything.
12
+ *
13
+ * Inactive: seeding writes `{ enabled: false, origin: "bundled" }` into the workspace's activation
14
+ * record (`activation.ts`) BEFORE the package can be read by anything, and the two places that
15
+ * consume installed plugins both consult that record:
16
+ *
17
+ * - `tool-registrations.ts` — will not register its `agent_plugin_<id>` tool;
18
+ * - `resolve-agent-plugin-refs.ts` — refuses to inject it even if a composer chip pins it by id.
19
+ *
20
+ * (A third surface, `capability-source.ts`'s `capability_search` discovery, was gated here too
21
+ * until that tool pair was removed 2026-08-26 — see
22
+ * `ADS-memory/knowledge/2026-08-26-removed-capability-search.md`.)
23
+ *
24
+ * So "inactive" means genuinely unexecutable, not merely unadvertised: its skills never reach a
25
+ * prompt, and its tools and MCP servers are never registered.
26
+ *
27
+ * It does NOT mean hidden from the operator. The admin Agent Plugins screen lists every installed
28
+ * plugin, with an inactive one's switch shown off, so a bundled plugin can be found and turned on
29
+ * there (owner decision, 2026-09-13; before it, that screen's default tab listed only active ones).
30
+ * Listing only READS the activation record, so the "absent means active" hazard described below is
31
+ * unaffected.
32
+ *
33
+ * ---------------------------------------------------------------------------
34
+ * Why it runs on every boot
35
+ * ---------------------------------------------------------------------------
36
+ * Seeding is idempotent twice over: `installAgentPlugin` short-circuits when the content digest is
37
+ * already published, and `recordBundledAgentPluginIfAbsent` never overwrites an existing decision.
38
+ * Re-running it therefore costs one directory walk and one hash, and buys two things a one-shot
39
+ * first-run seed would not:
40
+ *
41
+ * 1. A product upgrade that ships a NEW bundled plugin, or a NEW VERSION of one, reaches existing
42
+ * workspaces without a migration step.
43
+ *
44
+ * The second half of that claim was FALSE IN EFFECT until 2026-09-18, and the correction is the
45
+ * reason `bundled-digests.ts` exists. The package store is content-addressed and nothing retires
46
+ * an entry, so a revised bundled plugin installs ALONGSIDE its predecessor rather than replacing
47
+ * it — after which both consumption surfaces refused the plugin outright as ambiguous
48
+ * (`resolve-agent-plugin-refs.ts`'s "refusing to guess which one to use",
49
+ * `tool-registrations.ts`'s `poisonedPluginIds`). An upgrade did reach the workspace; it also
50
+ * silently broke the plugin. What makes the claim true now is the ledger this function writes
51
+ * below: the seed RECORDS which digest this build published, so the read side has the build's own
52
+ * answer to "which one" instead of a guess. The superseded bytes stay on disk — see
53
+ * `bundled-digests.ts`'s header for why nothing is deleted, and this file's own report for what
54
+ * an operator still has to prune by hand.
55
+ * 2. Deleting `activations.json` re-creates the bundled plugin's DISABLED record. Without the
56
+ * re-run, deleting that file would silently promote every bundled plugin to active, because
57
+ * `isAgentPluginActive`'s "absent means active" default is what keeps operator-installed plugins
58
+ * working. The boot-time re-seed is what closes that direction.
59
+ *
60
+ * A file that is PRESENT but unreadable is a different case from a deleted one, and is not
61
+ * treated the same way (t91 F1.1, 2026-09-16): seeding refuses outright rather than install
62
+ * around the fault, and every bundled plugin comes back `failed` with the recovery text
63
+ * `activation.ts`'s `AgentPluginActivationsUnreadableError` gives — see this file's header,
64
+ * "Failure isolation" below, and `activation.ts`'s own header, "Writers never rewrite what they
65
+ * could not read".
66
+ *
67
+ * ---------------------------------------------------------------------------
68
+ * Failure isolation
69
+ * ---------------------------------------------------------------------------
70
+ * One unusable bundled package must never stop a workspace from booting or hide the others. Each is
71
+ * seeded independently and its failure is captured into the returned report, not thrown. The caller
72
+ * decides whether to log it; nothing here writes to the console, so this stays a pure-ish function
73
+ * a test can assert on directly.
74
+ *
75
+ * Architectural role:
76
+ * Composition over `bundled-source-archive.ts` (directory to archive), `install.ts`
77
+ * (`installAgentPlugin`, where every extraction guarantee lives), and `activation.ts` (the record).
78
+ * No guarantee of its own.
79
+ */
1
80
  import { createPersistentStateModule } from "./persistent-state.js";
2
81
  import path from "node:path";
3
82
  import { createActivationModule } from "./activation.js";
4
83
  import { createBundledDigestsModule } from "./bundled-digests.js";
5
84
  import { createBundledSourceArchiveModule } from "./bundled-source-archive.js";
6
85
  import { createInstallModule } from "./install.js";
7
- import {} from "./retire-bundled.js";
8
86
  import { createRetireBundledModule } from "./retire-bundled.js";
9
87
  function buildModule(ports) {
10
88
  const readdir = ports.filesystem.readdir.bind(ports.filesystem);
@@ -14,13 +92,38 @@ function buildModule(ports) {
14
92
  const { createBundledSourceArchiveReader, packAgentPluginDirectory } = createBundledSourceArchiveModule(ports);
15
93
  const { installAgentPlugin } = createInstallModule(ports);
16
94
  const { retireBundledAgentPlugins } = createRetireBundledModule(ports);
95
+ /**
96
+ * Installs every bundled Agent Plugin into one workspace and records each inactive-by-default,
97
+ * except {@link BUNDLED_AGENT_PLUGINS_SEEDED_ENABLED}.
98
+ *
99
+ * @returns A per-plugin report. An absent or unreadable `sourceRoot` yields an empty `outcomes`
100
+ * list rather than an error: a build that shipped no bundled plugins is a legitimate configuration,
101
+ * and a self-hosted operator who deleted the directory has made a choice this function should not
102
+ * override by crashing.
103
+ * @throws Nothing. Every per-plugin failure is captured into the report.
104
+ * @complexity O(p) plugins, each O(f) files and O(b) bytes — all bounded by
105
+ * `bundled-source-archive.ts`'s file cap and `install.ts`'s own byte caps.
106
+ */
107
+ // The migration must finish before new installs, including bundled packages, are published.
108
+ // Rejected legacy entries are preserved in quarantine and count as handled. An incomplete run
109
+ // means a move/quarantine failed; a held lock also refuses seeding until migration can finish.
110
+ // ONE strict pre-flight read BEFORE any install — so a bundled package never lands on disk
111
+ // without the activation record that would keep it inactive (this file's header). Every bundled
112
+ // plugin is reported failed with the SAME reason, rather than attempting each one and hitting the
113
+ // identical fault p times.
114
+ // AFTER every install, never before: the ledger names digests, and a digest is only worth naming
115
+ // once its bytes are actually on disk for this workspace.
116
+ // AFTER seeding and the ledger: a retired plugin's successor is itself bundled, so its record and
117
+ // its ledger entry exist by now, and the retirement only has to decide whether to switch it on.
17
118
  async function seedBundledAgentPlugins(required) {
18
119
  const { layout, workspaceId, sourceRoot } = required;
19
120
  const pluginDirNames = await listBundledPluginDirs(sourceRoot);
20
121
  try {
21
- const migration = await createPersistentStateModule(ports).migrate({ workspaceRoot: layout.forWorkspace({ workspaceId }).root });
122
+ const migration = ports.migrateBeforeSeed
123
+ ? await ports.migrateBeforeSeed({ layout, workspaceId }, {})
124
+ : await createPersistentStateModule(ports).migrate({ workspaceRoot: layout.forWorkspace({ workspaceId }).root });
22
125
  if (!migration.complete)
23
- throw new Error('Legacy plugin migration incomplete; see migration events');
126
+ throw new Error('Legacy plugin layout migration has unfinished filesystem moves; see boot log');
24
127
  }
25
128
  catch (error) {
26
129
  const reason = error instanceof Error ? error.message : String(error);
@@ -42,6 +145,11 @@ function buildModule(ports) {
42
145
  const retirements = await retireBundledAgentPlugins({ layout, workspaceId });
43
146
  return { sourceRoot, outcomes, retirements, ...(ledgerFailure !== undefined ? { ledgerFailure } : {}) };
44
147
  }
148
+ /** Records which digest this boot published for each plugin that seeded successfully, and returns
149
+ * why it could not, rather than throwing — {@link seedBundledAgentPlugins}'s own contract is
150
+ * "@throws Nothing", and a ledger this boot failed to write costs the workspace nothing it had
151
+ * before the ledger existed.
152
+ * @complexity One file read and one atomic write. */
45
153
  async function recordSeededDigests(workspaceRoot, outcomes) {
46
154
  try {
47
155
  await recordBundledAgentPluginDigests({
@@ -54,6 +162,11 @@ function buildModule(ports) {
54
162
  return error instanceof Error ? error.message : String(error);
55
163
  }
56
164
  }
165
+ /** Why nothing may be seeded into this workspace right now, or `undefined`. One strict read BEFORE
166
+ * any install, so a bundled package never lands on disk without the record that keeps it inactive.
167
+ * Catches everything (including `forWorkspace`'s own throw) to keep this function's own contract
168
+ * "@throws Nothing", matching {@link seedBundledAgentPlugins}'s own guarantee.
169
+ * @complexity One file read. */
57
170
  async function activationsRefusal(layout, workspaceId) {
58
171
  try {
59
172
  await assertAgentPluginActivationsWritable(layout.forWorkspace({ workspaceId }).root);
@@ -63,6 +176,9 @@ function buildModule(ports) {
63
176
  return error instanceof Error ? error.message : String(error);
64
177
  }
65
178
  }
179
+ // The plugin's OWN manifest name, not the directory name — `install.ts` reads the id from
180
+ // `plugin.json`, and every consumer keys activation off that same id. Using the folder name
181
+ // here would produce a record nothing ever consults if the two ever disagreed.
66
182
  async function seedOne(args) {
67
183
  try {
68
184
  const packed = await packAgentPluginDirectory(args.sourceDir);
@@ -89,6 +205,17 @@ function buildModule(ports) {
89
205
  };
90
206
  }
91
207
  }
208
+ /**
209
+ * Lists the subdirectories of `sourceRoot` that actually look like Agent Plugin packages (they hold
210
+ * a `plugin.json` at their own root, which is where `indexInstalledRoot` looks for it).
211
+ *
212
+ * Filtering here rather than letting the installer reject them keeps a stray `README.md` or an
213
+ * editor's scratch directory from showing up in the report as a failed plugin.
214
+ *
215
+ * @complexity O(d) in the subdirectory count.
216
+ */
217
+ // A retired id is never seeded, even from a stale build that still carries its directory —
218
+ // otherwise every boot would reinstall it only for `retire-bundled.ts` to remove it again.
92
219
  async function listBundledPluginDirs(sourceRoot) {
93
220
  let entries;
94
221
  try {
@@ -109,6 +236,7 @@ function buildModule(ports) {
109
236
  dirs.push(entry.name);
110
237
  }
111
238
  catch {
239
+ // No plugin.json: not a package. Silently skipped — see this function's own doc.
112
240
  }
113
241
  }
114
242
  return dirs.sort();
@@ -1,4 +1,9 @@
1
1
  import type { AgentPluginLifecyclePorts } from "./ports.js";
2
+ /**
3
+ * The id named is not installed in this workspace. A distinct class rather than a bare `Error` so
4
+ * each caller can map it to its own boundary's vocabulary — 404 `AGENT_PLUGIN_NOT_FOUND` at the
5
+ * admin route, `ToolInputError` at the tool — without either one string-matching a message.
6
+ */
2
7
  export declare class AgentPluginNotInstalledError extends Error {
3
8
  constructor({ pluginId }: {
4
9
  readonly pluginId: string;
@@ -8,10 +13,12 @@ export interface SetAgentPluginEnabledInput {
8
13
  readonly workspaceId: string;
9
14
  readonly pluginId: string;
10
15
  readonly enabled: boolean;
16
+ /** Recorded as the activation record's updatedBy. The authenticated principal, never a constant. */
11
17
  readonly actor: string;
12
18
  }
13
19
  export interface SetAgentPluginEnabledResult {
14
20
  readonly pluginId: string;
21
+ /** As actually written and read back, not as requested. */
15
22
  readonly enabled: boolean;
16
23
  }
17
24
  /** Internal composition; one state/queue instance per injected host context. */
@@ -1,6 +1,11 @@
1
1
  /** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
2
2
  import { createActivationModule } from "./activation.js";
3
3
  import { createResolveAgentPluginRefsModule } from "./resolve-agent-plugin-refs.js";
4
+ /**
5
+ * The id named is not installed in this workspace. A distinct class rather than a bare `Error` so
6
+ * each caller can map it to its own boundary's vocabulary — 404 `AGENT_PLUGIN_NOT_FOUND` at the
7
+ * admin route, `ToolInputError` at the tool — without either one string-matching a message.
8
+ */
4
9
  export class AgentPluginNotInstalledError extends Error {
5
10
  constructor({ pluginId }) {
6
11
  super(`agent plugin '${pluginId}' is not installed in this workspace`);
@@ -1,18 +1,56 @@
1
1
  import type { AgentPluginLifecyclePorts } from "./ports.js";
2
+ /**
3
+ * @file The shared trust gate for plugin-contributed files that core reads or runs: deploy targets
4
+ * (`features/deployments/deploy-targets/registry.ts`), deploy-config generators, credential
5
+ * scheme rules, mail adapters (`./mail-adapter-registry.ts`) and git-host providers
6
+ * (`features/source-control/provider-registry.ts`). One place decides which installed Agent Plugin packages core may take a contribution
7
+ * from, so every seam applies the same rules.
8
+ *
9
+ * A plugin opts in to a seam by shipping that seam's file at its package root (e.g.
10
+ * a host-supplied deploy-target descriptor filename). {@link findTrustedPluginPackages} judges each package that ships it
11
+ * against the gates:
12
+ * 1. ACTIVE by the fail-closed reader (`resolveAgentPluginActivation`) when the seam asks for it
13
+ * (`requireActive`). An unreadable activation record refuses rather than reading as consent. A
14
+ * data-only seam may skip this gate; its own file header must say why.
15
+ * 2. Its installed digest is the one `bundled-digests.json` records for it, the build's own seed.
16
+ * An operator-installed package, or a tampered copy under a bundled id, never contributes
17
+ * (owner decision 2026-09-29: third-party contributions come later). Always applied.
18
+ *
19
+ * {@link importContainedModule} is the third gate for seams that run code: the module path must
20
+ * resolve inside the package root on disk (`assertContainedOnDisk`) and end in `.mjs` (the caller's
21
+ * descriptor parser checks the extension), so no `package.json` above the install directory can make
22
+ * Node read it as CommonJS.
23
+ *
24
+ * Failure isolation: a refused package comes back as a refusal verdict, never thrown. Only a filesystem
25
+ * fault listing the workspace's package directory itself propagates.
26
+ *
27
+ * Architectural role: `features/agent-plugins` capability. Depends only on this feature's own
28
+ * install, activation and path modules; the seams depend on it, nothing here names one.
29
+ */
30
+ /** A package a seam may read: whose it is and where its files live. */
2
31
  export interface TrustedPluginPackage {
3
32
  readonly pluginId: string;
4
33
  readonly packageRoot: string;
5
34
  }
6
35
  export interface TrustedPluginPackagesQuery {
7
36
  readonly workspaceId: string;
37
+ /** The file a plugin ships at its root to opt in to this seam. */
8
38
  readonly filename: string;
39
+ /** Plural noun for refusal text, e.g. `"deploy targets"`: "deploy targets from 'x' were not loaded: …". */
9
40
  readonly contribution: string;
41
+ /** Apply the activation gate (see this file's header, gate 1). */
10
42
  readonly requireActive: boolean;
43
+ /** Return verdicts sorted by plugin id instead of installed order. */
11
44
  readonly orderByPluginId?: (boolean) | undefined;
45
+ /** Hears each package the activation gate skips as switched off, e.g. to read its seam file as
46
+ * data and name the plugin in a refusal. Never called without `requireActive`. */
12
47
  readonly onInactive?: ((required: {
13
48
  readonly plugin: TrustedPluginPackage;
14
49
  }) => Promise<void>) | undefined;
15
50
  }
51
+ /** One installed package that ships the seam's file: trusted, or refused with the full refusal line.
52
+ * Returned in installed order (or plugin-id order, see `orderByPluginId`) so a seam that interleaves its own per-package refusals keeps them in
53
+ * the same order as the gate's. */
16
54
  export type TrustedPluginVerdict = {
17
55
  readonly trusted: TrustedPluginPackage;
18
56
  } | {
@@ -12,6 +12,15 @@ function buildModule(ports) {
12
12
  const { preferBundledAgentPluginDigests, readBundledAgentPluginDigests } = createBundledDigestsModule(ports);
13
13
  const { assertContainedOnDisk } = createPackagePathsModule(ports);
14
14
  const { listInstalledPlugins } = createResolveAgentPluginRefsModule(ports);
15
+ /**
16
+ * A verdict for every installed package in this workspace that ships `query.filename`, in
17
+ * installed order (bundled digests preferred when a plugin has more than one installed digest).
18
+ * A switched-off plugin is skipped silently (only `onInactive` hears it); every other refusal is reported.
19
+ *
20
+ * @throws Nothing for a plugin-level fault; only a filesystem fault listing the workspace's package
21
+ * directory itself propagates.
22
+ * @complexity O(p) installed plugins, one activation read each for a plugin that ships the file.
23
+ */
15
24
  async function findTrustedPluginPackages(query) {
16
25
  const layout = ports.layout.forWorkspace({ workspaceId: query.workspaceId });
17
26
  const bundled = await readBundledAgentPluginDigests(layout.root);
@@ -32,6 +41,8 @@ function buildModule(ports) {
32
41
  }
33
42
  return verdicts;
34
43
  }
44
+ /** Why one installed package may not contribute (`"inactive"` = skip silently), or `null` when it may.
45
+ * @complexity O(1) beyond one activation read. */
35
46
  async function trustRefusal(query, plugin, bundledDigest, workspaceRoot) {
36
47
  if (query.requireActive) {
37
48
  const activation = await resolveAgentPluginActivation(workspaceRoot, plugin.pluginId);
@@ -45,10 +56,17 @@ function buildModule(ports) {
45
56
  }
46
57
  return null;
47
58
  }
59
+ /** Reads one file at a trusted package's root as UTF-8. @complexity O(n) in the file size. */
48
60
  async function readTrustedPluginFile(plugin, filename) {
49
61
  const absolute = await assertContainedOnDisk(plugin.packageRoot, filename);
50
62
  return readFile(absolute, "utf8");
51
63
  }
64
+ /**
65
+ * Imports one module from a trusted package after the containment check. Returns the module's
66
+ * default export, or the refusal reason. The caller validates the export's shape.
67
+ *
68
+ * @complexity One `realpath` walk plus one dynamic import (cached by Node per file URL).
69
+ */
52
70
  async function importContainedModule(plugin, modulePath) {
53
71
  let resolved;
54
72
  try {
@@ -8,6 +8,18 @@ export interface InstalledAgentPluginSkill {
8
8
  export interface InstalledAgentPlugin {
9
9
  readonly pluginId: string;
10
10
  readonly version?: (string) | undefined;
11
+ /** the host extension namespace in `plugin.json`: `displayName` — the human-facing title when the id alone reads
12
+ * wrong ("deploy" shows as "Deploy Online"). Lives under the spec's host-extension namespace, not
13
+ * a top-level field, because the spec has no title field and `name` is the stable id that
14
+ * activations, tool ids and ledgers key on — renaming it would churn all of those. Absent means
15
+ * callers title-case the id. */
16
+ readonly displayName?: string | undefined;
17
+ /** the host extension namespace in `plugin.json`: `summary` — a short plain-language explainer for the site owner,
18
+ * shown in the admin in place of the spec `description`. That `description` stays the
19
+ * agent-facing text (search ranks it and returns it to the model), so it can stay dense and
20
+ * technical; this field is never ranked and never reaches the model. Blank lines separate
21
+ * paragraphs. Absent means the admin shows the description. */
22
+ readonly summary?: string | undefined;
11
23
  readonly description?: (string) | undefined;
12
24
  readonly keywords?: (readonly string[]) | undefined;
13
25
  readonly author?: string | PluginManifestAuthor | undefined;
@@ -1,15 +1,20 @@
1
1
  import type { AgentPluginLayout } from "./layout.js";
2
2
  import type { AgentPluginLifecyclePorts } from "./ports.js";
3
+ /** The plugin id has no installed package on disk for this workspace. */
3
4
  export declare class AgentPluginNotFoundError extends Error {
4
5
  constructor({ message }: {
5
6
  readonly message: string;
6
7
  }, optional?: ErrorOptions);
7
8
  }
9
+ /** The plugin is bundled with the host — see this file's header for why that is permanent, not merely
10
+ * "nothing to remove". */
8
11
  export declare class AgentPluginNotUninstallableError extends Error {
9
12
  constructor({ message }: {
10
13
  readonly message: string;
11
14
  }, optional?: ErrorOptions);
12
15
  }
16
+ /** The installed archives for this plugin id are no longer exactly the ones a confirmed preview listed —
17
+ * see `UninstallAgentPluginOptional.confirmedPreview`. Nothing was removed. */
13
18
  export declare class AgentPluginChangedSincePreviewError extends Error {
14
19
  constructor({ message }: {
15
20
  readonly message: string;
@@ -21,22 +26,46 @@ export interface UninstallAgentPluginRequired {
21
26
  readonly pluginId: string;
22
27
  }
23
28
  export interface UninstallAgentPluginOptional {
29
+ /** The preview a human confirmed. When given, the uninstall refuses, removing nothing, unless the digests it
30
+ * resolves now are exactly this preview's `archiveDigests`: a confirmation is consent to remove what was
31
+ * resolved for the dialog, not whatever the id names by the time the answer arrives (t91 F2.2). */
24
32
  readonly confirmedPreview?: (AgentPluginUninstallPreview) | undefined;
33
+ /** Only the browser-confirmed choice may delete persistent state. Off by default. */
25
34
  readonly deleteMemory?: boolean | undefined;
35
+ /** INTERNAL — `retire-bundled.ts` only, never an agent tool or HTTP route. Skips the bundled refusal
36
+ * (this file's header, item 2) and nothing else: that refusal exists because a bundled plugin is
37
+ * re-seeded on every boot, which stops being true once the host no longer ships its source
38
+ * (`RETIRED_BUNDLED_AGENT_PLUGINS`). The malformed-entry refusal still applies. */
26
39
  readonly retiredBundled?: (boolean) | undefined;
27
40
  }
28
41
  export interface UninstallAgentPluginResult {
29
42
  readonly pluginId: string;
43
+ /** Every installed archive digest removed for this plugin id — normally exactly one. All matching
44
+ * digests are removed: uninstall's job is to make the plugin genuinely gone, so unlike the tool
45
+ * loader, which must pick ONE to serve, there is no "which one" question here. */
30
46
  readonly removedDigests: readonly string[];
31
47
  }
48
+ /** What an uninstall of one plugin would remove, in a form safe to show a human: no host path. */
32
49
  export interface AgentPluginUninstallPreview {
33
50
  readonly pluginId: string;
51
+ /** Distinct manifest versions across the matching digests; empty when none declares one. */
34
52
  readonly versions: readonly string[];
35
53
  readonly archiveDigests: readonly string[];
36
54
  }
55
+ /** A reversible staged package and its original permissions; shared by uninstall and replacement. */
56
+ /** One package root renamed aside, and what restoring it takes: the path it must go back to, and the
57
+ * mode it carried before staging unfroze its root directory. */
58
+ export interface StagedTree {
59
+ readonly quarantined: string;
60
+ readonly original: string;
61
+ readonly originalMode: number;
62
+ }
37
63
  /** Internal composition; one state/queue instance per injected host context. */
38
64
  export declare function createUninstallModule(ports: AgentPluginLifecyclePorts): {
39
65
  previewAgentPluginUninstall: (required: UninstallAgentPluginRequired) => Promise<AgentPluginUninstallPreview>;
40
66
  uninstallAgentPlugin: (required: UninstallAgentPluginRequired, optional?: UninstallAgentPluginOptional) => Promise<UninstallAgentPluginResult>;
67
+ stageForRemoval: (packagesDir: string, packageRoot: string) => Promise<StagedTree>;
68
+ restoreStagedTrees: (staged: readonly StagedTree[], cause: unknown) => Promise<unknown>;
69
+ removeFrozenPackageTree: (root: string) => Promise<void>;
41
70
  };
42
71
  //# sourceMappingURL=uninstall.d.ts.map