@jini-ai/agent-plugins 0.4.7 → 0.4.8

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 (86) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +46 -0
  3. package/dist/admin-surfaces/action-dispatcher.d.ts +37 -0
  4. package/dist/admin-surfaces/action-dispatcher.js +69 -0
  5. package/dist/admin-surfaces/action-schema.d.ts +56 -0
  6. package/dist/admin-surfaces/action-schema.js +167 -0
  7. package/dist/admin-surfaces/action-tools.d.ts +40 -0
  8. package/dist/admin-surfaces/action-tools.js +52 -0
  9. package/dist/admin-surfaces/callback-field-inbox.d.ts +35 -0
  10. package/dist/admin-surfaces/callback-field-inbox.js +53 -0
  11. package/dist/admin-surfaces/capability-declarations.d.ts +41 -0
  12. package/dist/admin-surfaces/capability-declarations.js +102 -0
  13. package/dist/admin-surfaces/contribution.d.ts +49 -0
  14. package/dist/admin-surfaces/contribution.js +30 -0
  15. package/dist/admin-surfaces/delivery-bindings.d.ts +76 -0
  16. package/dist/admin-surfaces/delivery-bindings.js +99 -0
  17. package/dist/admin-surfaces/descriptor.d.ts +69 -0
  18. package/dist/admin-surfaces/descriptor.js +196 -0
  19. package/dist/admin-surfaces/external-links.d.ts +45 -0
  20. package/dist/admin-surfaces/external-links.js +70 -0
  21. package/dist/admin-surfaces/host-validator.d.ts +62 -0
  22. package/dist/admin-surfaces/host-validator.js +87 -0
  23. package/dist/admin-surfaces/index.d.ts +23 -0
  24. package/dist/admin-surfaces/index.js +21 -0
  25. package/dist/admin-surfaces/protocol.d.ts +103 -0
  26. package/dist/admin-surfaces/protocol.js +25 -0
  27. package/dist/admin-surfaces/sandbox.d.ts +78 -0
  28. package/dist/admin-surfaces/sandbox.js +92 -0
  29. package/dist/admin-surfaces/settings-value.d.ts +28 -0
  30. package/dist/admin-surfaces/settings-value.js +45 -0
  31. package/dist/admin-surfaces/surface-capabilities.d.ts +167 -0
  32. package/dist/admin-surfaces/surface-capabilities.js +215 -0
  33. package/dist/admin-surfaces/surface-client.d.ts +116 -0
  34. package/dist/admin-surfaces/surface-client.js +219 -0
  35. package/dist/admin-surfaces/surface-handlers.d.ts +111 -0
  36. package/dist/admin-surfaces/surface-handlers.js +172 -0
  37. package/dist/admin-surfaces/theme.d.ts +59 -0
  38. package/dist/admin-surfaces/theme.js +57 -0
  39. package/dist/folder-zip-node.js +59 -49
  40. package/dist/folder-zip.js +9 -7
  41. package/dist/lifecycle/automatic-use.d.ts +79 -0
  42. package/dist/lifecycle/automatic-use.js +165 -0
  43. package/dist/lifecycle/compatibility-metadata.d.ts +19 -0
  44. package/dist/lifecycle/compatibility-metadata.js +45 -0
  45. package/dist/lifecycle/compatibility.d.ts +52 -0
  46. package/dist/lifecycle/compatibility.js +60 -0
  47. package/dist/lifecycle/fetch-archive.js +26 -24
  48. package/dist/lifecycle/index.d.ts +14 -2
  49. package/dist/lifecycle/index.js +8 -1
  50. package/dist/lifecycle/install.js +7 -0
  51. package/dist/lifecycle/installed-package-index.js +41 -29
  52. package/dist/lifecycle/installed-package-roots.js +28 -20
  53. package/dist/lifecycle/manifest.d.ts +13 -0
  54. package/dist/lifecycle/manifest.js +115 -101
  55. package/dist/lifecycle/package-paths.js +3 -3
  56. package/dist/lifecycle/ports.d.ts +1 -1
  57. package/dist/lifecycle/provider-contribution.d.ts +65 -0
  58. package/dist/lifecycle/provider-contribution.js +93 -0
  59. package/dist/lifecycle/resolve-agent-plugin-refs.js +15 -2
  60. package/dist/lifecycle/retire-bundled.js +20 -18
  61. package/dist/lifecycle/search.d.ts +6 -1
  62. package/dist/lifecycle/search.js +45 -7
  63. package/dist/lifecycle/set-enabled.d.ts +2 -1
  64. package/dist/lifecycle/set-enabled.js +52 -8
  65. package/dist/lifecycle/skill-file-reader.js +85 -71
  66. package/dist/lifecycle/tools/capabilities.d.ts +199 -0
  67. package/dist/lifecycle/tools/capabilities.js +213 -0
  68. package/dist/lifecycle/tools/definition.d.ts +32 -0
  69. package/dist/lifecycle/tools/definition.js +33 -0
  70. package/dist/lifecycle/tools/index.d.ts +10 -0
  71. package/dist/lifecycle/tools/index.js +10 -0
  72. package/dist/lifecycle/tools/manifest.d.ts +33 -0
  73. package/dist/lifecycle/tools/manifest.js +89 -0
  74. package/dist/lifecycle/tools/projection.d.ts +90 -0
  75. package/dist/lifecycle/tools/projection.js +283 -0
  76. package/dist/lifecycle/types.d.ts +2 -0
  77. package/dist/lifecycle/uninstall.d.ts +3 -1
  78. package/dist/lifecycle/uninstall.js +51 -27
  79. package/dist/lifecycle/update-plan.d.ts +18 -0
  80. package/dist/lifecycle/update-plan.js +22 -0
  81. package/dist/lifecycle/update-store.d.ts +71 -0
  82. package/dist/lifecycle/update-store.js +150 -0
  83. package/dist/lifecycle/update.d.ts +55 -0
  84. package/dist/lifecycle/update.js +321 -0
  85. package/package.json +38 -6
  86. package/ui-ux-design/plugin.json +7 -2
