@adea-ai/plugins 1.0.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/LICENSE +201 -0
- package/README.md +74 -0
- package/dist/agent-plugins-catalog.d.ts +26 -0
- package/dist/agent-plugins-catalog.js +699 -0
- package/dist/agent-plugins-catalog.js.map +1 -0
- package/dist/agent-plugins-harness.d.ts +54 -0
- package/dist/agent-plugins-harness.js +212 -0
- package/dist/agent-plugins-harness.js.map +1 -0
- package/dist/agent-plugins-schema.d.ts +402 -0
- package/dist/agent-plugins-schema.js +200 -0
- package/dist/agent-plugins-schema.js.map +1 -0
- package/dist/consumer-index.d.ts +217 -0
- package/dist/consumer-index.js +358 -0
- package/dist/consumer-index.js.map +1 -0
- package/dist/display-name.d.ts +8 -0
- package/dist/display-name.js +160 -0
- package/dist/display-name.js.map +1 -0
- package/dist/harness.d.ts +13 -0
- package/dist/harness.js +169 -0
- package/dist/harness.js.map +1 -0
- package/dist/icon-mirror.d.ts +69 -0
- package/dist/icon-mirror.js +147 -0
- package/dist/icon-mirror.js.map +1 -0
- package/dist/icons.d.ts +65 -0
- package/dist/icons.js +268 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +304 -0
- package/dist/index.js +1877 -0
- package/dist/index.js.map +1 -0
- package/dist/portable-catalog.d.ts +9 -0
- package/dist/portable-catalog.js +175 -0
- package/dist/portable-catalog.js.map +1 -0
- package/dist/publication.d.ts +98 -0
- package/dist/publication.js +121 -0
- package/dist/publication.js.map +1 -0
- package/dist/schema.d.ts +705 -0
- package/dist/schema.js +267 -0
- package/dist/schema.js.map +1 -0
- package/dist/site-icons.d.ts +55 -0
- package/dist/site-icons.js +155 -0
- package/dist/site-icons.js.map +1 -0
- package/dist/sources.d.ts +54 -0
- package/dist/sources.js +306 -0
- package/dist/sources.js.map +1 -0
- package/package.json +102 -0
- package/schemas/agent-package.v1.json +387 -0
- package/schemas/catalog.v1.schema.json +407 -0
- package/schemas/compatibility.v1.schema.json +23 -0
- package/schemas/harness-profile.v1.json +75 -0
- package/schemas/installation-plan.v2.json +695 -0
- package/schemas/sources-lock.v1.schema.json +61 -0
- package/src/agent-plugins-catalog.test.ts +402 -0
- package/src/agent-plugins-catalog.ts +800 -0
- package/src/agent-plugins-harness.test.ts +272 -0
- package/src/agent-plugins-harness.ts +281 -0
- package/src/agent-plugins-schema.test.ts +27 -0
- package/src/agent-plugins-schema.ts +219 -0
- package/src/artifacts.test.ts +545 -0
- package/src/catalog-installation.test.ts +42 -0
- package/src/consumer-index.test.ts +465 -0
- package/src/consumer-index.ts +592 -0
- package/src/display-name.test.ts +56 -0
- package/src/display-name.ts +157 -0
- package/src/harness.ts +202 -0
- package/src/icon-mirror.ts +196 -0
- package/src/icons.test.ts +185 -0
- package/src/icons.ts +297 -0
- package/src/index.test.ts +888 -0
- package/src/index.ts +2538 -0
- package/src/portable-catalog.test.ts +186 -0
- package/src/portable-catalog.ts +203 -0
- package/src/publication.test.ts +117 -0
- package/src/publication.ts +171 -0
- package/src/schema.ts +309 -0
- package/src/site-icon-fallback.test.ts +267 -0
- package/src/site-icons.test.ts +171 -0
- package/src/site-icons.ts +161 -0
- package/src/sources.ts +360 -0
- package/src/upstream-headers.test.ts +148 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
import { validateHeaderName, validateHeaderValue } from 'node:http';
|
|
5
|
+
import { parseDocument } from 'yaml';
|
|
6
|
+
import { AGENT_PLUGINS_VERSION, PLUGIN_SCHEMA_ID, MCP_SCHEMA_ID, NORMALIZER_VERSION, AgentManifestSchema, McpServerSchema, SkillFrontmatterSchema, AgentPackageSchema, } from './agent-plugins-schema.js';
|
|
7
|
+
export { NORMALIZER_VERSION };
|
|
8
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const manifestKeys = new Set([
|
|
11
|
+
'$schema',
|
|
12
|
+
'name',
|
|
13
|
+
'version',
|
|
14
|
+
'description',
|
|
15
|
+
'author',
|
|
16
|
+
'homepage',
|
|
17
|
+
'repository',
|
|
18
|
+
'license',
|
|
19
|
+
'keywords',
|
|
20
|
+
'extensions',
|
|
21
|
+
]);
|
|
22
|
+
const extensionName = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/;
|
|
23
|
+
const remoteTransportHeaders = new Set([
|
|
24
|
+
'authorization',
|
|
25
|
+
'connection',
|
|
26
|
+
'content-length',
|
|
27
|
+
'host',
|
|
28
|
+
'keep-alive',
|
|
29
|
+
'proxy-authorization',
|
|
30
|
+
'proxy-connection',
|
|
31
|
+
'te',
|
|
32
|
+
'trailer',
|
|
33
|
+
'transfer-encoding',
|
|
34
|
+
'upgrade',
|
|
35
|
+
]);
|
|
36
|
+
const own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
37
|
+
export const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
38
|
+
export function stableJson(value) {
|
|
39
|
+
if (value === null || typeof value !== 'object')
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
43
|
+
const record = value;
|
|
44
|
+
return `{${Object.keys(record)
|
|
45
|
+
.toSorted(compare)
|
|
46
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
47
|
+
.join(',')}}`;
|
|
48
|
+
}
|
|
49
|
+
export function sha256(value) {
|
|
50
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
/** Existing v1 source-byte identity, deliberately distinct from the derived package identity. */
|
|
53
|
+
export function sourceDigest(files) {
|
|
54
|
+
const hash = createHash('sha256');
|
|
55
|
+
for (const [path, bytes] of [...files].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
56
|
+
hash.update(`${path.length}:${path}:${bytes.byteLength}:`);
|
|
57
|
+
hash.update(bytes);
|
|
58
|
+
}
|
|
59
|
+
return `sha256:${hash.digest('hex')}`;
|
|
60
|
+
}
|
|
61
|
+
export function packageDigest(files) {
|
|
62
|
+
return sha256(stableJson({
|
|
63
|
+
algorithm: 'adea-package-files/1',
|
|
64
|
+
files: [...files]
|
|
65
|
+
.toSorted((a, b) => compare(a.targetPath, b.targetPath))
|
|
66
|
+
.map((file) => ({
|
|
67
|
+
path: file.targetPath,
|
|
68
|
+
digest: file.digest,
|
|
69
|
+
mode: file.action === 'copy' ? 'preserve-source' : file.mode,
|
|
70
|
+
})),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export function isRecord(value) {
|
|
74
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
/** JSON grammar plus duplicate-key detection, including escaped/nested keys. No reviver or code evaluation. */
|
|
77
|
+
export function parsePluginJson(text) {
|
|
78
|
+
const value = JSON.parse(text);
|
|
79
|
+
const document = parseDocument(text, { schema: 'json', uniqueKeys: true, strict: true });
|
|
80
|
+
if (document.errors.length > 0)
|
|
81
|
+
throw new Error('Invalid or duplicate JSON object key.');
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function hasControlCharacters(value, includeSpace = false) {
|
|
85
|
+
for (const character of value) {
|
|
86
|
+
const code = character.codePointAt(0);
|
|
87
|
+
if (code < (includeSpace ? 33 : 32) || code === 127)
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
export function assertPackagePath(path) {
|
|
93
|
+
if (!path ||
|
|
94
|
+
path.length > 1024 ||
|
|
95
|
+
/[\\:]/.test(path) ||
|
|
96
|
+
hasControlCharacters(path) ||
|
|
97
|
+
path.startsWith('/') ||
|
|
98
|
+
path
|
|
99
|
+
.split('/')
|
|
100
|
+
.some((part) => !part ||
|
|
101
|
+
part === '.' ||
|
|
102
|
+
part === '..' ||
|
|
103
|
+
/[. ]$/.test(part) ||
|
|
104
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
|
|
105
|
+
throw new Error('Unsafe package-relative path.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Lexical checks do not replace realpath containment checks at activation time. */
|
|
109
|
+
export function containedRelative(path) {
|
|
110
|
+
if (path.includes('\\') || hasControlCharacters(path))
|
|
111
|
+
throw new Error('Invalid path characters.');
|
|
112
|
+
const normalized = posix.normalize(path);
|
|
113
|
+
if (normalized.startsWith('/') ||
|
|
114
|
+
normalized === '..' ||
|
|
115
|
+
normalized.startsWith('../') ||
|
|
116
|
+
/^[A-Za-z]:/.test(normalized))
|
|
117
|
+
throw new Error('Path escapes its root.');
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
function checkCwd(cwd) {
|
|
121
|
+
const prefix = ['./', '${PLUGIN_ROOT}', '${PLUGIN_DATA}'].find((candidate) => candidate === './'
|
|
122
|
+
? cwd.startsWith(candidate)
|
|
123
|
+
: cwd === candidate || cwd.startsWith(`${candidate}/`));
|
|
124
|
+
if (!prefix)
|
|
125
|
+
throw new Error('cwd must be rooted in the plugin or plugin data directory.');
|
|
126
|
+
const suffix = prefix === './' ? cwd.slice(2) : cwd.slice(prefix.length).replace(/^\//, '');
|
|
127
|
+
if (/\$\{(?:PLUGIN_ROOT|PLUGIN_DATA)\}/.test(suffix))
|
|
128
|
+
throw new Error('cwd cannot combine filesystem roots.');
|
|
129
|
+
containedRelative(suffix || '.');
|
|
130
|
+
}
|
|
131
|
+
function isLoopback(hostname) {
|
|
132
|
+
const host = hostname.replace(/^\[|\]$/g, '');
|
|
133
|
+
return host === 'localhost' || (isIP(host) === 4 && host.startsWith('127.')) || host === '::1';
|
|
134
|
+
}
|
|
135
|
+
export function validateServer(input) {
|
|
136
|
+
const server = McpServerSchema.parse(input);
|
|
137
|
+
if (server.type === 'stdio') {
|
|
138
|
+
if (server.command.startsWith('./')) {
|
|
139
|
+
const path = containedRelative(server.command.slice(2));
|
|
140
|
+
if (path === '.' || path.includes('${'))
|
|
141
|
+
throw new Error('Invalid package executable.');
|
|
142
|
+
}
|
|
143
|
+
else if (!/^[^\s/\\:$]+$/.test(server.command) ||
|
|
144
|
+
hasControlCharacters(server.command) ||
|
|
145
|
+
server.command === '.' ||
|
|
146
|
+
server.command === '..') {
|
|
147
|
+
throw new Error('command must be one bare executable or ./package-relative path; no shell strings or expansion.');
|
|
148
|
+
}
|
|
149
|
+
for (const [key, value] of Object.entries(server.env ?? {})) {
|
|
150
|
+
if (/^(PLUGIN_ROOT|PLUGIN_DATA)$/i.test(key))
|
|
151
|
+
throw new Error('Reserved plugin environment variable.');
|
|
152
|
+
if (!key || key.includes('=') || key.includes('\0') || value.includes('\0'))
|
|
153
|
+
throw new Error('Invalid environment entry.');
|
|
154
|
+
}
|
|
155
|
+
if (server.args?.some((arg) => arg.includes('\0')))
|
|
156
|
+
throw new Error('Invalid argument.');
|
|
157
|
+
if (server.cwd !== undefined)
|
|
158
|
+
checkCwd(server.cwd);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const url = new URL(server.url);
|
|
162
|
+
if (!['http:', 'https:'].includes(url.protocol) ||
|
|
163
|
+
url.username ||
|
|
164
|
+
url.password ||
|
|
165
|
+
server.url.includes('#') ||
|
|
166
|
+
/^https?:\/\/[^/]*@/.test(server.url) ||
|
|
167
|
+
server.url.includes('\\') ||
|
|
168
|
+
hasControlCharacters(server.url, true) ||
|
|
169
|
+
(url.protocol !== 'https:' && !isLoopback(url.hostname)))
|
|
170
|
+
throw new Error('Remote MCP requires HTTPS, except HTTP loopback; credentials and fragments are forbidden.');
|
|
171
|
+
const keys = new Set();
|
|
172
|
+
for (const [key, value] of Object.entries(server.headers ?? {})) {
|
|
173
|
+
validateHeaderName(key);
|
|
174
|
+
validateHeaderValue(key, value);
|
|
175
|
+
const folded = key.toLowerCase();
|
|
176
|
+
if (remoteTransportHeaders.has(folded))
|
|
177
|
+
throw new Error('Protocol-owned or authorization HTTP header.');
|
|
178
|
+
if (keys.has(folded))
|
|
179
|
+
throw new Error('Duplicate case-insensitive HTTP header.');
|
|
180
|
+
keys.add(folded);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return server;
|
|
184
|
+
}
|
|
185
|
+
function trimPortableEdges(value) {
|
|
186
|
+
let start = 0;
|
|
187
|
+
let end = value.length;
|
|
188
|
+
while (start < end && (value[start] === '.' || value[start] === '-'))
|
|
189
|
+
start += 1;
|
|
190
|
+
while (end > start && (value[end - 1] === '.' || value[end - 1] === '-'))
|
|
191
|
+
end -= 1;
|
|
192
|
+
return value.slice(start, end);
|
|
193
|
+
}
|
|
194
|
+
function portableName(name) {
|
|
195
|
+
const cleaned = trimPortableEdges(name
|
|
196
|
+
.toLowerCase()
|
|
197
|
+
.replace(/[^a-z0-9.-]+/g, '-')
|
|
198
|
+
.replace(/-+/g, '-')
|
|
199
|
+
.replace(/\.+/g, '.')) || 'plugin';
|
|
200
|
+
return cleaned.length <= 64
|
|
201
|
+
? cleaned
|
|
202
|
+
: `${trimPortableEdges(cleaned.slice(0, 54))}-${sha256(name).slice(-8)}`;
|
|
203
|
+
}
|
|
204
|
+
function diagnostic(code, scope, path, message, severity = 'error') {
|
|
205
|
+
return { code, scope, path, severity, message };
|
|
206
|
+
}
|
|
207
|
+
function nativeManifest(raw, diagnostics) {
|
|
208
|
+
if (!isRecord(raw))
|
|
209
|
+
throw new Error('The root manifest must be an object.');
|
|
210
|
+
const filtered = Object.fromEntries(Object.entries(raw).filter(([key]) => manifestKeys.has(key)));
|
|
211
|
+
for (const key of Object.keys(raw)
|
|
212
|
+
.filter((field) => !manifestKeys.has(field))
|
|
213
|
+
.toSorted(compare))
|
|
214
|
+
diagnostics.push(diagnostic('UNKNOWN_MANIFEST_FIELD', 'plugin', `plugin.json/${key}`, 'Unknown core field ignored; it does not override fixed component locations.', 'warning'));
|
|
215
|
+
if (own(filtered, 'extensions')) {
|
|
216
|
+
if (!isRecord(filtered.extensions)) {
|
|
217
|
+
delete filtered.extensions;
|
|
218
|
+
diagnostics.push(diagnostic('INVALID_EXTENSIONS', 'extension', 'plugin.json/extensions', 'Non-object extensions field ignored.', 'warning'));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// No extension namespace is implemented by this portable compiler.
|
|
222
|
+
// Never recursively validate an unknown namespace's client-owned data.
|
|
223
|
+
const extensions = Object.fromEntries(Object.entries(filtered.extensions).filter(([key, value]) => {
|
|
224
|
+
if (extensionName.test(key) && isRecord(value))
|
|
225
|
+
return true;
|
|
226
|
+
diagnostics.push(diagnostic('IGNORED_EXTENSION', 'extension', `plugin.json/extensions/${key}`, 'Unimplemented extension data is opaque and was not projected into the portable manifest.', 'warning'));
|
|
227
|
+
return false;
|
|
228
|
+
}));
|
|
229
|
+
filtered.extensions = extensions;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return AgentManifestSchema.parse(filtered);
|
|
233
|
+
}
|
|
234
|
+
function legacyManifest(raw, name) {
|
|
235
|
+
const output = { $schema: PLUGIN_SCHEMA_ID, name: portableName(name) };
|
|
236
|
+
for (const key of ['version', 'description', 'homepage', 'repository', 'license'])
|
|
237
|
+
if (typeof raw[key] === 'string')
|
|
238
|
+
output[key] = raw[key];
|
|
239
|
+
if (typeof raw.author === 'string')
|
|
240
|
+
output.author = { name: raw.author };
|
|
241
|
+
else if (isRecord(raw.author))
|
|
242
|
+
output.author = Object.fromEntries(Object.entries(raw.author).filter(([key, value]) => ['name', 'email', 'url'].includes(key) && typeof value === 'string'));
|
|
243
|
+
if (Array.isArray(raw.keywords))
|
|
244
|
+
output.keywords = raw.keywords.filter((item) => typeof item === 'string');
|
|
245
|
+
return AgentManifestSchema.parse(output);
|
|
246
|
+
}
|
|
247
|
+
function replaceLegacyRoot(value) {
|
|
248
|
+
return value
|
|
249
|
+
.replaceAll('${CLAUDE_PLUGIN_ROOT}', '${PLUGIN_ROOT}')
|
|
250
|
+
.replaceAll('${CURSOR_PLUGIN_ROOT}', '${PLUGIN_ROOT}');
|
|
251
|
+
}
|
|
252
|
+
function translateLegacyServer(input) {
|
|
253
|
+
if (!isRecord(input))
|
|
254
|
+
throw new Error('Server must be an object.');
|
|
255
|
+
const out = { ...input };
|
|
256
|
+
if (out.type === 'http')
|
|
257
|
+
out.type = 'streamable-http';
|
|
258
|
+
if (out.type === undefined && typeof out.command === 'string')
|
|
259
|
+
out.type = 'stdio';
|
|
260
|
+
// A URL alone is ambiguous across legacy dialects. Never infer transport from a suffix.
|
|
261
|
+
if (out.type === undefined)
|
|
262
|
+
throw new Error('Legacy remote server must declare http, streamable-http, or sse explicitly.');
|
|
263
|
+
if (typeof out.command === 'string') {
|
|
264
|
+
const command = replaceLegacyRoot(out.command);
|
|
265
|
+
out.command = command.startsWith('${PLUGIN_ROOT}/')
|
|
266
|
+
? `./${command.slice('${PLUGIN_ROOT}/'.length)}`
|
|
267
|
+
: command;
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(out.args))
|
|
270
|
+
out.args = out.args.map((arg) => (typeof arg === 'string' ? replaceLegacyRoot(arg) : arg));
|
|
271
|
+
if (isRecord(out.env))
|
|
272
|
+
out.env = Object.fromEntries(Object.entries(out.env).map(([key, value]) => [
|
|
273
|
+
key,
|
|
274
|
+
typeof value === 'string' ? replaceLegacyRoot(value) : value,
|
|
275
|
+
]));
|
|
276
|
+
if (typeof out.cwd === 'string')
|
|
277
|
+
out.cwd = replaceLegacyRoot(out.cwd);
|
|
278
|
+
const strings = [];
|
|
279
|
+
const collect = (value) => {
|
|
280
|
+
if (typeof value === 'string')
|
|
281
|
+
strings.push(value);
|
|
282
|
+
else if (Array.isArray(value))
|
|
283
|
+
value.forEach(collect);
|
|
284
|
+
else if (isRecord(value))
|
|
285
|
+
Object.values(value).forEach(collect);
|
|
286
|
+
};
|
|
287
|
+
collect(out);
|
|
288
|
+
if (strings.some((value) => /\$\{(?!PLUGIN_ROOT\}|PLUGIN_DATA\})[^}]+\}/.test(value)))
|
|
289
|
+
throw new Error('Legacy environment/credential interpolation needs an explicit Control Plane binding; it is not portable substitution.');
|
|
290
|
+
if (out.type !== 'stdio' &&
|
|
291
|
+
strings.some((value) => /\$\{(?:PLUGIN_ROOT|PLUGIN_DATA)\}/.test(value)))
|
|
292
|
+
throw new Error('Remote MCP fields cannot use plugin path expansion.');
|
|
293
|
+
return validateServer(out);
|
|
294
|
+
}
|
|
295
|
+
/** Compile a package recipe only. No writes, subprocesses, network access or lifecycle scripts. */
|
|
296
|
+
export function compileAgentPackage(input) {
|
|
297
|
+
const diagnostics = [];
|
|
298
|
+
const nonPortable = [];
|
|
299
|
+
const skills = [];
|
|
300
|
+
const servers = new Map();
|
|
301
|
+
const omitted = new Set();
|
|
302
|
+
const files = input.files;
|
|
303
|
+
const originFormat = files.has('plugin.json') &&
|
|
304
|
+
(() => {
|
|
305
|
+
try {
|
|
306
|
+
const raw = parsePluginJson(decoder.decode(files.get('plugin.json')));
|
|
307
|
+
return isRecord(raw) && own(raw, '$schema');
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
})()
|
|
313
|
+
? 'agent-plugins'
|
|
314
|
+
: 'legacy';
|
|
315
|
+
const actualSourceDigest = sourceDigest(files);
|
|
316
|
+
const base = {
|
|
317
|
+
contractVersion: 1,
|
|
318
|
+
normalizerVersion: NORMALIZER_VERSION,
|
|
319
|
+
format: 'agent-plugins',
|
|
320
|
+
specVersion: AGENT_PLUGINS_VERSION,
|
|
321
|
+
originFormat,
|
|
322
|
+
sourceDigest: actualSourceDigest,
|
|
323
|
+
skills,
|
|
324
|
+
nonPortable,
|
|
325
|
+
diagnostics,
|
|
326
|
+
};
|
|
327
|
+
const unavailable = (message) => AgentPackageSchema.parse({
|
|
328
|
+
...base,
|
|
329
|
+
status: 'unavailable',
|
|
330
|
+
skills: [],
|
|
331
|
+
files: [],
|
|
332
|
+
mcpServers: {},
|
|
333
|
+
requirements: { skills: false, mcpTransports: [], executables: [], environmentReview: true },
|
|
334
|
+
diagnostics: [...diagnostics, diagnostic('INVALID_PLUGIN', 'plugin', 'plugin.json', message)],
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
if (input.sourceDigest && input.sourceDigest !== actualSourceDigest)
|
|
338
|
+
throw new Error('Source content digest mismatch.');
|
|
339
|
+
if (input.symlinks?.length)
|
|
340
|
+
throw new Error('Symlinks are excluded by marketplace policy.');
|
|
341
|
+
const paths = new Set();
|
|
342
|
+
const spellings = new Map();
|
|
343
|
+
let total = 0;
|
|
344
|
+
if (files.size > 4096)
|
|
345
|
+
throw new Error('Package exceeds file-count policy.');
|
|
346
|
+
for (const [path, bytes] of files) {
|
|
347
|
+
assertPackagePath(path);
|
|
348
|
+
const folded = path.normalize('NFC').toLowerCase();
|
|
349
|
+
if (paths.has(folded))
|
|
350
|
+
throw new Error('Case/Unicode-normalization path collision.');
|
|
351
|
+
paths.add(folded);
|
|
352
|
+
for (let component = path; component !== '.'; component = posix.dirname(component)) {
|
|
353
|
+
const key = component.normalize('NFC').toLowerCase();
|
|
354
|
+
if (spellings.has(key) && spellings.get(key) !== component)
|
|
355
|
+
throw new Error('Case/Unicode-normalization directory collision.');
|
|
356
|
+
spellings.set(key, component);
|
|
357
|
+
}
|
|
358
|
+
total += bytes.byteLength;
|
|
359
|
+
if (bytes.byteLength > 5 * 1024 * 1024 || total > 50 * 1024 * 1024)
|
|
360
|
+
throw new Error('Package exceeds byte-size policy.');
|
|
361
|
+
for (let parent = posix.dirname(path); parent !== '.'; parent = posix.dirname(parent))
|
|
362
|
+
if (files.has(parent))
|
|
363
|
+
throw new Error('File/directory path collision.');
|
|
364
|
+
}
|
|
365
|
+
for (const path of paths)
|
|
366
|
+
for (let parent = posix.dirname(path); parent !== '.'; parent = posix.dirname(parent))
|
|
367
|
+
if (paths.has(parent))
|
|
368
|
+
throw new Error('File/directory path collision.');
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return unavailable(error instanceof Error ? error.message : 'Invalid snapshot.');
|
|
372
|
+
}
|
|
373
|
+
let manifest;
|
|
374
|
+
let rawManifest = {};
|
|
375
|
+
let manifestSourcePath = 'plugin.json';
|
|
376
|
+
try {
|
|
377
|
+
if (originFormat === 'agent-plugins') {
|
|
378
|
+
const raw = parsePluginJson(decoder.decode(files.get('plugin.json')));
|
|
379
|
+
manifest = nativeManifest(raw, diagnostics);
|
|
380
|
+
rawManifest = isRecord(raw) ? raw : {};
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const manifestPath = [
|
|
384
|
+
'plugin.json',
|
|
385
|
+
'.claude-plugin/plugin.json',
|
|
386
|
+
'.cursor-plugin/plugin.json',
|
|
387
|
+
'.codex-plugin/plugin.json',
|
|
388
|
+
'.agents/plugin.json',
|
|
389
|
+
].find((path) => files.has(path));
|
|
390
|
+
if (manifestPath) {
|
|
391
|
+
manifestSourcePath = manifestPath;
|
|
392
|
+
const raw = parsePluginJson(decoder.decode(files.get(manifestPath)));
|
|
393
|
+
if (!isRecord(raw))
|
|
394
|
+
throw new Error('Legacy manifest is not an object.');
|
|
395
|
+
rawManifest = raw;
|
|
396
|
+
}
|
|
397
|
+
manifest = legacyManifest(rawManifest, input.name);
|
|
398
|
+
const declarations = Array.isArray(rawManifest.skills)
|
|
399
|
+
? rawManifest.skills
|
|
400
|
+
: [rawManifest.skills];
|
|
401
|
+
if (rawManifest.skills !== undefined &&
|
|
402
|
+
!declarations.every((path) => path === './skills' || path === './skills/'))
|
|
403
|
+
nonPortable.push({
|
|
404
|
+
kind: 'legacy-skill-discovery',
|
|
405
|
+
paths: [manifestSourcePath],
|
|
406
|
+
reason: 'Custom skill discovery declarations need review. Only fixed skills/<name>/SKILL.md directories are projected.',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return unavailable('Root manifest is malformed, has invalid required metadata, or declares an unsupported schema. No legacy fallback is attempted.');
|
|
412
|
+
}
|
|
413
|
+
if (files.has('skills')) {
|
|
414
|
+
omitted.add('skills');
|
|
415
|
+
diagnostics.push(diagnostic('INVALID_SKILLS_LOCATION', 'skills', 'skills', 'skills must be a directory.'));
|
|
416
|
+
}
|
|
417
|
+
for (const [path, bytes] of [...files].toSorted(([a], [b]) => compare(a, b))) {
|
|
418
|
+
const match = /^skills\/([^/]+)\/SKILL\.md$/.exec(path);
|
|
419
|
+
if (!match)
|
|
420
|
+
continue;
|
|
421
|
+
try {
|
|
422
|
+
const text = decoder.decode(bytes);
|
|
423
|
+
const frontmatter = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text)?.[1];
|
|
424
|
+
if (frontmatter === undefined)
|
|
425
|
+
throw new Error('Missing YAML frontmatter.');
|
|
426
|
+
const document = parseDocument(frontmatter, { uniqueKeys: true, strict: true });
|
|
427
|
+
if (document.errors.length || document.warnings.length)
|
|
428
|
+
throw new Error('Invalid or unsupported YAML.');
|
|
429
|
+
const skill = SkillFrontmatterSchema.parse(document.toJS({ maxAliasCount: 100 }));
|
|
430
|
+
if (skill.name !== match[1])
|
|
431
|
+
throw new Error('Skill name must match its directory.');
|
|
432
|
+
skills.push({
|
|
433
|
+
name: skill.name,
|
|
434
|
+
path: posix.dirname(path),
|
|
435
|
+
description: skill.description,
|
|
436
|
+
...(skill.compatibility ? { compatibility: skill.compatibility } : {}),
|
|
437
|
+
...(skill['allowed-tools'] ? { allowedTools: skill['allowed-tools'] } : {}),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
omitted.add(path);
|
|
442
|
+
diagnostics.push(diagnostic('INVALID_SKILL', 'skill', path, 'Invalid Agent Skills frontmatter; this skill is excluded without disabling other components.'));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Deeper SKILL.md files are resources, never additional discovered skills.
|
|
446
|
+
let mcpEntries = {};
|
|
447
|
+
let mcpPath = 'mcp.json';
|
|
448
|
+
try {
|
|
449
|
+
if (originFormat === 'agent-plugins') {
|
|
450
|
+
if ([...files.keys()].some((path) => path.startsWith('mcp.json/')))
|
|
451
|
+
throw new Error('mcp.json is not a file.');
|
|
452
|
+
if (files.has('mcp.json')) {
|
|
453
|
+
const root = parsePluginJson(decoder.decode(files.get('mcp.json')));
|
|
454
|
+
if (!isRecord(root) ||
|
|
455
|
+
root.$schema !== MCP_SCHEMA_ID ||
|
|
456
|
+
!isRecord(root.mcpServers) ||
|
|
457
|
+
Object.keys(root).some((key) => !['$schema', 'mcpServers'].includes(key)))
|
|
458
|
+
throw new Error('Invalid MCP root/schema.');
|
|
459
|
+
mcpEntries = root.mcpServers;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
const candidates = ['mcp.json', '.mcp.json'].filter((path) => files.has(path));
|
|
464
|
+
const declared = rawManifest.mcpServers ?? rawManifest.mcp;
|
|
465
|
+
if (typeof declared === 'string') {
|
|
466
|
+
const path = declared.replace(/^\.\//, '');
|
|
467
|
+
assertPackagePath(path);
|
|
468
|
+
if (!files.has(path))
|
|
469
|
+
throw new Error('Declared MCP file is missing.');
|
|
470
|
+
if (!candidates.includes(path))
|
|
471
|
+
candidates.push(path);
|
|
472
|
+
}
|
|
473
|
+
else if (declared !== undefined && !isRecord(declared))
|
|
474
|
+
throw new Error('Unsupported legacy MCP declaration.');
|
|
475
|
+
if (candidates.length + (isRecord(declared) ? 1 : 0) > 1)
|
|
476
|
+
throw new Error('Ambiguous legacy MCP sources require an explicit translation.');
|
|
477
|
+
if (candidates.length) {
|
|
478
|
+
mcpPath = candidates[0];
|
|
479
|
+
const root = parsePluginJson(decoder.decode(files.get(mcpPath)));
|
|
480
|
+
if (!isRecord(root))
|
|
481
|
+
throw new Error('Invalid legacy MCP document.');
|
|
482
|
+
mcpEntries = isRecord(root.mcpServers) ? root.mcpServers : root;
|
|
483
|
+
}
|
|
484
|
+
else if (isRecord(declared))
|
|
485
|
+
mcpEntries = declared;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
diagnostics.push(diagnostic('INVALID_MCP_CONFIGURATION', 'mcp', mcpPath, 'MCP configuration/schema/location is invalid or ambiguous; other component types remain available.'));
|
|
490
|
+
mcpEntries = {};
|
|
491
|
+
}
|
|
492
|
+
for (const [name, inputServer] of Object.entries(mcpEntries).toSorted(([a], [b]) => compare(a, b))) {
|
|
493
|
+
try {
|
|
494
|
+
if (['__proto__', 'constructor', 'prototype'].includes(name) || hasControlCharacters(name))
|
|
495
|
+
throw new Error('Unsafe server identifier under marketplace policy.');
|
|
496
|
+
const server = originFormat === 'agent-plugins'
|
|
497
|
+
? validateServer(inputServer)
|
|
498
|
+
: translateLegacyServer(inputServer);
|
|
499
|
+
if (server.type === 'stdio' &&
|
|
500
|
+
server.command.startsWith('./') &&
|
|
501
|
+
!files.has(containedRelative(server.command.slice(2))))
|
|
502
|
+
throw new Error('Bundled executable is missing.');
|
|
503
|
+
servers.set(name, server);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
diagnostics.push(diagnostic('INVALID_MCP_SERVER', 'server', `${mcpPath}#${name}`, 'Server is invalid or needs an explicit legacy transport/credential/path translation. It is excluded; no transport or authorization behavior is guessed.'));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const extras = new Map();
|
|
510
|
+
for (const path of files.keys()) {
|
|
511
|
+
const top = path.split('/')[0];
|
|
512
|
+
const lower = path.toLowerCase();
|
|
513
|
+
const kind = [
|
|
514
|
+
'commands',
|
|
515
|
+
'agents',
|
|
516
|
+
'hooks',
|
|
517
|
+
'rules',
|
|
518
|
+
'subagents',
|
|
519
|
+
'schedules',
|
|
520
|
+
'cron',
|
|
521
|
+
'ui',
|
|
522
|
+
'components',
|
|
523
|
+
].includes(top)
|
|
524
|
+
? top
|
|
525
|
+
: ['claude.md', 'agents.md'].includes(lower) || lower.startsWith('.cursor/rules/')
|
|
526
|
+
? 'rules'
|
|
527
|
+
: lower === 'hooks.json'
|
|
528
|
+
? 'hooks'
|
|
529
|
+
: ['.claude', '.cursor', '.agents'].includes(top) ||
|
|
530
|
+
(extensionName.test(top) && path.includes('/'))
|
|
531
|
+
? 'client-extension'
|
|
532
|
+
: undefined;
|
|
533
|
+
if (kind)
|
|
534
|
+
extras.set(kind, [...(extras.get(kind) ?? []), path]);
|
|
535
|
+
}
|
|
536
|
+
for (const key of ['commands', 'agents', 'hooks', 'rules'])
|
|
537
|
+
if (rawManifest[key] !== undefined)
|
|
538
|
+
extras.set(key, [...(extras.get(key) ?? []), manifestSourcePath]);
|
|
539
|
+
for (const key of Object.keys(manifest.extensions ?? {}))
|
|
540
|
+
extras.set('client-extension', [
|
|
541
|
+
...(extras.get('client-extension') ?? []),
|
|
542
|
+
`plugin.json/extensions/${key}`,
|
|
543
|
+
]);
|
|
544
|
+
for (const [kind, paths] of [...extras].toSorted(([a], [b]) => compare(a, b)))
|
|
545
|
+
nonPortable.push({
|
|
546
|
+
kind,
|
|
547
|
+
paths: [...new Set(paths)].toSorted(compare),
|
|
548
|
+
reason: 'Preserved as source content, not activated or presented as portable Agent Plugins behavior.',
|
|
549
|
+
});
|
|
550
|
+
const operations = [];
|
|
551
|
+
for (const [path, bytes] of files) {
|
|
552
|
+
if (path === 'plugin.json' ||
|
|
553
|
+
path === 'mcp.json' ||
|
|
554
|
+
path.startsWith('plugin.json/') ||
|
|
555
|
+
path.startsWith('mcp.json/') ||
|
|
556
|
+
omitted.has(path))
|
|
557
|
+
continue;
|
|
558
|
+
operations.push({
|
|
559
|
+
action: 'copy',
|
|
560
|
+
sourcePath: path,
|
|
561
|
+
targetPath: path,
|
|
562
|
+
digest: sha256(bytes),
|
|
563
|
+
preserveMode: true,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const mcpServers = Object.fromEntries(servers);
|
|
567
|
+
for (const [targetPath, data] of [
|
|
568
|
+
['plugin.json', manifest],
|
|
569
|
+
['mcp.json', { $schema: MCP_SCHEMA_ID, mcpServers }],
|
|
570
|
+
]) {
|
|
571
|
+
const content = stableJson(data) + '\n';
|
|
572
|
+
operations.push({ action: 'write', targetPath, content, digest: sha256(content), mode: '0644' });
|
|
573
|
+
}
|
|
574
|
+
const sortedOperations = operations.toSorted((a, b) => compare(a.targetPath, b.targetPath));
|
|
575
|
+
const values = [...servers.values()];
|
|
576
|
+
const local = values.filter((server) => server.type === 'stdio');
|
|
577
|
+
const sortedDiagnostics = diagnostics.toSorted((a, b) => compare(`${a.path}:${a.code}`, `${b.path}:${b.code}`));
|
|
578
|
+
try {
|
|
579
|
+
return verifyAgentPackage({
|
|
580
|
+
...base,
|
|
581
|
+
diagnostics: sortedDiagnostics,
|
|
582
|
+
manifest,
|
|
583
|
+
status: sortedDiagnostics.some((item) => item.severity === 'error') || nonPortable.length
|
|
584
|
+
? 'partial'
|
|
585
|
+
: 'portable',
|
|
586
|
+
packageDigest: packageDigest(sortedOperations),
|
|
587
|
+
files: sortedOperations,
|
|
588
|
+
mcpServers,
|
|
589
|
+
requirements: {
|
|
590
|
+
skills: skills.length > 0,
|
|
591
|
+
mcpTransports: [...new Set(values.map((server) => server.type))].toSorted(compare),
|
|
592
|
+
executables: [...new Set(local.map((server) => server.command))].toSorted(compare),
|
|
593
|
+
environmentReview: local.length > 0 ||
|
|
594
|
+
skills.some((skill) => !!skill.compatibility || !!skill.allowedTools) ||
|
|
595
|
+
[...files.keys()].some((path) => /(?:^|\/)scripts\//.test(path)),
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
return unavailable('Canonical output contains conflicting paths or inconsistent component metadata.');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
export function verifyAgentPackage(input) {
|
|
604
|
+
const pkg = AgentPackageSchema.parse(input);
|
|
605
|
+
if (pkg.status === 'unavailable') {
|
|
606
|
+
if (pkg.manifest ||
|
|
607
|
+
pkg.packageDigest ||
|
|
608
|
+
pkg.files.length ||
|
|
609
|
+
pkg.skills.length ||
|
|
610
|
+
Object.keys(pkg.mcpServers).length)
|
|
611
|
+
throw new Error('Unavailable package contains activatable content.');
|
|
612
|
+
return pkg;
|
|
613
|
+
}
|
|
614
|
+
if (!pkg.manifest || !pkg.packageDigest || packageDigest(pkg.files) !== pkg.packageDigest)
|
|
615
|
+
throw new Error('Canonical package digest/manifest mismatch.');
|
|
616
|
+
const paths = new Set();
|
|
617
|
+
for (const file of pkg.files) {
|
|
618
|
+
assertPackagePath(file.targetPath);
|
|
619
|
+
const key = file.targetPath.normalize('NFC').toLowerCase();
|
|
620
|
+
if (paths.has(key))
|
|
621
|
+
throw new Error('Duplicate package target.');
|
|
622
|
+
paths.add(key);
|
|
623
|
+
if (file.action === 'copy')
|
|
624
|
+
assertPackagePath(file.sourcePath);
|
|
625
|
+
else if (sha256(file.content) !== file.digest)
|
|
626
|
+
throw new Error('Generated file digest mismatch.');
|
|
627
|
+
}
|
|
628
|
+
for (const path of paths)
|
|
629
|
+
for (let parent = posix.dirname(path); parent !== '.'; parent = posix.dirname(parent))
|
|
630
|
+
if (paths.has(parent))
|
|
631
|
+
throw new Error('File/directory target collision.');
|
|
632
|
+
const generated = (path) => {
|
|
633
|
+
const file = pkg.files.find((entry) => entry.targetPath === path);
|
|
634
|
+
if (!file || file.action !== 'write')
|
|
635
|
+
throw new Error('Missing canonical control file.');
|
|
636
|
+
return parsePluginJson(file.content);
|
|
637
|
+
};
|
|
638
|
+
if (stableJson(generated('plugin.json')) !== stableJson(pkg.manifest))
|
|
639
|
+
throw new Error('Manifest descriptor differs from package bytes.');
|
|
640
|
+
if (stableJson(generated('mcp.json')) !==
|
|
641
|
+
stableJson({ $schema: MCP_SCHEMA_ID, mcpServers: pkg.mcpServers }))
|
|
642
|
+
throw new Error('MCP descriptor differs from package bytes.');
|
|
643
|
+
for (const server of Object.values(pkg.mcpServers))
|
|
644
|
+
validateServer(server);
|
|
645
|
+
const names = new Set();
|
|
646
|
+
for (const skill of pkg.skills) {
|
|
647
|
+
if (skill.path !== `skills/${skill.name}` ||
|
|
648
|
+
names.has(skill.name) ||
|
|
649
|
+
!paths.has(`${skill.path}/SKILL.md`.toLowerCase()))
|
|
650
|
+
throw new Error('Invalid skill directory binding.');
|
|
651
|
+
names.add(skill.name);
|
|
652
|
+
}
|
|
653
|
+
const discovered = pkg.files
|
|
654
|
+
.map((file) => /^skills\/([^/]+)\/SKILL\.md$/.exec(file.targetPath)?.[1])
|
|
655
|
+
.filter((name) => name !== undefined);
|
|
656
|
+
if (discovered.length !== names.size || discovered.some((name) => !names.has(name)))
|
|
657
|
+
throw new Error('Skill inventory differs from package discovery.');
|
|
658
|
+
const expectedTransports = [
|
|
659
|
+
...new Set(Object.values(pkg.mcpServers).map((server) => server.type)),
|
|
660
|
+
].toSorted(compare);
|
|
661
|
+
const expectedExecutables = [
|
|
662
|
+
...new Set(Object.values(pkg.mcpServers)
|
|
663
|
+
.filter((server) => server.type === 'stdio')
|
|
664
|
+
.map((server) => server.command)),
|
|
665
|
+
].toSorted(compare);
|
|
666
|
+
if (stableJson([...pkg.requirements.executables].toSorted(compare)) !==
|
|
667
|
+
stableJson(expectedExecutables))
|
|
668
|
+
throw new Error('Package executable requirements differ from components.');
|
|
669
|
+
if (pkg.requirements.skills !== pkg.skills.length > 0 ||
|
|
670
|
+
stableJson([...pkg.requirements.mcpTransports].toSorted(compare)) !==
|
|
671
|
+
stableJson(expectedTransports))
|
|
672
|
+
throw new Error('Package requirements differ from components.');
|
|
673
|
+
if (pkg.status === 'portable' &&
|
|
674
|
+
(pkg.nonPortable.length || pkg.diagnostics.some((item) => item.severity === 'error')))
|
|
675
|
+
throw new Error('Partial package is marked fully portable.');
|
|
676
|
+
return pkg;
|
|
677
|
+
}
|
|
678
|
+
/** Apply a verified recipe in memory. Filesystem installation/approval remains Control Plane-owned. */
|
|
679
|
+
export function materializeAgentPackage(input, source) {
|
|
680
|
+
const pkg = verifyAgentPackage(input);
|
|
681
|
+
if (pkg.status === 'unavailable' || sourceDigest(source) !== pkg.sourceDigest)
|
|
682
|
+
throw new Error('Unavailable package or source digest mismatch.');
|
|
683
|
+
const rebuilt = compileAgentPackage({
|
|
684
|
+
files: source,
|
|
685
|
+
name: pkg.manifest.name,
|
|
686
|
+
sourceDigest: pkg.sourceDigest,
|
|
687
|
+
});
|
|
688
|
+
if (stableJson(rebuilt) !== stableJson(pkg))
|
|
689
|
+
throw new Error('Recipe differs from deterministic source normalization.');
|
|
690
|
+
const result = new Map();
|
|
691
|
+
for (const file of pkg.files) {
|
|
692
|
+
const bytes = file.action === 'write' ? encoder.encode(file.content) : source.get(file.sourcePath);
|
|
693
|
+
if (!bytes || sha256(bytes) !== file.digest)
|
|
694
|
+
throw new Error('Package file missing or digest mismatch.');
|
|
695
|
+
result.set(file.targetPath, bytes.slice());
|
|
696
|
+
}
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=agent-plugins-catalog.js.map
|