@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.
- package/CHANGELOG.md +21 -0
- package/README.md +46 -0
- package/dist/admin-surfaces/action-dispatcher.d.ts +37 -0
- package/dist/admin-surfaces/action-dispatcher.js +69 -0
- package/dist/admin-surfaces/action-schema.d.ts +56 -0
- package/dist/admin-surfaces/action-schema.js +167 -0
- package/dist/admin-surfaces/action-tools.d.ts +40 -0
- package/dist/admin-surfaces/action-tools.js +52 -0
- package/dist/admin-surfaces/callback-field-inbox.d.ts +35 -0
- package/dist/admin-surfaces/callback-field-inbox.js +53 -0
- package/dist/admin-surfaces/capability-declarations.d.ts +41 -0
- package/dist/admin-surfaces/capability-declarations.js +102 -0
- package/dist/admin-surfaces/contribution.d.ts +49 -0
- package/dist/admin-surfaces/contribution.js +30 -0
- package/dist/admin-surfaces/delivery-bindings.d.ts +76 -0
- package/dist/admin-surfaces/delivery-bindings.js +99 -0
- package/dist/admin-surfaces/descriptor.d.ts +69 -0
- package/dist/admin-surfaces/descriptor.js +196 -0
- package/dist/admin-surfaces/external-links.d.ts +45 -0
- package/dist/admin-surfaces/external-links.js +70 -0
- package/dist/admin-surfaces/host-validator.d.ts +62 -0
- package/dist/admin-surfaces/host-validator.js +87 -0
- package/dist/admin-surfaces/index.d.ts +23 -0
- package/dist/admin-surfaces/index.js +21 -0
- package/dist/admin-surfaces/protocol.d.ts +103 -0
- package/dist/admin-surfaces/protocol.js +25 -0
- package/dist/admin-surfaces/sandbox.d.ts +78 -0
- package/dist/admin-surfaces/sandbox.js +92 -0
- package/dist/admin-surfaces/settings-value.d.ts +28 -0
- package/dist/admin-surfaces/settings-value.js +45 -0
- package/dist/admin-surfaces/surface-capabilities.d.ts +167 -0
- package/dist/admin-surfaces/surface-capabilities.js +215 -0
- package/dist/admin-surfaces/surface-client.d.ts +116 -0
- package/dist/admin-surfaces/surface-client.js +219 -0
- package/dist/admin-surfaces/surface-handlers.d.ts +111 -0
- package/dist/admin-surfaces/surface-handlers.js +172 -0
- package/dist/admin-surfaces/theme.d.ts +59 -0
- package/dist/admin-surfaces/theme.js +57 -0
- package/dist/folder-zip-node.js +59 -49
- package/dist/folder-zip.js +9 -7
- package/dist/lifecycle/automatic-use.d.ts +79 -0
- package/dist/lifecycle/automatic-use.js +165 -0
- package/dist/lifecycle/compatibility-metadata.d.ts +19 -0
- package/dist/lifecycle/compatibility-metadata.js +45 -0
- package/dist/lifecycle/compatibility.d.ts +52 -0
- package/dist/lifecycle/compatibility.js +60 -0
- package/dist/lifecycle/fetch-archive.js +26 -24
- package/dist/lifecycle/index.d.ts +14 -2
- package/dist/lifecycle/index.js +8 -1
- package/dist/lifecycle/install.js +7 -0
- package/dist/lifecycle/installed-package-index.js +41 -29
- package/dist/lifecycle/installed-package-roots.js +28 -20
- package/dist/lifecycle/manifest.d.ts +13 -0
- package/dist/lifecycle/manifest.js +115 -101
- package/dist/lifecycle/package-paths.js +3 -3
- package/dist/lifecycle/ports.d.ts +1 -1
- package/dist/lifecycle/provider-contribution.d.ts +65 -0
- package/dist/lifecycle/provider-contribution.js +93 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.js +15 -2
- package/dist/lifecycle/retire-bundled.js +20 -18
- package/dist/lifecycle/search.d.ts +6 -1
- package/dist/lifecycle/search.js +45 -7
- package/dist/lifecycle/set-enabled.d.ts +2 -1
- package/dist/lifecycle/set-enabled.js +52 -8
- package/dist/lifecycle/skill-file-reader.js +85 -71
- package/dist/lifecycle/tools/capabilities.d.ts +199 -0
- package/dist/lifecycle/tools/capabilities.js +213 -0
- package/dist/lifecycle/tools/definition.d.ts +32 -0
- package/dist/lifecycle/tools/definition.js +33 -0
- package/dist/lifecycle/tools/index.d.ts +10 -0
- package/dist/lifecycle/tools/index.js +10 -0
- package/dist/lifecycle/tools/manifest.d.ts +33 -0
- package/dist/lifecycle/tools/manifest.js +89 -0
- package/dist/lifecycle/tools/projection.d.ts +90 -0
- package/dist/lifecycle/tools/projection.js +283 -0
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/lifecycle/uninstall.d.ts +3 -1
- package/dist/lifecycle/uninstall.js +51 -27
- package/dist/lifecycle/update-plan.d.ts +18 -0
- package/dist/lifecycle/update-plan.js +22 -0
- package/dist/lifecycle/update-store.d.ts +71 -0
- package/dist/lifecycle/update-store.js +150 -0
- package/dist/lifecycle/update.d.ts +55 -0
- package/dist/lifecycle/update.js +321 -0
- package/package.json +38 -6
- 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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
package/dist/lifecycle/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
111
|
-
skills
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
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;
|