@@ -57,12 +57,28 @@ export async function fetchAgentPluginArchive(required, optional = {}) {
57
57
  throw new RangeError("maxBytes must be a positive integer");
58
58
  assertAllowedUrl(required.url, "requested");
59
59
  const diagnosticUrl = redactArchiveUrl({ value: required.url });
60
- let response;
61
- let finalRequestedUrl = required.url;
60
+ const { response, resolvedUrl: finalRequestedUrl } = await requestArchive({ required, diagnosticUrl }, optional);
61
+ // A server-controlled reason phrase can reflect secrets; use the standard status label.
62
+ if (!response.ok) {
63
+ throw new AgentPluginFetchError({ code: "HTTP_ERROR", message: `'${diagnosticUrl}' returned HTTP ${response.status} ${STATUS_CODES[response.status] ?? ''}`.trimEnd() });
64
+ }
65
+ const resolvedUrl = response.url || finalRequestedUrl;
66
+ assertAllowedUrl(resolvedUrl, "redirected-to");
67
+ assertDeclaredSizeWithinCap(response, maxBytes, diagnosticUrl);
68
+ const archive = await readArchiveBody({ response, maxBytes, diagnosticUrl });
69
+ if (archive.byteLength === 0) {
70
+ throw new AgentPluginFetchError({ code: "EMPTY_BODY", message: `'${diagnosticUrl}' returned an empty body` });
71
+ }
72
+ return {
73
+ archive,
74
+ sha256: createHash("sha256").update(archive).digest("hex"),
75
+ resolvedUrl,
76
+ };
77
+ }
78
+ /** Runs the guarded request; any failure becomes one redacted `REQUEST_FAILED`. */
79
+ async function requestArchive({ required, diagnosticUrl }, optional = {}) {
62
80
  try {
63
- const downloaded = await fetchWithGuard(required, optional);
64
- response = downloaded.response;
65
- finalRequestedUrl = downloaded.resolvedUrl;
81
+ return await fetchWithGuard(required, optional);
66
82
  }
67
83
  catch {
68
84
  // Dependency messages and nested causes can contain signed redirect URLs or bare tokens.
@@ -71,16 +87,11 @@ export async function fetchAgentPluginArchive(required, optional = {}) {
71
87
  cause: new Error("archive request failed"),
72
88
  });
73
89
  }
