@jini-ai/agent-plugins 0.4.0
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 +57 -0
- package/LICENSE +201 -0
- package/README.md +261 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lifecycle/activation.d.ts +177 -0
- package/dist/lifecycle/activation.js +874 -0
- package/dist/lifecycle/bundled-digests.d.ts +27 -0
- package/dist/lifecycle/bundled-digests.js +149 -0
- package/dist/lifecycle/bundled-source-archive.d.ts +13 -0
- package/dist/lifecycle/bundled-source-archive.js +96 -0
- package/dist/lifecycle/capability-projection.d.ts +11 -0
- package/dist/lifecycle/capability-projection.js +60 -0
- package/dist/lifecycle/fetch-archive.d.ts +88 -0
- package/dist/lifecycle/fetch-archive.js +215 -0
- package/dist/lifecycle/index.d.ts +174 -0
- package/dist/lifecycle/index.js +94 -0
- package/dist/lifecycle/install-from-url.d.ts +78 -0
- package/dist/lifecycle/install-from-url.js +73 -0
- package/dist/lifecycle/install.d.ts +52 -0
- package/dist/lifecycle/install.js +286 -0
- package/dist/lifecycle/layout.d.ts +20 -0
- package/dist/lifecycle/layout.js +30 -0
- package/dist/lifecycle/manifest.d.ts +214 -0
- package/dist/lifecycle/manifest.js +409 -0
- package/dist/lifecycle/mcp-provisioning.d.ts +20 -0
- package/dist/lifecycle/mcp-provisioning.js +14 -0
- package/dist/lifecycle/node.d.ts +11 -0
- package/dist/lifecycle/node.js +34 -0
- package/dist/lifecycle/package-paths.d.ts +12 -0
- package/dist/lifecycle/package-paths.js +74 -0
- package/dist/lifecycle/ports.d.ts +81 -0
- package/dist/lifecycle/ports.js +2 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +18 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.js +143 -0
- package/dist/lifecycle/retire-bundled.d.ts +36 -0
- package/dist/lifecycle/retire-bundled.js +69 -0
- package/dist/lifecycle/search.d.ts +23 -0
- package/dist/lifecycle/search.js +50 -0
- package/dist/lifecycle/seed-bundled.d.ts +29 -0
- package/dist/lifecycle/seed-bundled.js +118 -0
- package/dist/lifecycle/set-enabled.d.ts +21 -0
- package/dist/lifecycle/set-enabled.js +39 -0
- package/dist/lifecycle/trusted-plugin-files.d.ts +29 -0
- package/dist/lifecycle/trusted-plugin-files.js +82 -0
- package/dist/lifecycle/types.d.ts +20 -0
- package/dist/lifecycle/types.js +2 -0
- package/dist/lifecycle/uninstall.d.ts +41 -0
- package/dist/lifecycle/uninstall.js +148 -0
- package/dist/lifecycle/yauzl-archive-reader.d.ts +62 -0
- package/dist/lifecycle/yauzl-archive-reader.js +73 -0
- package/dist/manifest.d.ts +68 -0
- package/dist/manifest.js +38 -0
- package/package.json +92 -0
- package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/ui-ux-design/mcp.json +4 -0
- package/ui-ux-design/plugin.json +5 -0
- package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
- package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
- package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
- package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
- package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
- package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
- package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
- package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
- package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
- package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
- package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
- package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
- package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
- package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
- package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
- package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
- package/ui-ux-design/skills/shadcn-ui/SKILL.md +326 -0
- package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/ui-ux-design/skills/ui-ux-design/SKILL.md +77 -0
- package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +72 -0
- package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +107 -0
- package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +65 -0
- package/ui-ux-design/skills/ui-ux-design/references/foundations.md +144 -0
- package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
- package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +102 -0
- package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
- package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +63 -0
- package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +78 -0
- package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +93 -0
- package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +123 -0
- package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +57 -0
- package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
- package/ui-ux-design/skills/web-compliance/SKILL.md +129 -0
- package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseAgentPluginManifest as parseAgentPluginManifestValue } from "./manifest.js";
|
|
6
|
+
import { PackagePathViolation } from "./package-paths.js";
|
|
7
|
+
import { createPackagePathsModule } from "./package-paths.js";
|
|
8
|
+
export class AgentPluginInstallError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor({ code, message }, options = {}) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "AgentPluginInstallError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function buildModule(ports) {
|
|
17
|
+
const chmod = ports.filesystem.chmod.bind(ports.filesystem);
|
|
18
|
+
const mkdir = ports.filesystem.mkdir.bind(ports.filesystem);
|
|
19
|
+
const mkdtemp = ports.filesystem.mkdtemp.bind(ports.filesystem);
|
|
20
|
+
const open = ports.filesystem.open.bind(ports.filesystem);
|
|
21
|
+
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
22
|
+
const readFile = ports.filesystem.readFile.bind(ports.filesystem);
|
|
23
|
+
const rename = ports.filesystem.rename.bind(ports.filesystem);
|
|
24
|
+
const rm = ports.filesystem.rm.bind(ports.filesystem);
|
|
25
|
+
const stat = ports.filesystem.stat.bind(ports.filesystem);
|
|
26
|
+
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value });
|
|
27
|
+
const { assertContainedOnDisk, normalizePackageEntryPath } = createPackagePathsModule(ports);
|
|
28
|
+
const LIMITS = {
|
|
29
|
+
maxArchiveBytes: 32 * 1024 * 1024,
|
|
30
|
+
maxEntries: 4096,
|
|
31
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
32
|
+
maxTotalExtractedBytes: 64 * 1024 * 1024,
|
|
33
|
+
};
|
|
34
|
+
function maxAgentPluginInstallArchiveBytes() {
|
|
35
|
+
return LIMITS.maxArchiveBytes;
|
|
36
|
+
}
|
|
37
|
+
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
38
|
+
async function installAgentPlugin(required, _optional = {}) {
|
|
39
|
+
const { archive, expectedSha256, archiveReader, layout, workspaceId } = required;
|
|
40
|
+
const workspaceLayout = layout.forWorkspace({ workspaceId });
|
|
41
|
+
if (archive.byteLength > LIMITS.maxArchiveBytes) {
|
|
42
|
+
throw new AgentPluginInstallError({ code: "ARCHIVE_TOO_LARGE", message: `archive is ${archive.byteLength} bytes, over the ${LIMITS.maxArchiveBytes}-byte cap` });
|
|
43
|
+
}
|
|
44
|
+
const digest = sha256(archive);
|
|
45
|
+
if (!SHA256_HEX_PATTERN.test(expectedSha256) || digest !== expectedSha256.toLowerCase()) {
|
|
46
|
+
throw new AgentPluginInstallError({ code: "DIGEST_MISMATCH", message: `archive SHA-256 '${digest}' does not match the expected '${expectedSha256}' — refusing to extract unverified bytes` });
|
|
47
|
+
}
|
|
48
|
+
await mkdir(workspaceLayout.packages, { recursive: true, mode: 0o700 });
|
|
49
|
+
const finalRoot = path.join(workspaceLayout.packages, digest);
|
|
50
|
+
const alreadyPublished = await isRealDirectory(finalRoot);
|
|
51
|
+
if (alreadyPublished) {
|
|
52
|
+
return indexInstalledRoot(finalRoot, digest);
|
|
53
|
+
}
|
|
54
|
+
await mkdir(workspaceLayout.staging, { recursive: true, mode: 0o700 });
|
|
55
|
+
await assertEntryCountWithinCap(archiveReader.entries({ archive }));
|
|
56
|
+
const transactionRoot = await mkdtemp(path.join(workspaceLayout.staging, "install-"));
|
|
57
|
+
const extractionRoot = path.join(transactionRoot, "root");
|
|
58
|
+
await mkdir(extractionRoot, { mode: 0o700 });
|
|
59
|
+
try {
|
|
60
|
+
const executablePaths = await extractEntries(archiveReader.entries({ archive }), extractionRoot);
|
|
61
|
+
const indexed = await indexInstalledRoot(extractionRoot, digest);
|
|
62
|
+
await publish(extractionRoot, finalRoot);
|
|
63
|
+
await freezeTree(finalRoot, executablePaths);
|
|
64
|
+
return { ...indexed, packageRoot: finalRoot };
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(transactionRoot, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function symlinkRejectionMessage(entry) {
|
|
71
|
+
const targetSuffix = "linkTarget" in entry && entry.linkTarget ? ` (target '${entry.linkTarget}')` : "";
|
|
72
|
+
return `archive entry '${entry.entryPath}' has kind '${entry.kind}', which Agent Plugin packages are not permitted to contain${targetSuffix}`;
|
|
73
|
+
}
|
|
74
|
+
function normalizeEntryPathOrThrow(entryPath) {
|
|
75
|
+
try {
|
|
76
|
+
return normalizePackageEntryPath(entryPath);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new AgentPluginInstallError({ code: "UNSAFE_ENTRY_PATH", message: `archive entry path is unsafe: '${entryPath}'` }, { cause: error });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function resolveContainedDestinationOrThrow(extractionRoot, normalized) {
|
|
83
|
+
try {
|
|
84
|
+
return await assertContainedOnDisk(extractionRoot, normalized);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new AgentPluginInstallError({ code: "UNSAFE_ENTRY_PATH", message: `archive entry resolves outside the package root: '${normalized}'` }, {
|
|
88
|
+
cause: error instanceof PackagePathViolation ? error : undefined,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function extractOneEntry(args) {
|
|
93
|
+
const { entry, extractionRoot, seen, executablePaths, totalBytesSoFar } = args;
|
|
94
|
+
if (entry.kind !== "file" && entry.kind !== "directory") {
|
|
95
|
+
throw new AgentPluginInstallError({ code: "SYMLINK_ENTRY_REJECTED", message: symlinkRejectionMessage(entry) });
|
|
96
|
+
}
|
|
97
|
+
const normalized = normalizeEntryPathOrThrow(entry.entryPath);
|
|
98
|
+
if (seen.has(normalized)) {
|
|
99
|
+
throw new AgentPluginInstallError({ code: "DUPLICATE_ENTRY", message: `duplicate archive entry '${normalized}' — refusing a second write over an already-vetted first one` });
|
|
100
|
+
}
|
|
101
|
+
seen.add(normalized);
|
|
102
|
+
const destination = await resolveContainedDestinationOrThrow(extractionRoot, normalized);
|
|
103
|
+
if (entry.kind === "directory") {
|
|
104
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
105
|
+
return totalBytesSoFar;
|
|
106
|
+
}
|
|
107
|
+
if ((entry.declaredSize ?? 0) > LIMITS.maxFileBytes) {
|
|
108
|
+
throw new AgentPluginInstallError({ code: "FILE_TOO_LARGE", message: `archive file '${normalized}' declares ${entry.declaredSize} bytes, over the ${LIMITS.maxFileBytes}-byte cap` });
|
|
109
|
+
}
|
|
110
|
+
await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
111
|
+
const totalBytes = await writeContainedFile({
|
|
112
|
+
destination,
|
|
113
|
+
normalized,
|
|
114
|
+
stream: entry.openReadStream({}),
|
|
115
|
+
totalBytesSoFar,
|
|
116
|
+
});
|
|
117
|
+
if (entry.executable)
|
|
118
|
+
executablePaths.add(normalized);
|
|
119
|
+
return totalBytes;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Refuses a "million small files" archive from entry metadata alone, before a staging
|
|
123
|
+
* transaction exists. Checking only inside the extraction loop meant an over-cap archive first
|
|
124
|
+
* had the cap's worth of files contained, written and then deleted again. Every reader derives
|
|
125
|
+
* its entries from the archive bytes, so a second iteration costs one central-directory walk
|
|
126
|
+
* and opens no content stream.
|
|
127
|
+
*/
|
|
128
|
+
async function assertEntryCountWithinCap(entries) {
|
|
129
|
+
let entryCount = 0;
|
|
130
|
+
for await (const _entry of entries) {
|
|
131
|
+
entryCount += 1;
|
|
132
|
+
if (entryCount > LIMITS.maxEntries)
|
|
133
|
+
throw tooManyEntriesError();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function tooManyEntriesError() {
|
|
137
|
+
return new AgentPluginInstallError({ code: "TOO_MANY_ENTRIES", message: `archive exceeds the ${LIMITS.maxEntries}-entry cap` });
|
|
138
|
+
}
|
|
139
|
+
async function extractEntries(entries, extractionRoot) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const executablePaths = new Set();
|
|
142
|
+
let entryCount = 0;
|
|
143
|
+
let totalBytes = 0;
|
|
144
|
+
for await (const entry of entries) {
|
|
145
|
+
entryCount += 1;
|
|
146
|
+
// Still enforced here: the pre-pass trusts the reader to yield the same entries twice.
|
|
147
|
+
if (entryCount > LIMITS.maxEntries)
|
|
148
|
+
throw tooManyEntriesError();
|
|
149
|
+
totalBytes = await extractOneEntry({ entry, extractionRoot, seen, executablePaths, totalBytesSoFar: totalBytes });
|
|
150
|
+
}
|
|
151
|
+
return executablePaths;
|
|
152
|
+
}
|
|
153
|
+
async function writeContainedFile(params) {
|
|
154
|
+
let flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL;
|
|
155
|
+
if (ports.process.platform !== "win32")
|
|
156
|
+
flags |= constants.O_NOFOLLOW;
|
|
157
|
+
const handle = await open(params.destination, flags, 0o600);
|
|
158
|
+
let fileBytes = 0;
|
|
159
|
+
let totalBytes = params.totalBytesSoFar;
|
|
160
|
+
try {
|
|
161
|
+
for await (const chunk of params.stream) {
|
|
162
|
+
fileBytes += chunk.byteLength;
|
|
163
|
+
totalBytes += chunk.byteLength;
|
|
164
|
+
if (fileBytes > LIMITS.maxFileBytes) {
|
|
165
|
+
throw new AgentPluginInstallError({ code: "DECOMPRESSION_BOMB", message: `archive file '${params.normalized}' exceeded the ${LIMITS.maxFileBytes}-byte cap while decompressing — its declared size was a lie` });
|
|
166
|
+
}
|
|
167
|
+
if (totalBytes > LIMITS.maxTotalExtractedBytes) {
|
|
168
|
+
throw new AgentPluginInstallError({ code: "TOTAL_SIZE_EXCEEDED", message: `archive exceeds the ${LIMITS.maxTotalExtractedBytes}-byte total extracted-size cap` });
|
|
169
|
+
}
|
|
170
|
+
await handle.write(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await handle.close();
|
|
175
|
+
}
|
|
176
|
+
return totalBytes;
|
|
177
|
+
}
|
|
178
|
+
async function indexInstalledRoot(packageRoot, archiveDigest) {
|
|
179
|
+
let manifestRaw;
|
|
180
|
+
try {
|
|
181
|
+
manifestRaw = await readFile(path.join(packageRoot, "plugin.json"), "utf8");
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
185
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_MISSING", message: "the archive does not contain a plugin.json at its root" });
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
let manifestValue;
|
|
190
|
+
try {
|
|
191
|
+
manifestValue = JSON.parse(manifestRaw);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: "plugin.json is not valid JSON" }, { cause: error });
|
|
195
|
+
}
|
|
196
|
+
const parsed = parseAgentPluginManifest(manifestValue);
|
|
197
|
+
if (!parsed.ok) {
|
|
198
|
+
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
|
|
199
|
+
}
|
|
200
|
+
const files = [];
|
|
201
|
+
const skills = [];
|
|
202
|
+
async function walk(absolute, prefix) {
|
|
203
|
+
for (const entry of await readdir(absolute, { withFileTypes: true })) {
|
|
204
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
await walk(path.join(absolute, entry.name), relative);
|
|
207
|
+
}
|
|
208
|
+
else if (entry.isFile()) {
|
|
209
|
+
files.push(relative);
|
|
210
|
+
const match = relative.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
|
211
|
+
if (match)
|
|
212
|
+
skills.push({ name: match[1], skillPath: relative });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
await walk(packageRoot, "");
|
|
217
|
+
return {
|
|
218
|
+
pluginId: parsed.manifest.name,
|
|
219
|
+
version: parsed.manifest.version,
|
|
220
|
+
...(parsed.manifest.description !== undefined ? { description: parsed.manifest.description } : {}),
|
|
221
|
+
...(parsed.manifest.keywords !== undefined ? { keywords: parsed.manifest.keywords } : {}),
|
|
222
|
+
...(parsed.manifest.author !== undefined ? { author: parsed.manifest.author } : {}),
|
|
223
|
+
...(parsed.manifest.license !== undefined ? { license: parsed.manifest.license } : {}),
|
|
224
|
+
archiveDigest,
|
|
225
|
+
packageRoot,
|
|
226
|
+
files: files.sort(),
|
|
227
|
+
skills: skills.sort((a, b) => a.name.localeCompare(b.name)),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function freezeTree(root, executables, prefix = "") {
|
|
231
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
232
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
233
|
+
const absolute = path.join(root, entry.name);
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
await freezeTree(absolute, executables, relative);
|
|
236
|
+
await chmod(absolute, 0o555);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
await chmod(absolute, executables.has(relative) ? 0o555 : 0o444);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await chmod(root, 0o555);
|
|
243
|
+
}
|
|
244
|
+
async function publish(extractionRoot, finalRoot) {
|
|
245
|
+
try {
|
|
246
|
+
await rename(extractionRoot, finalRoot);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (!isErrnoException(error) || (error.code !== "EEXIST" && error.code !== "ENOTEMPTY" && error.code !== "EACCES")) {
|
|
250
|
+
throw new AgentPluginInstallError({ code: "PUBLISH_FAILED", message: `failed to publish the extracted package to '${finalRoot}'` }, { cause: error });
|
|
251
|
+
}
|
|
252
|
+
if (!(await isRealDirectory(finalRoot))) {
|
|
253
|
+
throw new AgentPluginInstallError({ code: "PUBLISH_FAILED", message: `'${finalRoot}' exists but is not a real, published package directory` }, { cause: error });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function isRealDirectory(target) {
|
|
258
|
+
try {
|
|
259
|
+
const info = await stat(target);
|
|
260
|
+
return info.isDirectory();
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
264
|
+
return false;
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function sha256(value) {
|
|
269
|
+
return createHash("sha256").update(value).digest("hex");
|
|
270
|
+
}
|
|
271
|
+
function isErrnoException(error) {
|
|
272
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
273
|
+
}
|
|
274
|
+
return { maxAgentPluginInstallArchiveBytes, installAgentPlugin, indexInstalledRoot };
|
|
275
|
+
}
|
|
276
|
+
const instances = new WeakMap();
|
|
277
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
278
|
+
export function createInstallModule(ports) {
|
|
279
|
+
let instance = instances.get(ports);
|
|
280
|
+
if (!instance) {
|
|
281
|
+
instance = buildModule(ports);
|
|
282
|
+
instances.set(ports, instance);
|
|
283
|
+
}
|
|
284
|
+
return instance;
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AgentPluginWorkspaceLayout {
|
|
2
|
+
readonly root: string;
|
|
3
|
+
readonly packages: string;
|
|
4
|
+
readonly staging: string;
|
|
5
|
+
pluginDataDir(required: {
|
|
6
|
+
readonly pluginId: string;
|
|
7
|
+
}): string;
|
|
8
|
+
}
|
|
9
|
+
export interface AgentPluginLayoutPort {
|
|
10
|
+
readonly root: string;
|
|
11
|
+
forWorkspace(required: {
|
|
12
|
+
readonly workspaceId: string;
|
|
13
|
+
}): AgentPluginWorkspaceLayout;
|
|
14
|
+
}
|
|
15
|
+
export type AgentPluginLayout = AgentPluginLayoutPort;
|
|
16
|
+
/** Explicit root only; site-root and environment resolution belong to the consumer. */
|
|
17
|
+
export declare function createAgentPluginLayout(required: {
|
|
18
|
+
readonly root: string;
|
|
19
|
+
}): AgentPluginLayoutPort;
|
|
20
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/** Explicit root only; site-root and environment resolution belong to the consumer. */
|
|
3
|
+
export function createAgentPluginLayout(required) {
|
|
4
|
+
if (!path.isAbsolute(required.root))
|
|
5
|
+
throw new Error('Agent plugin root must be absolute');
|
|
6
|
+
const root = path.resolve(required.root);
|
|
7
|
+
return {
|
|
8
|
+
root,
|
|
9
|
+
forWorkspace({ workspaceId }) {
|
|
10
|
+
const id = workspaceId.toLowerCase();
|
|
11
|
+
assertSafeSegment(id);
|
|
12
|
+
const workspaceRoot = path.join(root, 'ws', id);
|
|
13
|
+
return {
|
|
14
|
+
root: workspaceRoot,
|
|
15
|
+
packages: path.join(workspaceRoot, 'packages', 'sha256'),
|
|
16
|
+
staging: path.join(workspaceRoot, 'staging'),
|
|
17
|
+
pluginDataDir({ pluginId }) {
|
|
18
|
+
assertSafeSegment(pluginId);
|
|
19
|
+
return path.join(workspaceRoot, 'data', pluginId);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function assertSafeSegment(value) {
|
|
26
|
+
if (!/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(value) || value.length > 64) {
|
|
27
|
+
throw new Error(`'${value}' is not a safe plugin/workspace id`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historical lifecycle rationale, carried with the implementation during consumer adoption.
|
|
3
|
+
* @file `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` — the Agent Plugins v1.0.0
|
|
4
|
+
* `plugin.json` / `mcp.json` grammar (agent-plugins.org/specification), verified against the live
|
|
5
|
+
* spec rather than inferred from an example manifest.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately a SEPARATE, smaller validator from `src/features/plugin-runtime/manifest.ts`'s
|
|
8
|
+
* `validateManifest()`. That file validates `.host-plugin`'s own tarball format — `integrity`
|
|
9
|
+
* (per-file SHA-256), `sdkRange`, `tier`, an exactly-three-token capability vocabulary, a single
|
|
10
|
+
* `content.entry.beforeSave` hook. This file validates the OPEN standard's `plugin.json`, which
|
|
11
|
+
* shares none of those fields and defines no execution/trust/capability model at all — see this
|
|
12
|
+
* feature's own header comment in `install.ts` for why the two formats cannot be unified.
|
|
13
|
+
*
|
|
14
|
+
* Spec facts this module encodes, quoted/paraphrased from agent-plugins.org/specification (verified
|
|
15
|
+
* 2026-08-12, not carried forward from an earlier round's inference):
|
|
16
|
+
* - `plugin.json`'s `name`: 1-64 chars, lowercase alphanumeric/hyphen/period only, must start and
|
|
17
|
+
* end alphanumeric, no `--` or `..` consecutive delimiters (§5.5). Valid: `my-plugin`,
|
|
18
|
+
* `acme.tools`, `lint3r`, `a`. Invalid: `My-Plugin`, `-start`, `has--double`.
|
|
19
|
+
* - `plugin.json`'s `$schema` MUST equal `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`
|
|
20
|
+
* for this pinned version; `mcp.json`'s MUST equal
|
|
21
|
+
* `https://agent-plugins.org/schemas/1.0.0/mcp.schema.json`.
|
|
22
|
+
* - Unknown top-level `plugin.json` fields produce warnings but do not block loading — this module
|
|
23
|
+
* returns them as `warnings`, never as a rejection reason.
|
|
24
|
+
* - `mcp.json`'s top-level shape is `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }` —
|
|
25
|
+
* `mcpServers` MUST be an object whose member names identify servers.
|
|
26
|
+
* - Each server entry MUST carry a `type` discriminator matching exactly one of three CLOSED
|
|
27
|
+
* variants (re-verified live against `agent-plugins.org/schemas/1.0.0/mcp.schema.json` and
|
|
28
|
+
* `/specification`, 2026-09-10 — superseding this file's earlier "not validated, no caller needs
|
|
29
|
+
* it" note, which is now false: `capability-projection.ts`'s MCP-server descriptors are no longer
|
|
30
|
+
* unconditionally `execute: { kind: "unavailable" }`, and launching a server needs its real
|
|
31
|
+
* transport config, not just its id):
|
|
32
|
+
* - `stdio`: requires `command` (non-empty string); optional `args` (string[]), `env`
|
|
33
|
+
* (string-valued object — MUST NOT set `PLUGIN_ROOT`/`PLUGIN_DATA`, which the spec reserves),
|
|
34
|
+
* `cwd` (plugin-relative path).
|
|
35
|
+
* - `streamable-http` / `sse`: requires `url` (non-empty string); optional `headers`
|
|
36
|
+
* (string-valued object).
|
|
37
|
+
* - **The spec defines NO auth-related field on a server entry at all.** Verbatim from
|
|
38
|
+
* `/specification`: "Agent Plugins v1 defines no OAuth configuration or portable
|
|
39
|
+
* credential-reference fields. Authorization discovery, user interaction, and credential storage
|
|
40
|
+
* are client-managed." A plugin therefore cannot declare "this server needs OAuth" using any
|
|
41
|
+
* spec-defined field — the host is the client the spec defers that decision to. This module accepts
|
|
42
|
+
* the host-specific metadata in `plugin.json`'s `extensions.<host-namespace>.mcpServers[serverId]`, keeping
|
|
43
|
+
* `mcp.json` transport entries closed per §7.2.1. Hosts may translate legacy auth and sign-in defaults in MCP
|
|
44
|
+
* entries through readServerMetadata for existing packages, but those cannot supply operator read trust.
|
|
45
|
+
* `authMode: "oauth" | "none"` selects client auth; absent defaults to "none".
|
|
46
|
+
* `defaultTools: { allow?, write?, read? }` names sign-in grants and reviewed read tools.
|
|
47
|
+
* `write` requires `allow` and must be its subset; `read` must be a subset when `allow` is
|
|
48
|
+
* present. A read-only declaration without `allow` leaves sign-in grants unchanged.
|
|
49
|
+
* Remote hints may veto a read declaration but never supply it. A third,
|
|
50
|
+
* `tokenAuth: { helpUrl, probeUrl }`, offers a pasted access token as the sign-in fallback
|
|
51
|
+
* (`access-token-tool.ts`). A fourth, `renamedTools: { oldName: newName }`, carries saved tool
|
|
52
|
+
* selections across a vendor's tool rename (`apply-tool-renames.ts`). A malformed per-server entry
|
|
53
|
+
* (wrong/missing `type`, missing required field) does not fail the whole file — see
|
|
54
|
+
* `ParseAgentPluginMcpConfigResult`'s own doc for the fail-open contract this preserves.
|
|
55
|
+
*
|
|
56
|
+
* Architectural role:
|
|
57
|
+
* Pure, no-I/O parsing over an already-`JSON.parse`d value — mirrors `plugin-runtime/manifest.ts`'s
|
|
58
|
+
* own "receives a JS value, never a file path or raw bytes" contract. `install.ts` is the only
|
|
59
|
+
* caller that reads the file and hands the parsed value here.
|
|
60
|
+
*/
|
|
61
|
+
export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
|
|
62
|
+
export type { PluginManifest, McpManifest, McpServerEntry } from '../manifest.js';
|
|
63
|
+
import type { PluginManifestAuthor } from '../manifest.js';
|
|
64
|
+
export type { PluginManifestAuthor } from '../manifest.js';
|
|
65
|
+
/** Agent Plugins 1.0.0 parsing, with application metadata confined to an explicit namespace. */
|
|
66
|
+
export type AgentPluginServerMetadataReader = (required: {
|
|
67
|
+
readonly serverId: string;
|
|
68
|
+
readonly value: Readonly<Record<string, unknown>>;
|
|
69
|
+
readonly server: Readonly<Record<string, unknown>>;
|
|
70
|
+
}) => Readonly<Record<string, unknown>>;
|
|
71
|
+
export interface AgentPluginManifest {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly version?: (string) | undefined;
|
|
74
|
+
readonly description?: (string) | undefined;
|
|
75
|
+
readonly author?: string | PluginManifestAuthor | undefined;
|
|
76
|
+
readonly license?: (string) | undefined;
|
|
77
|
+
readonly keywords?: (readonly string[]) | undefined;
|
|
78
|
+
readonly extensions?: (Readonly<Record<string, unknown>>) | undefined;
|
|
79
|
+
}
|
|
80
|
+
export type ParseAgentPluginManifestResult = {
|
|
81
|
+
readonly ok: true;
|
|
82
|
+
readonly manifest: AgentPluginManifest;
|
|
83
|
+
readonly warnings: readonly string[];
|
|
84
|
+
} | {
|
|
85
|
+
readonly ok: false;
|
|
86
|
+
readonly errors: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Validates an already-`JSON.parse`d `plugin.json` value against the v1.0.0 grammar.
|
|
90
|
+
*
|
|
91
|
+
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
92
|
+
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
93
|
+
* `plugin-runtime/discovery.ts`).
|
|
94
|
+
* @complexity O(k + n) time and space in the input keys and keyword entries.
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseAgentPluginManifest({ value }: {
|
|
97
|
+
readonly value: unknown;
|
|
98
|
+
}): ParseAgentPluginManifestResult;
|
|
99
|
+
/**
|
|
100
|
+
* The three closed transport variants `mcp.schema.json` v1.0.0 defines. See this file's header for
|
|
101
|
+
* the verified field shape of each.
|
|
102
|
+
*/
|
|
103
|
+
export declare const MCP_SERVER_TRANSPORTS: readonly ["stdio", "streamable-http", "sse"];
|
|
104
|
+
export type McpServerTransport = (typeof MCP_SERVER_TRANSPORTS)[number];
|
|
105
|
+
export interface StdioMcpServerConfig {
|
|
106
|
+
readonly type: "stdio";
|
|
107
|
+
readonly command: string;
|
|
108
|
+
readonly args?: (readonly string[]) | undefined;
|
|
109
|
+
readonly env?: (Readonly<Record<string, string>>) | undefined;
|
|
110
|
+
readonly cwd?: (string) | undefined;
|
|
111
|
+
}
|
|
112
|
+
export interface AgentPluginServerMetadata {
|
|
113
|
+
readonly authMode?: ('oauth' | 'none') | undefined;
|
|
114
|
+
readonly defaultTools?: (AgentPluginDefaultTools) | undefined;
|
|
115
|
+
readonly tokenAuth?: (AgentPluginTokenAuth) | undefined;
|
|
116
|
+
readonly renamedTools?: (Readonly<Record<string, string>>) | undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `streamable-http` and `sse` share an identical field shape (`url` + optional `headers`); only
|
|
120
|
+
* their `type` differs, and both are handled by the one remote branch below.
|
|
121
|
+
*/
|
|
122
|
+
export interface RemoteMcpServerConfig extends AgentPluginServerMetadata {
|
|
123
|
+
readonly type: 'streamable-http' | 'sse';
|
|
124
|
+
readonly url: string;
|
|
125
|
+
readonly headers?: (Readonly<Record<string, string>>) | undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* the host-specific extension, not part of the spec — see this file's header for why it exists and
|
|
129
|
+
* what a strictly spec-conformant client does with it (ignores it).
|
|
130
|
+
*
|
|
131
|
+
* the host-specific extension, not part of the spec: the tools this server's row is granted on the
|
|
132
|
+
* operator's FIRST successful sign-in (`apply-connect-defaults.ts`), so connecting is the only step.
|
|
133
|
+
* `write` is a subset of `allow`, the same rule the external-MCP store enforces on a save.
|
|
134
|
+
*
|
|
135
|
+
* the host-specific extension, not part of the spec: this server also takes a pasted access token, sent
|
|
136
|
+
* as `Authorization: Bearer` — the fallback when its sign-in cannot start (`access-token-tool.ts`).
|
|
137
|
+
*
|
|
138
|
+
* the host-specific extension, not part of the spec: `{ oldName: newName }` for tools the vendor
|
|
139
|
+
* renamed. A row this plugin provisioned has each old name in its saved lists replaced by the new
|
|
140
|
+
* one at boot (`apply-tool-renames.ts`), so a rename never strands an operator's selection.
|
|
141
|
+
*
|
|
142
|
+
* Where a human makes the token (`helpUrl`, linked from the form) and the URL a pasted token is
|
|
143
|
+
* checked against before it is saved (`probeUrl`: one GET, 401/403 means the token was rejected).
|
|
144
|
+
* Both https.
|
|
145
|
+
*
|
|
146
|
+
* `importFromEnv` names an env var whose token is copied onto this server's row at boot when the row
|
|
147
|
+
* has no credential yet (`import-access-token.ts`); `retiredEnv` names old env vars that are no
|
|
148
|
+
* longer read, so boot can say so once. Both let a plugin take over an old env-configured
|
|
149
|
+
* integration without core naming the vendor.
|
|
150
|
+
*/
|
|
151
|
+
export interface AgentPluginTokenAuth {
|
|
152
|
+
readonly helpUrl: string;
|
|
153
|
+
readonly probeUrl: string;
|
|
154
|
+
readonly importFromEnv?: (string) | undefined;
|
|
155
|
+
readonly retiredEnv?: (readonly string[]) | undefined;
|
|
156
|
+
}
|
|
157
|
+
export interface AgentPluginDefaultTools {
|
|
158
|
+
readonly allow?: (readonly string[]) | undefined;
|
|
159
|
+
readonly write: readonly string[];
|
|
160
|
+
readonly read?: (readonly string[]) | undefined;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Absent means no first-sign-in defaults, rather than an empty grant.
|
|
164
|
+
*
|
|
165
|
+
* Operator-reviewed remote names; absent on older programmatic configs means no read grants.
|
|
166
|
+
*/
|
|
167
|
+
export type McpServerConfig = StdioMcpServerConfig | RemoteMcpServerConfig;
|
|
168
|
+
export interface AgentPluginMcpConfig {
|
|
169
|
+
readonly serverIds: readonly string[];
|
|
170
|
+
readonly servers: Readonly<Record<string, McpServerConfig>>;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `mcpServers`' own member names, in declaration order — the identity a future admission record
|
|
174
|
+
* pins to (`serverId` in the debate's own admission shape). Includes every declared key
|
|
175
|
+
* regardless of whether that entry's transport shape validated — see {@link servers} for the
|
|
176
|
+
* validated subset — so a discovery caller that only ever needed ids (`search_agent_plugin_local`)
|
|
177
|
+
* sees no behavior change from before this module parsed transports at all.
|
|
178
|
+
*
|
|
179
|
+
* Only the entries whose transport shape validated against one of the three closed variants.
|
|
180
|
+
* A `serverId` present in {@link serverIds} but absent here declared an unrecognized `type` or was
|
|
181
|
+
* missing a required field for the `type` it declared — fail-open per this file's header, not an
|
|
182
|
+
* error for the whole `mcp.json`.
|
|
183
|
+
*/
|
|
184
|
+
export type ParseAgentPluginMcpConfigResult = {
|
|
185
|
+
readonly ok: true;
|
|
186
|
+
readonly config: AgentPluginMcpConfig;
|
|
187
|
+
} | {
|
|
188
|
+
readonly ok: false;
|
|
189
|
+
readonly errors: readonly string[];
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Validates an already-`JSON.parse`d `mcp.json` value against the v1.0.0 top-level shape:
|
|
193
|
+
* `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }`, then validates each declared
|
|
194
|
+
* server's own transport shape (see this file's header for the three variants).
|
|
195
|
+
*
|
|
196
|
+
* @throws Nothing — see {@link parseAgentPluginManifest}.
|
|
197
|
+
* @complexity O(s) in the number of declared servers.
|
|
198
|
+
*/
|
|
199
|
+
export declare function parseAgentPluginMcpConfig({ value, extensionNamespace }: {
|
|
200
|
+
readonly value: unknown;
|
|
201
|
+
readonly extensionNamespace: string;
|
|
202
|
+
}, optional?: {
|
|
203
|
+
readonly pluginManifest?: unknown;
|
|
204
|
+
readonly readServerMetadata?: AgentPluginServerMetadataReader | undefined;
|
|
205
|
+
}): ParseAgentPluginMcpConfigResult;
|
|
206
|
+
/** Typed extension reader: unknown namespaces have no semantics until a host supplies a reader. */
|
|
207
|
+
export declare function readAgentPluginExtension<T>(required: {
|
|
208
|
+
readonly manifest: Pick<AgentPluginManifest, 'extensions'>;
|
|
209
|
+
readonly namespace: string;
|
|
210
|
+
readonly read: (required: {
|
|
211
|
+
readonly value: Readonly<Record<string, unknown>>;
|
|
212
|
+
}) => T;
|
|
213
|
+
}): T | undefined;
|
|
214
|
+
//# sourceMappingURL=manifest.d.ts.map
|