@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +201 -0
  3. package/README.md +261 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.js +2 -0
  6. package/dist/lifecycle/activation.d.ts +177 -0
  7. package/dist/lifecycle/activation.js +874 -0
  8. package/dist/lifecycle/bundled-digests.d.ts +27 -0
  9. package/dist/lifecycle/bundled-digests.js +149 -0
  10. package/dist/lifecycle/bundled-source-archive.d.ts +13 -0
  11. package/dist/lifecycle/bundled-source-archive.js +96 -0
  12. package/dist/lifecycle/capability-projection.d.ts +11 -0
  13. package/dist/lifecycle/capability-projection.js +60 -0
  14. package/dist/lifecycle/fetch-archive.d.ts +88 -0
  15. package/dist/lifecycle/fetch-archive.js +215 -0
  16. package/dist/lifecycle/index.d.ts +174 -0
  17. package/dist/lifecycle/index.js +94 -0
  18. package/dist/lifecycle/install-from-url.d.ts +78 -0
  19. package/dist/lifecycle/install-from-url.js +73 -0
  20. package/dist/lifecycle/install.d.ts +52 -0
  21. package/dist/lifecycle/install.js +286 -0
  22. package/dist/lifecycle/layout.d.ts +20 -0
  23. package/dist/lifecycle/layout.js +30 -0
  24. package/dist/lifecycle/manifest.d.ts +214 -0
  25. package/dist/lifecycle/manifest.js +409 -0
  26. package/dist/lifecycle/mcp-provisioning.d.ts +20 -0
  27. package/dist/lifecycle/mcp-provisioning.js +14 -0
  28. package/dist/lifecycle/node.d.ts +11 -0
  29. package/dist/lifecycle/node.js +34 -0
  30. package/dist/lifecycle/package-paths.d.ts +12 -0
  31. package/dist/lifecycle/package-paths.js +74 -0
  32. package/dist/lifecycle/ports.d.ts +81 -0
  33. package/dist/lifecycle/ports.js +2 -0
  34. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +18 -0
  35. package/dist/lifecycle/resolve-agent-plugin-refs.js +143 -0
  36. package/dist/lifecycle/retire-bundled.d.ts +36 -0
  37. package/dist/lifecycle/retire-bundled.js +69 -0
  38. package/dist/lifecycle/search.d.ts +23 -0
  39. package/dist/lifecycle/search.js +50 -0
  40. package/dist/lifecycle/seed-bundled.d.ts +29 -0
  41. package/dist/lifecycle/seed-bundled.js +118 -0
  42. package/dist/lifecycle/set-enabled.d.ts +21 -0
  43. package/dist/lifecycle/set-enabled.js +39 -0
  44. package/dist/lifecycle/trusted-plugin-files.d.ts +29 -0
  45. package/dist/lifecycle/trusted-plugin-files.js +82 -0
  46. package/dist/lifecycle/types.d.ts +20 -0
  47. package/dist/lifecycle/types.js +2 -0
  48. package/dist/lifecycle/uninstall.d.ts +41 -0
  49. package/dist/lifecycle/uninstall.js +148 -0
  50. package/dist/lifecycle/yauzl-archive-reader.d.ts +62 -0
  51. package/dist/lifecycle/yauzl-archive-reader.js +73 -0
  52. package/dist/manifest.d.ts +68 -0
  53. package/dist/manifest.js +38 -0
  54. package/package.json +92 -0
  55. package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  56. package/ui-ux-design/mcp.json +4 -0
  57. package/ui-ux-design/plugin.json +5 -0
  58. package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
  59. package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
  60. package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
  61. package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
  62. package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
  63. package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
  64. package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
  65. package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
  66. package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
  67. package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
  68. package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
  69. package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
  70. package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
  71. package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
  72. package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
  73. package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
  74. package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
  75. package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
  76. package/ui-ux-design/skills/shadcn-ui/SKILL.md +326 -0
  77. package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  78. package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
  79. package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  80. package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
  81. package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
  82. package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
  83. package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
  84. package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  85. package/ui-ux-design/skills/ui-ux-design/SKILL.md +77 -0
  86. package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +72 -0
  87. package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +107 -0
  88. package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +65 -0
  89. package/ui-ux-design/skills/ui-ux-design/references/foundations.md +144 -0
  90. package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
  91. package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +102 -0
  92. package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
  93. package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +63 -0
  94. package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +78 -0
  95. package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +93 -0
  96. package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +123 -0
  97. package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +57 -0
  98. package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
  99. package/ui-ux-design/skills/web-compliance/SKILL.md +129 -0
  100. 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