74
- // A server-controlled reason phrase can reflect secrets; use the standard status label.
75
- if (!response.ok) {
76
- throw new AgentPluginFetchError({ code: "HTTP_ERROR", message: `'${diagnosticUrl}' returned HTTP ${response.status} ${STATUS_CODES[response.status] ?? ''}`.trimEnd() });
77
- }
78
- const resolvedUrl = response.url || finalRequestedUrl;
79
- assertAllowedUrl(resolvedUrl, "redirected-to");
80
- assertDeclaredSizeWithinCap(response, maxBytes, diagnosticUrl);
81
- let archive;
90
+ }
91
+ /** Reads the body within the cap; an over-cap body stays `ARCHIVE_TOO_LARGE`, anything else one redacted `REQUEST_FAILED`. */
92
+ async function readArchiveBody({ response, maxBytes, diagnosticUrl }) {
82
93
  try {
83
- archive = await readBodyWithinCap(response, maxBytes, diagnosticUrl);
94
+ return await readBodyWithinCap(response, maxBytes, diagnosticUrl);
84
95
  }
85
96
  catch (error) {
86
97
  // Preserve our bounded-download error; stream failures must not escape as raw transport text.
@@ -91,14 +102,6 @@ export async function fetchAgentPluginArchive(required, optional = {}) {
91
102
  cause: new Error("archive response body failed"),
92
103
  });
93
104
  }
94
- if (archive.byteLength === 0) {
95
- throw new AgentPluginFetchError({ code: "EMPTY_BODY", message: `'${diagnosticUrl}' returned an empty body` });
96
- }
97
- return {
98
- archive,
99
- sha256: createHash("sha256").update(archive).digest("hex"),
100
- resolvedUrl,
101
- };
102
105
  }
