@jini-ai/agent-plugins 0.4.6 → 0.4.7
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 +9 -0
- package/README.md +2 -0
- package/dist/folder-zip-node.d.ts +11 -0
- package/dist/folder-zip-node.js +74 -0
- package/dist/folder-zip.d.ts +38 -0
- package/dist/folder-zip.js +132 -0
- package/dist/lifecycle/install-errors.d.ts +14 -0
- package/dist/lifecycle/install-errors.js +9 -0
- package/dist/lifecycle/install.d.ts +2 -13
- package/dist/lifecycle/install.js +5 -123
- package/dist/lifecycle/installed-package-index.d.ts +15 -0
- package/dist/lifecycle/installed-package-index.js +116 -0
- package/dist/lifecycle/manifest.d.ts +1 -0
- package/dist/lifecycle/manifest.js +1 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.js +4 -4
- package/dist/manifest.d.ts +2 -1
- package/dist/manifest.js +2 -1
- package/package.json +14 -2
- package/ui-ux-design/plugin.json +1 -0
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle/activation.d.ts.map +0 -1
- package/dist/lifecycle/activation.js.map +0 -1
- package/dist/lifecycle/bundled-digests.d.ts.map +0 -1
- package/dist/lifecycle/bundled-digests.js.map +0 -1
- package/dist/lifecycle/bundled-source-archive.d.ts.map +0 -1
- package/dist/lifecycle/bundled-source-archive.js.map +0 -1
- package/dist/lifecycle/capability-projection.d.ts.map +0 -1
- package/dist/lifecycle/capability-projection.js.map +0 -1
- package/dist/lifecycle/contribution-loader.d.ts.map +0 -1
- package/dist/lifecycle/contribution-loader.js.map +0 -1
- package/dist/lifecycle/fetch-archive.d.ts.map +0 -1
- package/dist/lifecycle/fetch-archive.js.map +0 -1
- package/dist/lifecycle/index.d.ts.map +0 -1
- package/dist/lifecycle/index.js.map +0 -1
- package/dist/lifecycle/install-from-url.d.ts.map +0 -1
- package/dist/lifecycle/install-from-url.js.map +0 -1
- package/dist/lifecycle/install.d.ts.map +0 -1
- package/dist/lifecycle/install.js.map +0 -1
- package/dist/lifecycle/installed-package-roots.d.ts.map +0 -1
- package/dist/lifecycle/installed-package-roots.js.map +0 -1
- package/dist/lifecycle/layout.d.ts.map +0 -1
- package/dist/lifecycle/layout.js.map +0 -1
- package/dist/lifecycle/manifest.d.ts.map +0 -1
- package/dist/lifecycle/manifest.js.map +0 -1
- package/dist/lifecycle/mcp-provisioning.d.ts.map +0 -1
- package/dist/lifecycle/mcp-provisioning.js.map +0 -1
- package/dist/lifecycle/node.d.ts.map +0 -1
- package/dist/lifecycle/node.js.map +0 -1
- package/dist/lifecycle/package-paths.d.ts.map +0 -1
- package/dist/lifecycle/package-paths.js.map +0 -1
- package/dist/lifecycle/persistent-state.d.ts.map +0 -1
- package/dist/lifecycle/persistent-state.js.map +0 -1
- package/dist/lifecycle/ports.d.ts.map +0 -1
- package/dist/lifecycle/ports.js.map +0 -1
- package/dist/lifecycle/resolve-agent-plugin-refs.d.ts.map +0 -1
- package/dist/lifecycle/resolve-agent-plugin-refs.js.map +0 -1
- package/dist/lifecycle/retire-bundled.d.ts.map +0 -1
- package/dist/lifecycle/retire-bundled.js.map +0 -1
- package/dist/lifecycle/search.d.ts.map +0 -1
- package/dist/lifecycle/search.js.map +0 -1
- package/dist/lifecycle/seed-bundled.d.ts.map +0 -1
- package/dist/lifecycle/seed-bundled.js.map +0 -1
- package/dist/lifecycle/set-enabled.d.ts.map +0 -1
- package/dist/lifecycle/set-enabled.js.map +0 -1
- package/dist/lifecycle/skill-file-reader.d.ts.map +0 -1
- package/dist/lifecycle/skill-file-reader.js.map +0 -1
- package/dist/lifecycle/trusted-plugin-files.d.ts.map +0 -1
- package/dist/lifecycle/trusted-plugin-files.js.map +0 -1
- package/dist/lifecycle/types.d.ts.map +0 -1
- package/dist/lifecycle/types.js.map +0 -1
- package/dist/lifecycle/uninstall.d.ts.map +0 -1
- package/dist/lifecycle/uninstall.js.map +0 -1
- package/dist/lifecycle/yauzl-archive-reader.d.ts.map +0 -1
- package/dist/lifecycle/yauzl-archive-reader.js.map +0 -1
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.7 — 2026-10-15 — folder uploads share one ZIP encoder
|
|
4
|
+
|
|
5
|
+
- New `./folder-zip` (universal): `zipFolderFiles()`, the binary-safe stored-mode folder encoder,
|
|
6
|
+
moved here from Tovu's admin so browser and server folder installs pack identically.
|
|
7
|
+
- New `./folder-zip/node`: `zipLocalFolder()` reads a local folder into that same encoder. Refuses
|
|
8
|
+
symlinks and special files, bounds entries (4096) and bytes before reading, and sorts paths so the
|
|
9
|
+
same folder always packs to the same digest.
|
|
10
|
+
- The bundled `ui-ux-design/plugin.json` declares its version (abb0009e).
|
|
11
|
+
|
|
3
12
|
## 0.4.6 — 2026-10-13 — skills' supporting files are readable
|
|
4
13
|
|
|
5
14
|
- New `createSkillFileReader()` and lifecycle `readInstalledSkillFile()`: read `SKILL.md` or a
|
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ Catalog format) that this package does not implement.
|
|
|
106
106
|
| `./lifecycle` | node | Host-injected installation, activation, locks, digests, bundles, trusted files, reference resolution, ranking and MCP provisioning helpers. |
|
|
107
107
|
| `./lifecycle/node` | node | `createNodeAgentPluginEffects({})` opt-in native effects. |
|
|
108
108
|
| `./lifecycle/yauzl` | node | `createYauzlAgentPluginArchiveReader({ yauzl })`; the host supplies the optional peer library. |
|
|
109
|
+
| `./folder-zip` | universal | `zipFolderFiles({ files, maxBytes })`: packs picked folder `File`s into one stored-mode, binary-safe `.zip` `File` (the admin "Upload a folder" encoder). |
|
|
110
|
+
| `./folder-zip/node` | node | `zipLocalFolder({ path, maxBytes })`: the same encoder over a local folder; refuses symlinks/special files, bounds entries and bytes. |
|
|
109
111
|
| `./ui-ux-design/*` | — | Raw files of the bundled `ui-ux-design` plugin — static JSON/Markdown, not run through the TS build. |
|
|
110
112
|
|
|
111
113
|
The universal root handles structural validation. The separate Node lifecycle entry implements
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Reads a local folder into the browser upload encoder. Refuses links/special files and bounds
|
|
2
|
+
* entries and bytes before allocation; the resulting ZIP still requires the install validator.
|
|
3
|
+
* @throws {FolderZipError} Empty, unreadable, linked, or over-limit folders.
|
|
4
|
+
* @complexity O(e log e + b) time for e entries and b bytes; O(e + b) space,
|
|
5
|
+
* bounded to 4096 entries and maxBytes. No network I/O.
|
|
6
|
+
*/
|
|
7
|
+
export declare function zipLocalFolder(required: {
|
|
8
|
+
path: string;
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
}, _optional?: Record<string, never>): Promise<Uint8Array>;
|
|
11
|
+
//# sourceMappingURL=folder-zip-node.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { FOLDER_MAX_FILES, FOLDER_UNREADABLE, FolderZipError, zipFolderFiles } from "./folder-zip.js";
|
|
5
|
+
/** Reads a local folder into the browser upload encoder. Refuses links/special files and bounds
|
|
6
|
+
* entries and bytes before allocation; the resulting ZIP still requires the install validator.
|
|
7
|
+
* @throws {FolderZipError} Empty, unreadable, linked, or over-limit folders.
|
|
8
|
+
* @complexity O(e log e + b) time for e entries and b bytes; O(e + b) space,
|
|
9
|
+
* bounded to 4096 entries and maxBytes. No network I/O.
|
|
10
|
+
*/
|
|
11
|
+
export async function zipLocalFolder(required, _optional = {}) {
|
|
12
|
+
const files = [];
|
|
13
|
+
let entries = 0;
|
|
14
|
+
let total = 0;
|
|
15
|
+
try {
|
|
16
|
+
const root = await fs.realpath(required.path);
|
|
17
|
+
const pending = [root];
|
|
18
|
+
while (pending.length) {
|
|
19
|
+
const directory = pending.pop();
|
|
20
|
+
if (!(await fs.lstat(directory)).isDirectory() || await fs.realpath(directory) !== directory)
|
|
21
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
22
|
+
const handle = await fs.opendir(directory);
|
|
23
|
+
for await (const entry of handle) {
|
|
24
|
+
if (++entries > FOLDER_MAX_FILES)
|
|
25
|
+
throw new FolderZipError("too-many", `More than ${FOLDER_MAX_FILES} entries.`);
|
|
26
|
+
const filename = path.join(directory, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
pending.push(filename);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!entry.isFile())
|
|
32
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
33
|
+
if (await fs.realpath(filename) !== filename)
|
|
34
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
35
|
+
// O_NOFOLLOW refuses a file replaced with a symlink between enumeration and opening.
|
|
36
|
+
const file = await fs.open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
37
|
+
try {
|
|
38
|
+
const info = await file.stat();
|
|
39
|
+
if (!info.isFile())
|
|
40
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
41
|
+
total += info.size;
|
|
42
|
+
if (total > required.maxBytes)
|
|
43
|
+
throw new FolderZipError("too-large", "Folder is over the upload limit.");
|
|
44
|
+
// One extra byte detects growth without an unbounded readFile allocation.
|
|
45
|
+
const bytes = Buffer.alloc(info.size + 1);
|
|
46
|
+
let length = 0;
|
|
47
|
+
while (length < bytes.length) {
|
|
48
|
+
const read = await file.read(bytes, length, bytes.length - length, null);
|
|
49
|
+
if (read.bytesRead === 0)
|
|
50
|
+
break;
|
|
51
|
+
length += read.bytesRead;
|
|
52
|
+
}
|
|
53
|
+
if (length !== info.size)
|
|
54
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
55
|
+
const picked = new File([new Uint8Array(bytes.subarray(0, length))], entry.name);
|
|
56
|
+
Object.defineProperty(picked, "webkitRelativePath", { value: `folder/${path.relative(root, filename).split(path.sep).join("/")}` });
|
|
57
|
+
files.push(picked);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await file.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Stable order gives repeat local-folder installs identical ZIP digests.
|
|
65
|
+
files.sort((a, b) => a.webkitRelativePath < b.webkitRelativePath ? -1 : a.webkitRelativePath > b.webkitRelativePath ? 1 : 0);
|
|
66
|
+
return new Uint8Array(await (await zipFolderFiles({ files, maxBytes: required.maxBytes })).arrayBuffer());
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof FolderZipError)
|
|
70
|
+
throw error;
|
|
71
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=folder-zip-node.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Packs a folder the operator picked (`<input type=file webkitdirectory>`) into one `.zip`
|
|
3
|
+
* `File`, so "Upload a folder" travels the exact path a `.zip` upload does: same size limit, same
|
|
4
|
+
* server-side archive checks, same Preview trust review. The install routes only take
|
|
5
|
+
* `application/zip`, so zipping here keeps one server path instead of adding a second upload format.
|
|
6
|
+
*
|
|
7
|
+
* Stored mode (no compression): the upload limit is on the archive, and a plugin folder is small
|
|
8
|
+
* text, so deflate would add a dependency for little gain. `@jini-ai/ui` has a stored-mode
|
|
9
|
+
* `buildZip`, but it is text-only (it would corrupt an image) and not exported, so this is the
|
|
10
|
+
* binary-safe twin, extracted to Jini so browser and local-folder uploads use the same encoder.
|
|
11
|
+
*/
|
|
12
|
+
/** Refusals, as dictionary keys the caller translates. */
|
|
13
|
+
export declare const FOLDER_EMPTY = "That folder is empty.";
|
|
14
|
+
export declare const FOLDER_UNREADABLE = "Could not read that folder. Try again.";
|
|
15
|
+
/** The archive readers' entry cap (`install-archive.ts`, the Agent Plugins reader): refused here
|
|
16
|
+
* before any bytes are read. */
|
|
17
|
+
export declare const FOLDER_MAX_FILES = 4096;
|
|
18
|
+
export declare class FolderZipError extends Error {
|
|
19
|
+
readonly reason: "empty" | "too-large" | "too-many" | "unreadable";
|
|
20
|
+
constructor(reason: "empty" | "too-large" | "too-many" | "unreadable", message: string);
|
|
21
|
+
}
|
|
22
|
+
/** @complexity O(n) in the byte count. */
|
|
23
|
+
export declare function crc32(bytes: Uint8Array): number;
|
|
24
|
+
/**
|
|
25
|
+
* Zips the files of one picked folder.
|
|
26
|
+
*
|
|
27
|
+
* @param required.files - Every file the folder picker returned.
|
|
28
|
+
* @param required.maxBytes - The upload limit; the folder's raw size alone over it is refused before
|
|
29
|
+
* reading, and the finished zip is checked again (headers add a little).
|
|
30
|
+
* @returns `<folder>.zip`, typed `application/zip`.
|
|
31
|
+
* @throws {FolderZipError} empty folder, too many files, too large, or a file that could not be read.
|
|
32
|
+
* @complexity O(total bytes).
|
|
33
|
+
*/
|
|
34
|
+
export declare function zipFolderFiles(required: {
|
|
35
|
+
files: readonly File[];
|
|
36
|
+
maxBytes: number;
|
|
37
|
+
}, _optional?: Record<string, never>): Promise<File>;
|
|
38
|
+
//# sourceMappingURL=folder-zip.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Packs a folder the operator picked (`<input type=file webkitdirectory>`) into one `.zip`
|
|
3
|
+
* `File`, so "Upload a folder" travels the exact path a `.zip` upload does: same size limit, same
|
|
4
|
+
* server-side archive checks, same Preview trust review. The install routes only take
|
|
5
|
+
* `application/zip`, so zipping here keeps one server path instead of adding a second upload format.
|
|
6
|
+
*
|
|
7
|
+
* Stored mode (no compression): the upload limit is on the archive, and a plugin folder is small
|
|
8
|
+
* text, so deflate would add a dependency for little gain. `@jini-ai/ui` has a stored-mode
|
|
9
|
+
* `buildZip`, but it is text-only (it would corrupt an image) and not exported, so this is the
|
|
10
|
+
* binary-safe twin, extracted to Jini so browser and local-folder uploads use the same encoder.
|
|
11
|
+
*/
|
|
12
|
+
/** Refusals, as dictionary keys the caller translates. */
|
|
13
|
+
export const FOLDER_EMPTY = "That folder is empty.";
|
|
14
|
+
export const FOLDER_UNREADABLE = "Could not read that folder. Try again.";
|
|
15
|
+
/** The archive readers' entry cap (`install-archive.ts`, the Agent Plugins reader): refused here
|
|
16
|
+
* before any bytes are read. */
|
|
17
|
+
export const FOLDER_MAX_FILES = 4096;
|
|
18
|
+
export class FolderZipError extends Error {
|
|
19
|
+
reason;
|
|
20
|
+
constructor(reason, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** 1980-01-01, the earliest DOS date: a fixed stamp, so the same folder always zips to the same
|
|
26
|
+
* bytes (an all-zero date is month 0, which some readers refuse). */
|
|
27
|
+
const DOS_DATE_1980 = (1 << 5) | 1;
|
|
28
|
+
const CRC_TABLE = (() => {
|
|
29
|
+
const table = new Uint32Array(256);
|
|
30
|
+
for (let n = 0; n < 256; n++) {
|
|
31
|
+
let c = n;
|
|
32
|
+
for (let k = 0; k < 8; k++)
|
|
33
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
34
|
+
table[n] = c >>> 0;
|
|
35
|
+
}
|
|
36
|
+
return table;
|
|
37
|
+
})();
|
|
38
|
+
/** @complexity O(n) in the byte count. */
|
|
39
|
+
export function crc32(bytes) {
|
|
40
|
+
let c = 0xffffffff;
|
|
41
|
+
for (let i = 0; i < bytes.length; i++)
|
|
42
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
43
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
/** A picked file's path inside the picked folder: `webkitRelativePath` minus the folder's own name,
|
|
46
|
+
* so the package's files sit at the top of the zip — what a server-path install of that same
|
|
47
|
+
* folder reads, and where both install routes look for the manifest. */
|
|
48
|
+
function entryPath(file) {
|
|
49
|
+
const relative = (file.webkitRelativePath || file.name).replaceAll("\\", "/");
|
|
50
|
+
const slash = relative.indexOf("/");
|
|
51
|
+
return slash < 0 ? { folder: "", path: relative } : { folder: relative.slice(0, slash), path: relative.slice(slash + 1) };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Zips the files of one picked folder.
|
|
55
|
+
*
|
|
56
|
+
* @param required.files - Every file the folder picker returned.
|
|
57
|
+
* @param required.maxBytes - The upload limit; the folder's raw size alone over it is refused before
|
|
58
|
+
* reading, and the finished zip is checked again (headers add a little).
|
|
59
|
+
* @returns `<folder>.zip`, typed `application/zip`.
|
|
60
|
+
* @throws {FolderZipError} empty folder, too many files, too large, or a file that could not be read.
|
|
61
|
+
* @complexity O(total bytes).
|
|
62
|
+
*/
|
|
63
|
+
export async function zipFolderFiles(required, _optional = {}) {
|
|
64
|
+
const { files, maxBytes } = required;
|
|
65
|
+
if (files.length === 0)
|
|
66
|
+
throw new FolderZipError("empty", FOLDER_EMPTY);
|
|
67
|
+
if (files.length > FOLDER_MAX_FILES)
|
|
68
|
+
throw new FolderZipError("too-many", `More than ${FOLDER_MAX_FILES} files.`);
|
|
69
|
+
if (files.reduce((sum, file) => sum + file.size, 0) > maxBytes)
|
|
70
|
+
throw new FolderZipError("too-large", "Folder is over the upload limit.");
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const chunks = [];
|
|
73
|
+
const central = [];
|
|
74
|
+
let offset = 0;
|
|
75
|
+
let folder = "";
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const entry = entryPath(file);
|
|
78
|
+
folder ||= entry.folder;
|
|
79
|
+
let data;
|
|
80
|
+
try {
|
|
81
|
+
data = new Uint8Array(await file.arrayBuffer());
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
|
|
85
|
+
}
|
|
86
|
+
const name = encoder.encode(entry.path);
|
|
87
|
+
const crc = crc32(data);
|
|
88
|
+
// Bit 11: the name is UTF-8, so a reader never decodes it as CP437.
|
|
89
|
+
const flags = name.some((b) => b > 0x7f) ? 0x0800 : 0;
|
|
90
|
+
const local = new Uint8Array(30 + name.length);
|
|
91
|
+
const lv = new DataView(local.buffer);
|
|
92
|
+
lv.setUint32(0, 0x04034b50, true);
|
|
93
|
+
lv.setUint16(4, 20, true);
|
|
94
|
+
lv.setUint16(6, flags, true);
|
|
95
|
+
lv.setUint16(12, DOS_DATE_1980, true);
|
|
96
|
+
lv.setUint32(14, crc, true);
|
|
97
|
+
lv.setUint32(18, data.length, true);
|
|
98
|
+
lv.setUint32(22, data.length, true);
|
|
99
|
+
lv.setUint16(26, name.length, true);
|
|
100
|
+
local.set(name, 30);
|
|
101
|
+
// Version-made-by 20 (MS-DOS, external attributes 0): a plain file to every reader, so the
|
|
102
|
+
// server's Unix special-entry guard never sees a mode to refuse.
|
|
103
|
+
const header = new Uint8Array(46 + name.length);
|
|
104
|
+
const cv = new DataView(header.buffer);
|
|
105
|
+
cv.setUint32(0, 0x02014b50, true);
|
|
106
|
+
cv.setUint16(4, 20, true);
|
|
107
|
+
cv.setUint16(6, 20, true);
|
|
108
|
+
cv.setUint16(8, flags, true);
|
|
109
|
+
cv.setUint16(14, DOS_DATE_1980, true);
|
|
110
|
+
cv.setUint32(16, crc, true);
|
|
111
|
+
cv.setUint32(20, data.length, true);
|
|
112
|
+
cv.setUint32(24, data.length, true);
|
|
113
|
+
cv.setUint16(28, name.length, true);
|
|
114
|
+
cv.setUint32(42, offset, true);
|
|
115
|
+
header.set(name, 46);
|
|
116
|
+
chunks.push(local, data);
|
|
117
|
+
central.push(header);
|
|
118
|
+
offset += local.length + data.length;
|
|
119
|
+
}
|
|
120
|
+
const centralSize = central.reduce((sum, part) => sum + part.length, 0);
|
|
121
|
+
const end = new Uint8Array(22);
|
|
122
|
+
const ev = new DataView(end.buffer);
|
|
123
|
+
ev.setUint32(0, 0x06054b50, true);
|
|
124
|
+
ev.setUint16(8, files.length, true);
|
|
125
|
+
ev.setUint16(10, files.length, true);
|
|
126
|
+
ev.setUint32(12, centralSize, true);
|
|
127
|
+
ev.setUint32(16, offset, true);
|
|
128
|
+
if (offset + centralSize + end.length > maxBytes)
|
|
129
|
+
throw new FolderZipError("too-large", "Folder is over the upload limit.");
|
|
130
|
+
return new File([...chunks, ...central, end], `${folder || "folder"}.zip`, { type: "application/zip" });
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=folder-zip.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One extraction/install failure reason. A caller (an admin route, a future marketplace installer)
|
|
2
|
+
* branches on `code` rather than parsing `message` — this codebase's own convention
|
|
3
|
+
* (`PluginLoadError.reason` in `plugin-runtime/loader.ts` is the direct precedent). */
|
|
4
|
+
export type AgentPluginInstallErrorCode = "PLUGIN_ID_CONFLICT" | "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
|
|
5
|
+
export declare class AgentPluginInstallError extends Error {
|
|
6
|
+
readonly code: AgentPluginInstallErrorCode;
|
|
7
|
+
constructor({ code, message }: {
|
|
8
|
+
readonly code: AgentPluginInstallErrorCode;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}, options?: {
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=install-errors.d.ts.map
|
|
@@ -2,19 +2,8 @@ import type { AgentPluginLayout } from "./layout.js";
|
|
|
2
2
|
import type { AgentPluginLifecyclePorts } from "./ports.js";
|
|
3
3
|
import type { InstalledAgentPlugin } from "./types.js";
|
|
4
4
|
export type { InstalledAgentPlugin, InstalledAgentPluginSkill } from "./types.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* (`PluginLoadError.reason` in `plugin-runtime/loader.ts` is the direct precedent). */
|
|
8
|
-
export type AgentPluginInstallErrorCode = "PLUGIN_ID_CONFLICT" | "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
|
|
9
|
-
export declare class AgentPluginInstallError extends Error {
|
|
10
|
-
readonly code: AgentPluginInstallErrorCode;
|
|
11
|
-
constructor({ code, message }: {
|
|
12
|
-
readonly code: AgentPluginInstallErrorCode;
|
|
13
|
-
readonly message: string;
|
|
14
|
-
}, options?: {
|
|
15
|
-
cause?: unknown;
|
|
16
|
-
});
|
|
17
|
-
}
|
|
5
|
+
export { AgentPluginInstallError } from "./install-errors.js";
|
|
6
|
+
export type { AgentPluginInstallErrorCode } from "./install-errors.js";
|
|
18
7
|
/** One archive entry. A symlink is modeled as its OWN kind rather than folded into "file" — the
|
|
19
8
|
* zip-slip research this module's header cites is explicit that the symlink-entry vector is
|
|
20
9
|
* distinct from the lexical-traversal one, and the two need independent tests and independent
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { listInstalledPackageRoots } from "./installed-package-roots.js";
|
|
2
2
|
import { createUninstallModule } from "./uninstall.js";
|
|
3
|
+
import { AgentPluginInstallError } from "./install-errors.js";
|
|
4
|
+
import { createInstalledPackageIndex } from "./installed-package-index.js";
|
|
3
5
|
import { readPluginIdentity, assertPluginIdentityAvailable } from "@jini-ai/core/plugin-directory";
|
|
4
6
|
/**
|
|
5
7
|
* @file `installAgentPlugin()` — content-addressed, adversarially-hardened extraction of one Agent
|
|
@@ -60,33 +62,14 @@ import { readPluginIdentity, assertPluginIdentityAvailable } from "@jini-ai/core
|
|
|
60
62
|
* (archive bytes and their expected digest are caller-supplied — see the module header above for why
|
|
61
63
|
* "how does the archive get here" is out of this slice) and no execution of anything extracted.
|
|
62
64
|
*/
|
|
63
|
-
/** One trimmed, non-blank string field of the host-supplied extension namespace, capped at `maxLength`, else
|
|
64
|
-
* `undefined` — a malformed value falls back rather than failing the whole index, since every
|
|
65
|
-
* field read this way is cosmetic. The cap stops a hostile package flooding the admin row.
|
|
66
|
-
* @complexity O(maxLength). */
|
|
67
|
-
/** `{ displayName }` when the host namespace's `displayName` is a non-blank string, else `{}` (callers
|
|
68
|
-
* title-case the id).
|
|
69
|
-
* @complexity O(1). */
|
|
70
|
-
/** `{ summary }` when the host namespace's `summary` is a non-blank string, else `{}` (the admin shows
|
|
71
|
-
* the spec `description` instead). Inner newlines are kept: blank lines are the paragraph breaks
|
|
72
|
-
* the admin renders.
|
|
73
|
-
* @complexity O(1). */
|
|
74
65
|
import { createPersistentStateModule } from "./persistent-state.js";
|
|
75
66
|
/** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
|
|
76
67
|
import { createHash } from "node:crypto";
|
|
77
68
|
import { constants } from "node:fs";
|
|
78
69
|
import path from "node:path";
|
|
79
|
-
import { readAgentPluginExtension, parseAgentPluginManifest as parseAgentPluginManifestValue } from "./manifest.js";
|
|
80
70
|
import { PackagePathViolation } from "./package-paths.js";
|
|
81
71
|
import { createPackagePathsModule } from "./package-paths.js";
|
|
82
|
-
export
|
|
83
|
-
code;
|
|
84
|
-
constructor({ code, message }, options = {}) {
|
|
85
|
-
super(message, options);
|
|
86
|
-
this.name = "AgentPluginInstallError";
|
|
87
|
-
this.code = code;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
72
|
+
export { AgentPluginInstallError } from "./install-errors.js";
|
|
90
73
|
function buildModule(ports) {
|
|
91
74
|
const persistentState = createPersistentStateModule(ports);
|
|
92
75
|
const chmod = ports.filesystem.chmod.bind(ports.filesystem);
|
|
@@ -98,7 +81,8 @@ function buildModule(ports) {
|
|
|
98
81
|
const rename = ports.filesystem.rename.bind(ports.filesystem);
|
|
99
82
|
const rm = ports.filesystem.rm.bind(ports.filesystem);
|
|
100
83
|
const stat = ports.filesystem.stat.bind(ports.filesystem);
|
|
101
|
-
const
|
|
84
|
+
const index = createInstalledPackageIndex({ ports });
|
|
85
|
+
const indexInstalledRoot = (packageRoot, archiveDigest) => index.indexInstalledRoot({ packageRoot, archiveDigest });
|
|
102
86
|
const { assertContainedOnDisk, normalizePackageEntryPath } = createPackagePathsModule(ports);
|
|
103
87
|
const LIMITS = {
|
|
104
88
|
maxArchiveBytes: 32 * 1024 * 1024,
|
|
@@ -145,7 +129,6 @@ function buildModule(ports) {
|
|
|
145
129
|
*/
|
|
146
130
|
async function pruneSupersededRevisions({ workspaceRoot, installed }, _optional = {}) {
|
|
147
131
|
try {
|
|
148
|
-
// Resolve lazily: uninstall's read-side index uses this install module for manifest indexing.
|
|
149
132
|
const removal = createUninstallModule(ports);
|
|
150
133
|
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
151
134
|
if (root.localId !== installed.pluginId || root.archiveDigest === installed.archiveDigest)
|
|
@@ -400,107 +383,6 @@ function buildModule(ports) {
|
|
|
400
383
|
}
|
|
401
384
|
return totalBytes;
|
|
402
385
|
}
|
|
403
|
-
/** Cosmetic fields are trimmed and capped rather than invalidating the package. The cap stops
|
|
404
|
-
* a hostile package flooding an admin row. Inner newlines remain paragraph breaks in summaries.
|
|
405
|
-
* @complexity O(n) in the supplied text length; output is capped at maxLength. */
|
|
406
|
-
function readDisplayText(required, _optional = {}) {
|
|
407
|
-
const text = readAgentPluginExtension({ manifest: required.manifest, namespace: ports.extensionNamespace,
|
|
408
|
-
read: ({ value }) => {
|
|
409
|
-
const field = value[required.key];
|
|
410
|
-
return typeof field === 'string' ? field.trim() : undefined;
|
|
411
|
-
} });
|
|
412
|
-
return text ? { [required.key]: text.slice(0, required.maxLength) } : {};
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Reads the (already-extracted-and-trusted, or already-published) `plugin.json` at `packageRoot`
|
|
416
|
-
* and walks the tree to build the `files`/`skills` index. Shared between a fresh install and the
|
|
417
|
-
* content-addressed dedup fast path so both return an identically-shaped result.
|
|
418
|
-
*
|
|
419
|
-
* Exported (2026-08-21) for a second caller outside this module:
|
|
420
|
-
* `resolve-agent-plugin-refs.ts`'s run-time resolution of a pinned `pluginRefId` against the
|
|
421
|
-
* packages already on disk needs the SAME `plugin.json`-parse-plus-tree-walk this function
|
|
422
|
-
* already does correctly (manifest validation via `parseAgentPluginManifest`, sorted `files`), so
|
|
423
|
-
* it re-derives an `InstalledAgentPlugin` per installed digest rather than hand-rolling a second,
|
|
424
|
-
* less-validated reader.
|
|
425
|
-
*/
|
|
426
|
-
// Conditional spreads, not `field: parsed.manifest.field` directly: an `exactOptionalPropertyTypes`-
|
|
427
|
-
// style caller (and this codebase's own convention elsewhere — see `buildToolSource`'s identical
|
|
428
|
-
// `defaultSkillReason` spread in `tool-registrations.ts`) must see an ABSENT key, not a key present
|
|
429
|
-
// with value `undefined`, when the manifest declared none of these — `deepEqual` fixtures across
|
|
430
|
-
// this feature's own tests already assert exact object shape.
|
|
431
|
-
async function indexInstalledRoot(packageRoot, /** SHA-256 of the raw archive bytes — the content-addressing key and the descriptor `revision`
|
|
432
|
-
* a future capability projection pins invocation to (`capability-projection.ts`). */ archiveDigest) {
|
|
433
|
-
let manifestRaw;
|
|
434
|
-
try {
|
|
435
|
-
manifestRaw = await readFile(path.join(packageRoot, "plugin.json"), "utf8");
|
|
436
|
-
}
|
|
437
|
-
catch (error) {
|
|
438
|
-
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
439
|
-
throw new AgentPluginInstallError({ code: "MANIFEST_MISSING", message: "the archive does not contain a plugin.json at its root" });
|
|
440
|
-
}
|
|
441
|
-
throw error;
|
|
442
|
-
}
|
|
443
|
-
let manifestValue;
|
|
444
|
-
try {
|
|
445
|
-
manifestValue = JSON.parse(manifestRaw);
|
|
446
|
-
}
|
|
447
|
-
catch (error) {
|
|
448
|
-
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: "plugin.json is not valid JSON" }, { cause: error });
|
|
449
|
-
}
|
|
450
|
-
const parsed = parseAgentPluginManifest(manifestValue);
|
|
451
|
-
if (!parsed.ok) {
|
|
452
|
-
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
|
|
453
|
-
}
|
|
454
|
-
const files = [];
|
|
455
|
-
for (const message of parsed.warnings)
|
|
456
|
-
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
|
|
457
|
-
const skills = [];
|
|
458
|
-
async function walk(absolute, prefix) {
|
|
459
|
-
for (const entry of await readdir(absolute, { withFileTypes: true })) {
|
|
460
|
-
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
461
|
-
if (entry.isDirectory()) {
|
|
462
|
-
await walk(path.join(absolute, entry.name), relative);
|
|
463
|
-
}
|
|
464
|
-
else if (entry.isFile()) {
|
|
465
|
-
files.push(relative);
|
|
466
|
-
const match = relative.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
|
467
|
-
if (match)
|
|
468
|
-
skills.push({ name: match[1], skillPath: relative });
|
|
469
|
-
}
|
|
470
|
-
// Anything that is neither a directory nor a regular file (a symlink somehow already present,
|
|
471
|
-
// a device node) is skipped rather than indexed — extraction itself never creates one, so
|
|
472
|
-
// encountering one here would mean the filesystem changed out from under an already-published,
|
|
473
|
-
// supposedly-frozen package, which this function has no business acting on either way.
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
await walk(packageRoot, "");
|
|
477
|
-
return {
|
|
478
|
-
pluginId: parsed.manifest.name,
|
|
479
|
-
id: parsed.manifest.id,
|
|
480
|
-
provides: parsed.manifest.provides,
|
|
481
|
-
version: parsed.manifest.version,
|
|
482
|
-
...( /**
|
|
483
|
-
* `plugin.json`'s own `description`/`keywords`/`author`/`license` (`manifest.ts`'s
|
|
484
|
-
* `AgentPluginManifest`) — parsed at index time but, before 2026-09-09, discarded immediately
|
|
485
|
-
* after: only `name`/`version` ever reached `InstalledAgentPlugin`. Added for
|
|
486
|
-
* `search_agent_plugin_local` (`tool-registrations.ts`), which needs the description and the
|
|
487
|
-
* author-curated keywords to rank installed plugins against a free-text query — the manifest is
|
|
488
|
-
* already read and parsed here on every call, so carrying these through is zero additional I/O,
|
|
489
|
-
* not a new read path. All four are optional because the spec itself makes them optional (the real
|
|
490
|
-
* installed `ui-ux-design` fixture ships `description` but no `keywords`; a bare-minimum manifest
|
|
491
|
-
* ships neither) — a caller must not assume any of them are present.
|
|
492
|
-
*/parsed.manifest.description !== undefined ? { description: parsed.manifest.description } : {}),
|
|
493
|
-
...(parsed.manifest.keywords !== undefined ? { keywords: parsed.manifest.keywords } : {}),
|
|
494
|
-
...(parsed.manifest.author !== undefined ? { author: parsed.manifest.author } : {}),
|
|
495
|
-
...(parsed.manifest.license !== undefined ? { license: parsed.manifest.license } : {}),
|
|
496
|
-
...readDisplayText({ manifest: parsed.manifest, key: "displayName", maxLength: 64 }),
|
|
497
|
-
...readDisplayText({ manifest: parsed.manifest, key: "summary", maxLength: 1200 }),
|
|
498
|
-
archiveDigest,
|
|
499
|
-
packageRoot,
|
|
500
|
-
files: files.sort(),
|
|
501
|
-
skills: skills.sort((a, b) => a.name.localeCompare(b.name)),
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
386
|
/** Makes every file/directory under `root` read-only (executables keep `+x`, per the Agent Plugins
|
|
505
387
|
* spec's own MCP `command` needing to launch a `./`-relative script — not exercised by anything in
|
|
506
388
|
* this slice, since MCP execution is out of scope for v1, but the bit is preserved now rather than
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentPluginLifecyclePorts } from "./ports.js";
|
|
2
|
+
import type { InstalledAgentPlugin } from "./types.js";
|
|
3
|
+
/** Indexes manifests and files with the same injected effects used by installation and resolution.
|
|
4
|
+
* @complexity O(f) reads/walk work for f package files; no mutation or executable imports. */
|
|
5
|
+
export declare function createInstalledPackageIndex(required: {
|
|
6
|
+
readonly ports: AgentPluginLifecyclePorts;
|
|
7
|
+
}, _optional?: Record<string, never>): {
|
|
8
|
+
indexInstalledRoot: ({ packageRoot, archiveDigest }: {
|
|
9
|
+
readonly packageRoot: string;
|
|
10
|
+
/** SHA-256 of the raw archive bytes — the content-addressing key and the descriptor `revision`
|
|
11
|
+
* a future capability projection pins invocation to (`capability-projection.ts`). */
|
|
12
|
+
readonly archiveDigest: string;
|
|
13
|
+
}, _optional?: Record<string, never>) => Promise<InstalledAgentPlugin>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=installed-package-index.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Read-side owner of installed package metadata; never constructs a mutation lifecycle. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AgentPluginInstallError } from "./install-errors.js";
|
|
4
|
+
import { readAgentPluginExtension, parseAgentPluginManifest as parseAgentPluginManifestValue } from "./manifest.js";
|
|
5
|
+
/** Indexes manifests and files with the same injected effects used by installation and resolution.
|
|
6
|
+
* @complexity O(f) reads/walk work for f package files; no mutation or executable imports. */
|
|
7
|
+
export function createInstalledPackageIndex(required, _optional = {}) {
|
|
8
|
+
const { ports } = required;
|
|
9
|
+
const readFile = ports.filesystem.readFile.bind(ports.filesystem);
|
|
10
|
+
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
11
|
+
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value }, { identityNamespace: ports.extensionNamespace });
|
|
12
|
+
/** Cosmetic fields are trimmed and capped rather than invalidating the package. The cap stops
|
|
13
|
+
* a hostile package flooding an admin row. Malformed values fall back because these fields are
|
|
14
|
+
* cosmetic: callers title-case the id and show the spec description when these keys are absent.
|
|
15
|
+
* Inner newlines remain paragraph breaks in summaries.
|
|
16
|
+
* @complexity O(n) in the supplied text length; output is capped at maxLength. */
|
|
17
|
+
function readDisplayText(required, _optional = {}) {
|
|
18
|
+
const text = readAgentPluginExtension({ manifest: required.manifest, namespace: ports.extensionNamespace,
|
|
19
|
+
read: ({ value }) => {
|
|
20
|
+
const field = value[required.key];
|
|
21
|
+
return typeof field === 'string' ? field.trim() : undefined;
|
|
22
|
+
} });
|
|
23
|
+
return text ? { [required.key]: text.slice(0, required.maxLength) } : {};
|
|
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 = {}) {
|
|
43
|
+
let manifestRaw;
|
|
44
|
+
try {
|
|
45
|
+
manifestRaw = await readFile(path.join(packageRoot, "plugin.json"), "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
49
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_MISSING", message: "the archive does not contain a plugin.json at its root" });
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
let manifestValue;
|
|
54
|
+
try {
|
|
55
|
+
manifestValue = JSON.parse(manifestRaw);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: "plugin.json is not valid JSON" }, { cause: error });
|
|
59
|
+
}
|
|
60
|
+
const parsed = parseAgentPluginManifest(manifestValue);
|
|
61
|
+
if (!parsed.ok) {
|
|
62
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
|
|
63
|
+
}
|
|
64
|
+
const files = [];
|
|
65
|
+
for (const message of parsed.warnings)
|
|
66
|
+
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
|
|
67
|
+
const skills = [];
|
|
68
|
+
async function walk(absolute, prefix) {
|
|
69
|
+
for (const entry of await readdir(absolute, { withFileTypes: true })) {
|
|
70
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
await walk(path.join(absolute, entry.name), relative);
|
|
73
|
+
}
|
|
74
|
+
else if (entry.isFile()) {
|
|
75
|
+
files.push(relative);
|
|
76
|
+
const match = relative.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
|
77
|
+
if (match)
|
|
78
|
+
skills.push({ name: match[1], skillPath: relative });
|
|
79
|
+
}
|
|
80
|
+
// Anything that is neither a directory nor a regular file (a symlink somehow already present,
|
|
81
|
+
// a device node) is skipped rather than indexed — extraction itself never creates one, so
|
|
82
|
+
// encountering one here would mean the filesystem changed out from under an already-published,
|
|
83
|
+
// supposedly-frozen package, which this function has no business acting on either way.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await walk(packageRoot, "");
|
|
87
|
+
return {
|
|
88
|
+
pluginId: parsed.manifest.name,
|
|
89
|
+
id: parsed.manifest.id,
|
|
90
|
+
provides: parsed.manifest.provides,
|
|
91
|
+
version: parsed.manifest.version,
|
|
92
|
+
...( /**
|
|
93
|
+
* `plugin.json`'s own `description`/`keywords`/`author`/`license` (`manifest.ts`'s
|
|
94
|
+
* `AgentPluginManifest`) — parsed at index time but, before 2026-09-09, discarded immediately
|
|
95
|
+
* after: only `name`/`version` ever reached `InstalledAgentPlugin`. Added for
|
|
96
|
+
* `search_agent_plugin_local` (`tool-registrations.ts`), which needs the description and the
|
|
97
|
+
* author-curated keywords to rank installed plugins against a free-text query — the manifest is
|
|
98
|
+
* already read and parsed here on every call, so carrying these through is zero additional I/O,
|
|
99
|
+
* not a new read path. All four are optional because the spec itself makes them optional (the real
|
|
100
|
+
* installed `ui-ux-design` fixture ships `description` but no `keywords`; a bare-minimum manifest
|
|
101
|
+
* ships neither) — a caller must not assume any of them are present.
|
|
102
|
+
*/parsed.manifest.description !== undefined ? { description: parsed.manifest.description } : {}),
|
|
103
|
+
...(parsed.manifest.keywords !== undefined ? { keywords: parsed.manifest.keywords } : {}),
|
|
104
|
+
...(parsed.manifest.author !== undefined ? { author: parsed.manifest.author } : {}),
|
|
105
|
+
...(parsed.manifest.license !== undefined ? { license: parsed.manifest.license } : {}),
|
|
106
|
+
...readDisplayText({ manifest: parsed.manifest, key: "displayName", maxLength: 64 }),
|
|
107
|
+
...readDisplayText({ manifest: parsed.manifest, key: "summary", maxLength: 1200 }),
|
|
108
|
+
archiveDigest,
|
|
109
|
+
packageRoot,
|
|
110
|
+
files: files.sort(),
|
|
111
|
+
skills: skills.sort((a, b) => a.name.localeCompare(b.name)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { indexInstalledRoot };
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=installed-package-index.js.map
|
|
@@ -30,6 +30,7 @@ export type ParseAgentPluginManifestResult = {
|
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
33
|
+
* Hosts inject identityNamespace; the default retains the existing plugin extension format.
|
|
33
34
|
*
|
|
34
35
|
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
35
36
|
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|