103
106
  /**
104
107
  * The module's own cap, exported so a caller (or a test pinning it against `install.ts`'s
@@ -150,10 +153,9 @@ async function readBodyWithinCap(response, maxBytes, url) {
150
153
  try {
151
154
  for (;;) {
152
155
  const { done, value } = await reader.read();
156
+ // A read that is not `done` always carries a chunk (`ReadableStreamReadValueResult`).
153
157
  if (done)
154
158
  break;
155
- if (value === undefined)
156
- continue;
157
159
  total += value.byteLength;
158
160
  if (total > maxBytes) {
159
161
  throw new AgentPluginFetchError({ code: "ARCHIVE_TOO_LARGE", message: `'${url}' body exceeded the ${maxBytes}-byte cap` });
@@ -1,3 +1,8 @@
1
+ export * from './update-plan.js';
2
+ export { AgentPluginUpdateError } from './update-store.js';
3
+ export type { UpdateAgentPluginRequired, UpdateAgentPluginResult } from './update.js';
4
+ export * from './compatibility.js';
5
+ export * from './automatic-use.js';
1
6
  import { maxAgentPluginArchiveBytes } from './fetch-archive.js';
2
7
  import type { FetchAgentPluginArchiveOptional } from './fetch-archive.js';
3
8
  import type { AgentPluginLifecycleRequired, AgentPluginLifecycleOptional } from './ports.js';
@@ -13,6 +18,7 @@ export * from './manifest.js';
13
18
  export * from './fetch-archive.js';
14
19
  export * from './mcp-provisioning.js';
15
20
  export * from './contribution-loader.js';
21
+ export * from './provider-contribution.js';
16
22
  export { createAgentPluginActivations, ACTIVATIONS_FILENAME, ACTIVATIONS_LOCK_FILENAME, AgentPluginActivationsBusyError, AgentPluginActivationsUnreadableError } from './activation.js';
17
23
  export { BUNDLED_DIGESTS_FILENAME } from './bundled-digests.js';
18
24
  export { AgentPluginInstallError } from './install.js';
@@ -72,7 +78,7 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
72
78
  }) => Promise<boolean>;
73
79
  installAgentPlugin: (required: import("./install.js").InstallAgentPluginRequired, optional?: import("./install.js").InstallAgentPluginOptional) => Promise<import("./types.js").InstalledAgentPlugin>;
74
80
  installAgentPluginFromUrl: (required: import("./install-from-url.js").InstallAgentPluginFromUrlRequired, optional?: import("./install-from-url.js").InstallAgentPluginFromUrlOptional) => Promise<import("./install-from-url.js").InstalledAgentPluginFromUrl>;
75
- setAgentPluginEnabled: (input: import("./set-enabled.js").SetAgentPluginEnabledInput) => Promise<import("./set-enabled.js").SetAgentPluginEnabledResult>;
81
+ setAgentPluginEnabled: (input: import("./set-enabled.js").SetAgentPluginEnabledInput, _optional?: Record<string, never>) => Promise<import("./set-enabled.js").SetAgentPluginEnabledResult>;
76
82
  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[]>;
77
83
  previewAgentPluginUninstall: (required: import("./uninstall.js").UninstallAgentPluginRequired) => Promise<import("./uninstall.js").AgentPluginUninstallPreview>;
78
84
  uninstallAgentPlugin: (required: import("./uninstall.js").UninstallAgentPluginRequired, optional?: import("./uninstall.js").UninstallAgentPluginOptional) => Promise<import("./uninstall.js").UninstallAgentPluginResult>;
@@ -176,8 +182,10 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
176
182
  }) => Promise<boolean>;
177
183
  rankInstalledAgentPlugins: (input: {
178
184
  readonly query: string;
179
- readonly candidates: Parameters<(query: string, candidates: readonly import("./search.js").AgentPluginSearchCandidate[], limit: number) => readonly import("./search.js").AgentPluginSearchMatch[]>[1];
185
+ readonly candidates: Parameters<(query: string, candidates: readonly import("./search.js").AgentPluginSearchCandidate[], limit: number, automatic?: boolean) => readonly import("./search.js").AgentPluginSearchMatch[]>[1];
180
186
  readonly limit: number;
187
+ }, optional?: {
188
+ readonly automatic?: boolean;
181
189
  }) => readonly import("./search.js").AgentPluginSearchMatch[];
182
190
  readTrustedPluginFile: (input: {
183
191
  readonly plugin: Parameters<(plugin: import("./trusted-plugin-files.js").TrustedPluginPackage, filename: string) => Promise<string>>[0];
@@ -208,5 +216,9 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
208
216
  readonly plugin: import("./trusted-plugin-files.js").TrustedPluginPackage;
209
217
  readonly definition: import("./contribution-loader.js").PluginContributionDefinition<D, T>;
210
218
  }, _options?: Record<string, never>) => Promise<import("./contribution-loader.js").PluginContributionResult<T>>;
219
+ updateAgentPlugin: (required: import("./update.js").UpdateAgentPluginRequired, _optional?: Record<string, never>) => Promise<import("./update.js").UpdateAgentPluginResult>;
220
+ reconcileAgentPluginUpdates: ({ workspaceId }: {
221
+ workspaceId: string;
222
+ }, _optional?: Record<string, never>) => Promise<void>;
211
223
  };
212
224
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,8 @@
1
+ export * from './update-plan.js';
2
+ export { AgentPluginUpdateError } from './update-store.js';
3
+ import { createUpdateModule } from './update.js';
4
+ export * from './compatibility.js';
5
+ export * from './automatic-use.js';
1
6
  import { createPersistentStateModule } from "./persistent-state.js";
2
7
  import { fetchAgentPluginArchive, maxAgentPluginArchiveBytes } from './fetch-archive.js';
3
8
  import { createSeedBundledModule } from './seed-bundled.js';
@@ -22,6 +27,7 @@ export * from './manifest.js';
22
27
  export * from './fetch-archive.js';
23
28
  export * from './mcp-provisioning.js';
24
29
  export * from './contribution-loader.js';
30
+ export * from './provider-contribution.js';
25
31
  export { createAgentPluginActivations, ACTIVATIONS_FILENAME, ACTIVATIONS_LOCK_FILENAME, AgentPluginActivationsBusyError, AgentPluginActivationsUnreadableError } from './activation.js';
26
32
  export { BUNDLED_DIGESTS_FILENAME } from './bundled-digests.js';
27
33
  export { AgentPluginInstallError } from './install.js';
@@ -56,6 +62,7 @@ export function createAgentPluginLifecycle(required, optional = {}) {
56
62
  });
57
63
  const uninstall = createUninstallModule(context);
58
64
  return {
65
+ ...createUpdateModule(context),
59
66
  ...contributions,
60
67
  fetchAgentPluginArchive: (input, optional = {}) => fetchAgentPluginArchive({ ...input, fetch: context.fetch, outboundGuard: context.outboundGuard }, optional),
61
68
  maxAgentPluginArchiveBytes,
@@ -102,7 +109,7 @@ export function createAgentPluginLifecycle(required, optional = {}) {
102
109
  }),
103
110
  listInstalledPlugins: (input) => refs.listInstalledPlugins(input.workspaceRoot),
104
111
  isInstalledDigestPresent: (input) => refs.isInstalledDigestPresent(input.packagesDir, input.archiveDigest),
105
- rankInstalledAgentPlugins: (input) => search.rankInstalledAgentPlugins(input.query, input.candidates, input.limit),
112
+ rankInstalledAgentPlugins: (input, optional = {}) => search.rankInstalledAgentPlugins(input.query, input.candidates, input.limit, optional.automatic),
106
113
  readTrustedPluginFile: (input) => trusted.readTrustedPluginFile(input.plugin, input.filename),
107
114
  importContainedModule: (input) => trusted.importContainedModule(input.plugin, input.modulePath),
108
115
  maxAgentPluginInstallArchiveBytes: (_input) => install.maxAgentPluginInstallArchiveBytes(),
@@ -63,6 +63,7 @@ import { readPluginIdentity, assertPluginIdentityAvailable } from "@jini-ai/core
63
63
  * "how does the archive get here" is out of this slice) and no execution of anything extracted.
64
64
  */
65
65
  import { createPersistentStateModule } from "./persistent-state.js";
66
+ import { createAgentPluginUpdateStore, settleAgentPluginUpdateJournal } from "./update-store.js";
66
67
  /** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
67
68
  import { createHash } from "node:crypto";
68
69
  import { constants } from "node:fs";
@@ -196,6 +197,12 @@ function buildModule(ports) {
196
197
  return await persistentState.withIdentityLock({ workspaceRoot: workspaceLayout.root, run: async () => {
197
198
  await assertIdentityAvailable(workspaceLayout.root, indexed);
198
199
  const publishWithinPluginLock = () => persistentState.withPluginLock({ workspaceRoot: workspaceLayout.root, pluginId: indexed.pluginId, run: async () => {
200
+ // Every install and replacement, not only an update, is a mutation of this plugin: an
201
+ // unfinished update journal refuses it here, under the same plugin lock, because boot
202
+ // recovery removes only the journal's own candidate digest and would otherwise restore the
203
+ // old revision beside whatever this install published.
204
+ // The journal lives under the layout this install was given, the one it publishes into.
205
+ await settleAgentPluginUpdateJournal({ store: createAgentPluginUpdateStore({ ports: { ...ports, layout }, workspaceId, pluginId: indexed.pluginId }) });
199
206
  // Layout B needs the manifest id before choosing the package destination. Dedup still
200
207
  // skips publication and freezing, after one bounded staging extraction.
201
208
  // Owner decision 2026-10-04: learned facts begin empty for each site; author guidance stays
@@ -22,24 +22,8 @@ export function createInstalledPackageIndex(required, _optional = {}) {
22
22
  } });
23
23
  return text ? { [required.key]: text.slice(0, required.maxLength) } : {};
24
24
  }
25
- /**
26
- * Reads the (already-extracted-and-trusted, or already-published) `plugin.json` at `packageRoot`
27
- * and walks the tree to build the `files`/`skills` index. Shared between a fresh install and the
28
- * content-addressed dedup fast path so both return an identically-shaped result.
29
- *
30
- * Exported (2026-08-21) for a second caller outside this module:
31
- * `resolve-agent-plugin-refs.ts`'s run-time resolution of a pinned `pluginRefId` against the
32
- * packages already on disk needs the SAME `plugin.json`-parse-plus-tree-walk this function
33
- * already does correctly (manifest validation via `parseAgentPluginManifest`, sorted `files`), so
34
- * it re-derives an `InstalledAgentPlugin` per installed digest rather than hand-rolling a second,
35
- * less-validated reader.
36
- */
37
- // Conditional spreads, not `field: parsed.manifest.field` directly: an `exactOptionalPropertyTypes`-
38
- // style caller (and this codebase's own convention elsewhere — see `buildToolSource`'s identical
39
- // `defaultSkillReason` spread in `tool-registrations.ts`) must see an ABSENT key, not a key present
40
- // with value `undefined`, when the manifest declared none of these — `deepEqual` fixtures across
41
- // this feature's own tests already assert exact object shape.
42
- async function indexInstalledRoot({ packageRoot, archiveDigest }, _optional = {}) {
25
+ /** Reads and validates `packageRoot`'s `plugin.json`, refusing a missing, unparseable or invalid one. */
26
+ async function readInstalledManifest({ packageRoot }) {
43
27
  let manifestRaw;
44
28
  try {
45
29
  manifestRaw = await readFile(path.join(packageRoot, "plugin.json"), "utf8");
@@ -61,22 +45,19 @@ export function createInstalledPackageIndex(required, _optional = {}) {
61
45
  if (!parsed.ok) {
62
46
  throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
63
47
  }
48
+ return parsed;
49
+ }
50
+ /** Every regular file under `packageRoot` (sorted) and every `skills/<name>/SKILL.md` (sorted by name). */
51
+ async function walkPackageTree({ packageRoot }) {
64
52
  const files = [];
65
- for (const message of parsed.warnings)
66
- ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
67
53
  const skills = [];
68
54
  async function walk(absolute, prefix) {
69
55
  for (const entry of await readdir(absolute, { withFileTypes: true })) {
70
56
  const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
71
- if (entry.isDirectory()) {
57
+ if (entry.isDirectory())
72
58
  await walk(path.join(absolute, entry.name), relative);
73
- }
74
- else if (entry.isFile()) {
59
+ else if (entry.isFile())
75
60
  files.push(relative);
76
- const match = relative.match(/^skills\/([^/]+)\/SKILL\.md$/);
77
- if (match)
78
- skills.push({ name: match[1], skillPath: relative });
79
- }
80
61
  // Anything that is neither a directory nor a regular file (a symlink somehow already present,
81
62
  // a device node) is skipped rather than indexed — extraction itself never creates one, so
82
63
  // encountering one here would mean the filesystem changed out from under an already-published,
@@ -84,10 +65,41 @@ export function createInstalledPackageIndex(required, _optional = {}) {
84
65
  }
85
66
  }
86
67
  await walk(packageRoot, "");
68
+ for (const relative of files) {
69
+ const match = relative.match(/^skills\/([^/]+)\/SKILL\.md$/);
70
+ if (match)
71
+ skills.push({ name: match[1], skillPath: relative });
72
+ }
73
+ return { files: files.sort(), skills: skills.sort((a, b) => a.name.localeCompare(b.name)) };
74
+ }
75
+ /**
76
+ * Reads the (already-extracted-and-trusted, or already-published) `plugin.json` at `packageRoot`
77
+ * and walks the tree to build the `files`/`skills` index. Shared between a fresh install and the
78
+ * content-addressed dedup fast path so both return an identically-shaped result.
79
+ *
80
+ * Exported (2026-08-21) for a second caller outside this module:
81
+ * `resolve-agent-plugin-refs.ts`'s run-time resolution of a pinned `pluginRefId` against the
82
+ * packages already on disk needs the SAME `plugin.json`-parse-plus-tree-walk this function
83
+ * already does correctly (manifest validation via `parseAgentPluginManifest`, sorted `files`), so
84
+ * it re-derives an `InstalledAgentPlugin` per installed digest rather than hand-rolling a second,
85
+ * less-validated reader.
86
+ */
87
+ // Conditional spreads, not `field: parsed.manifest.field` directly: an `exactOptionalPropertyTypes`-
88
+ // style caller (and this codebase's own convention elsewhere — see `buildToolSource`'s identical
89
+ // `defaultSkillReason` spread in `tool-registrations.ts`) must see an ABSENT key, not a key present
90
+ // with value `undefined`, when the manifest declared none of these — `deepEqual` fixtures across
91
+ // this feature's own tests already assert exact object shape.
92
+ async function indexInstalledRoot({ packageRoot, archiveDigest }, _optional = {}) {
93
+ const parsed = await readInstalledManifest({ packageRoot });
94
+ for (const message of parsed.warnings)
95
+ ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
96
+ const { files, skills } = await walkPackageTree({ packageRoot });
87
97
  return {
88
98
  pluginId: parsed.manifest.name,
89
99
  id: parsed.manifest.id,
90
100
  provides: parsed.manifest.provides,
101
+ ...(parsed.manifest.requires === undefined ? {} : { requires: parsed.manifest.requires }),
102
+ ...(parsed.manifest.hostRange === undefined ? {} : { hostRange: parsed.manifest.hostRange }),
91
103
  version: parsed.manifest.version,
92
104
  ...( /**
93
105
  * `plugin.json`'s own `description`/`keywords`/`author`/`license` (`manifest.ts`'s
@@ -107,8 +119,8 @@ export function createInstalledPackageIndex(required, _optional = {}) {
107
119
  ...readDisplayText({ manifest: parsed.manifest, key: "summary", maxLength: 1200 }),
108
120
  archiveDigest,
109
121
  packageRoot,
110
- files: files.sort(),
111
- skills: skills.sort((a, b) => a.name.localeCompare(b.name)),
122
+ files,
123
+ skills,
112
124
  };
113
125
  }
114
126
  return { indexInstalledRoot };
@@ -9,12 +9,14 @@ export const INSTALLED_PLUGIN_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
9
9
  * @complexity O(d) directory/containment operations for d revisions; no executable imports.
10
10
  */
11
11
  export async function listInstalledPackageRoots({ ports, workspaceRoot }, _optional = {}) {
12
- const persistentState = createPersistentStateModule(ports);
13
- const { assertContainedOnDisk } = createPackagePathsModule(ports);
14
- const readdir = ports.filesystem.readdir.bind(ports.filesystem);
12
+ const effects = {
13
+ readdir: ports.filesystem.readdir.bind(ports.filesystem),
14
+ assertOwnedPath: createPersistentStateModule(ports).assertOwnedPath,
15
+ assertContainedOnDisk: createPackagePathsModule(ports).assertContainedOnDisk,
16
+ };
15
17
  let folders;
16
18
  try {
17
- folders = await readdir(workspaceRoot, { withFileTypes: true });
19
+ folders = await effects.readdir(workspaceRoot, { withFileTypes: true });
18
20
  }
19
21
  catch (error) {
20
22
  if (isEnoent(error))
@@ -25,25 +27,31 @@ export async function listInstalledPackageRoots({ ports, workspaceRoot }, _optio
25
27
  for (const folder of folders) {
26
28
  if (!folder.isDirectory() || !/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(folder.name) || folder.name.length > 64)
27
29
  continue;
28
- const packagesDir = path.join(workspaceRoot, folder.name, 'package', 'sha256');
29
- let entries;
30
+ roots.push(...await listFolderRevisions({ effects, workspaceRoot, localId: folder.name }));
31
+ }
32
+ return roots;
33
+ }
34
+ /** One plugin folder's contained digest revisions; a folder with no owned package store has none. */
35
+ async function listFolderRevisions({ effects, workspaceRoot, localId }) {
36
+ const packagesDir = path.join(workspaceRoot, localId, 'package', 'sha256');
37
+ let entries;
38
+ try {
39
+ await effects.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, packagesDir) });
40
+ entries = await effects.readdir(packagesDir, { withFileTypes: true });
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ const roots = [];
46
+ for (const entry of entries) {
47
+ if (!entry.isDirectory() || !INSTALLED_PLUGIN_DIGEST_PATTERN.test(entry.name))
48
+ continue;
30
49
  try {
31
- await persistentState.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, packagesDir) });
32
- entries = await readdir(packagesDir, { withFileTypes: true });
50
+ const packageRoot = await effects.assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
51
+ roots.push({ localId, archiveDigest: entry.name, packageRoot });
33
52
  }
34
53
  catch {
35
- continue;
36
- }
37
- for (const entry of entries) {
38
- if (!entry.isDirectory() || !INSTALLED_PLUGIN_DIGEST_PATTERN.test(entry.name))
39
- continue;
40
- try {
41
- const packageRoot = await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
42
- roots.push({ localId: folder.name, archiveDigest: entry.name, packageRoot });
43
- }
44
- catch {
45
- // One corrupt package never hides another plugin. Memory-only folders are not installs.
46
- }
54
+ // One corrupt package never hides another plugin. Memory-only folders are not installs.
47
55
  }
48
56
  }
49
57
  return roots;
@@ -8,6 +8,15 @@ export type AgentPluginServerMetadataReader = (required: {
8
8
  readonly value: Readonly<Record<string, unknown>>;
9
9
  readonly server: Readonly<Record<string, unknown>>;
10
10
  }) => Readonly<Record<string, unknown>>;
11
+ /**
12
+ * True when `value` is a valid Agent Plugin id: `plugin.json`'s `name` grammar (§5.5, see
13
+ * `NAME_PATTERN`), 1-64 characters. The one owner of that grammar: a host checks an id taken from a
14
+ * URL or a tool input here before it names anything on disk, instead of restating the pattern.
15
+ * @complexity O(n) in the value's length.
16
+ */
17
+ export declare function isAgentPluginName({ value }: {
18
+ readonly value: unknown;
19
+ }, _optional?: Record<string, never>): boolean;
11
20
  export interface AgentPluginManifest {
12
21
  readonly name: string;
13
22
  readonly version?: (string) | undefined;
@@ -19,6 +28,10 @@ export interface AgentPluginManifest {
19
28
  /** Normalized publisher identity, independent of name/storage key and version. */
20
29
  readonly id?: string | undefined;
21
30
  readonly provides?: readonly string[] | undefined;
31
+ /** `extensions.<ns>.requires`: advisory capability ids other active plugins should provide. */
32
+ readonly requires?: readonly string[] | undefined;
33
+ /** `extensions.<ns>.hostRange`: semver range over AGENT_PLUGIN_CONTRACT_VERSION; a malformed value is stored as `<invalid>`. */
34
+ readonly hostRange?: string | undefined;
22
35
  }
23
36
  export type ParseAgentPluginManifestResult = {
24
37
  readonly ok: true;