@lesliechan721/aw-plugin-core 0.1.0-beta.6 → 0.1.0-beta.61
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/dist/aw-capabilities.d.ts +27 -0
- package/dist/aw-capabilities.js +117 -0
- package/dist/aw-capabilities.js.map +1 -0
- package/dist/catalog-file-modes.d.ts +5 -0
- package/dist/catalog-file-modes.js +67 -0
- package/dist/catalog-file-modes.js.map +1 -0
- package/dist/catalog-workspace-resolution.d.ts +3 -0
- package/dist/catalog-workspace-resolution.js +34 -0
- package/dist/catalog-workspace-resolution.js.map +1 -0
- package/dist/generated/plugin-manifest-v3.d.ts +1532 -0
- package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v3.js} +1 -1
- package/dist/generated/plugin-manifest-v3.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/managed-commands.d.ts +167 -0
- package/dist/managed-commands.js +463 -0
- package/dist/managed-commands.js.map +1 -0
- package/dist/manifest-contract.d.ts +3 -6
- package/dist/manifest-contract.js +271 -407
- package/dist/manifest-contract.js.map +1 -1
- package/dist/manifest-type-contract.d.ts +2 -2
- package/dist/manifest.d.ts +12 -8
- package/dist/manifest.js +18 -7
- package/dist/manifest.js.map +1 -1
- package/dist/native-runtime/reader-runtime.mjs +34 -0
- package/dist/native-runtime/reader.d.mts +6 -0
- package/dist/native-runtime/reader.mjs +11 -0
- package/dist/native-tools.d.ts +48 -0
- package/dist/native-tools.js +192 -0
- package/dist/native-tools.js.map +1 -0
- package/dist/plugin-authoring-capabilities.d.ts +1 -1
- package/dist/plugin-authoring-capabilities.js +133 -26
- package/dist/plugin-authoring-capabilities.js.map +1 -1
- package/dist/plugin-config.d.ts +47 -0
- package/dist/plugin-config.js +1355 -0
- package/dist/plugin-config.js.map +1 -0
- package/dist/plugin-requirements.d.ts +34 -0
- package/dist/plugin-requirements.js +300 -0
- package/dist/plugin-requirements.js.map +1 -0
- package/dist/plugin-script-contract.d.ts +11 -12
- package/dist/plugin-script-contract.js +44 -71
- package/dist/plugin-script-contract.js.map +1 -1
- package/dist/runtime-config.d.ts +38 -0
- package/dist/runtime-config.js +31 -0
- package/dist/runtime-config.js.map +1 -0
- package/dist/types.d.ts +266 -73
- package/dist/workspace.d.ts +37 -9
- package/dist/workspace.js +436 -195
- package/dist/workspace.js.map +1 -1
- package/package.json +16 -6
- package/schemas/plugin-manifest-v3.json +2655 -0
- package/dist/generated/plugin-manifest-v1.d.ts +0 -368
- package/dist/generated/plugin-manifest-v1.js.map +0 -1
- package/dist/plugin-release.d.ts +0 -30
- package/dist/plugin-release.js +0 -187
- package/dist/plugin-release.js.map +0 -1
- package/schemas/plugin-manifest-v1.json +0 -329
package/dist/workspace.js
CHANGED
|
@@ -2,9 +2,10 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
|
-
import {
|
|
5
|
+
import { catalogFileMode, listCatalogBundleFiles as listBundleFiles, readCatalogExecutableFiles, writeCatalogExecutableFiles, } from './catalog-file-modes.js';
|
|
6
|
+
import { assertCatalogVersion, assertStrictSemVer, compareStrictSemVer, parseStrictSemVer, MARKETPLACE_SOURCE_PATH, SOURCE_MANIFEST_PATH, createMarketplaceManifest, createMarketplaceManifestEntry, normalizeCatalogManifestBase, normalizeCatalogMarketplaceIdentity, normalizeCatalogManifestEntries, normalizeManifestDigestInput, readMarketplaceSourceMeta, readPluginManifest, validatePlatformManifestProjection, } from './manifest-contract.js';
|
|
6
7
|
import { resolveSupportedPlatforms } from './platform-support.js';
|
|
7
|
-
import {
|
|
8
|
+
import { createAwPluginRequirementsReceipt, normalizeAwCapabilities, parseAwPluginRequirementsReceipt, } from './aw-capabilities.js';
|
|
8
9
|
export const DEFAULT_CATALOG_WORKSPACE_VERSION = '0.1.0';
|
|
9
10
|
export const DEFAULT_CATALOG_OUTPUT_PATH = path.join('.aw', 'catalog');
|
|
10
11
|
export const DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH = path.join('.aw', 'publish', 'catalog');
|
|
@@ -12,6 +13,7 @@ export const DEFAULT_PLUGIN_OUTPUT_PATH = path.join('.aw', 'plugins');
|
|
|
12
13
|
export const CATALOG_MANIFEST_FILENAME = 'catalog-manifest.json';
|
|
13
14
|
export const GENERATED_OUTPUT_MARKER_FILENAME = '.aw-generated.json';
|
|
14
15
|
export const CATALOG_WORKSPACE_MARKER_FILENAME = '.aw-workspace.json';
|
|
16
|
+
const AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME = 'aw-plugin-requirements.json';
|
|
15
17
|
function isInside(root, candidate) {
|
|
16
18
|
const relative = path.relative(root, candidate);
|
|
17
19
|
return relative === '' || (relative !== '..'
|
|
@@ -138,9 +140,18 @@ function validateCatalogWorkspaceMarker(value) {
|
|
|
138
140
|
}
|
|
139
141
|
return { workspaceIdentity: marker.workspaceIdentity };
|
|
140
142
|
}
|
|
141
|
-
async function readCatalogWorkspaceMarker(workspaceRoot) {
|
|
143
|
+
export async function readCatalogWorkspaceMarker(workspaceRoot) {
|
|
142
144
|
const markerPath = path.join(workspaceRoot, CATALOG_WORKSPACE_MARKER_FILENAME);
|
|
143
|
-
|
|
145
|
+
let value;
|
|
146
|
+
try {
|
|
147
|
+
value = await readRegularJson(markerPath, 'Catalog Workspace marker');
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof SyntaxError)
|
|
151
|
+
throw new Error('Invalid Catalog Workspace marker.');
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
const marker = validateCatalogWorkspaceMarker(value);
|
|
144
155
|
return { markerPath, workspaceIdentity: marker.workspaceIdentity };
|
|
145
156
|
}
|
|
146
157
|
async function readRegularJson(filePath, label) {
|
|
@@ -398,29 +409,54 @@ function stableStringify(value) {
|
|
|
398
409
|
function sha256(value) {
|
|
399
410
|
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
400
411
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
.
|
|
423
|
-
|
|
412
|
+
const SOURCE_IDENTITY_EXCLUDED_DIRECTORIES = new Set([
|
|
413
|
+
'.git', '.aw', 'node_modules', 'evals', 'reports',
|
|
414
|
+
]);
|
|
415
|
+
const SOURCE_IDENTITY_EXCLUDED_FILES = new Set([
|
|
416
|
+
'.DS_Store', 'AGENTS.md', 'CLAUDE.md',
|
|
417
|
+
]);
|
|
418
|
+
async function sourceIdentityRecords(root, relative = '') {
|
|
419
|
+
const records = [];
|
|
420
|
+
for (const entry of (await fs.readdir(path.join(root, relative), { withFileTypes: true }))
|
|
421
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
422
|
+
if (SOURCE_IDENTITY_EXCLUDED_DIRECTORIES.has(entry.name) || SOURCE_IDENTITY_EXCLUDED_FILES.has(entry.name)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const child = relative ? path.posix.join(relative, entry.name) : entry.name;
|
|
426
|
+
const filePath = path.join(root, child);
|
|
427
|
+
const stat = await fs.lstat(filePath);
|
|
428
|
+
if (stat.isSymbolicLink())
|
|
429
|
+
throw new Error(`Catalog source identity does not accept symbolic links: ${filePath}`);
|
|
430
|
+
if (stat.isDirectory())
|
|
431
|
+
records.push(...await sourceIdentityRecords(root, child));
|
|
432
|
+
else if (stat.isFile()) {
|
|
433
|
+
const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
|
|
434
|
+
records.push(`${child}\0${mode}\0${sha256(await fs.readFile(filePath))}`);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
throw new Error(`Catalog source identity only accepts regular files and directories: ${filePath}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return records;
|
|
441
|
+
}
|
|
442
|
+
export async function calculateCatalogWorkspaceSourceIdentity(workspace) {
|
|
443
|
+
const roots = [workspace.metadataPath, ...workspace.plugins.map((plugin) => plugin.rootDir)];
|
|
444
|
+
const records = [];
|
|
445
|
+
for (const sourceRoot of roots.sort((left, right) => left.localeCompare(right))) {
|
|
446
|
+
const relativeRoot = path.relative(workspace.workspaceRoot, sourceRoot).split(path.sep).join('/');
|
|
447
|
+
const stat = await fs.lstat(sourceRoot);
|
|
448
|
+
if (stat.isSymbolicLink())
|
|
449
|
+
throw new Error(`Catalog source identity does not accept symbolic links: ${sourceRoot}`);
|
|
450
|
+
if (stat.isFile()) {
|
|
451
|
+
const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
|
|
452
|
+
records.push(`${relativeRoot}\0${mode}\0${sha256(await fs.readFile(sourceRoot))}`);
|
|
453
|
+
}
|
|
454
|
+
else if (stat.isDirectory()) {
|
|
455
|
+
for (const record of await sourceIdentityRecords(sourceRoot))
|
|
456
|
+
records.push(`${relativeRoot}/${record}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return sha256(records.sort().join('\n'));
|
|
424
460
|
}
|
|
425
461
|
function generatedOutputMarker(options) {
|
|
426
462
|
return {
|
|
@@ -431,46 +467,93 @@ function generatedOutputMarker(options) {
|
|
|
431
467
|
...(options.sourceName ? { sourceName: options.sourceName } : {}),
|
|
432
468
|
};
|
|
433
469
|
}
|
|
434
|
-
async function
|
|
435
|
-
const result = [];
|
|
436
|
-
for (const entry of await fs.readdir(path.join(root, relative), { withFileTypes: true })) {
|
|
437
|
-
const child = relative ? path.posix.join(relative, entry.name) : entry.name;
|
|
438
|
-
if (entry.isDirectory())
|
|
439
|
-
result.push(...await listBundleFiles(root, child));
|
|
440
|
-
else
|
|
441
|
-
result.push(child);
|
|
442
|
-
}
|
|
443
|
-
return result.sort();
|
|
444
|
-
}
|
|
445
|
-
async function catalogPathRecord(root, relative, versionNeutral) {
|
|
470
|
+
async function catalogPathRecords(root, relative, executableFiles) {
|
|
446
471
|
const filePath = path.join(root, ...relative.split('/'));
|
|
447
472
|
const stat = await fs.lstat(filePath);
|
|
448
473
|
if (stat.isSymbolicLink()) {
|
|
449
|
-
|
|
474
|
+
const record = { path: relative, kind: 'symlink', mode: '120000', digest: sha256(await fs.readlink(filePath)) };
|
|
475
|
+
return { artifact: record, content: record };
|
|
450
476
|
}
|
|
451
477
|
if (!stat.isFile())
|
|
452
478
|
throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
|
|
479
|
+
const bytes = await fs.readFile(filePath);
|
|
480
|
+
const artifact = {
|
|
481
|
+
path: relative,
|
|
482
|
+
kind: 'file',
|
|
483
|
+
mode: catalogFileMode(relative, stat.mode, executableFiles),
|
|
484
|
+
digest: sha256(bytes),
|
|
485
|
+
};
|
|
453
486
|
const manifests = new Set(['.agents-plugin/plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json']);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
487
|
+
let contentDigest = artifact.digest;
|
|
488
|
+
if (manifests.has(relative)) {
|
|
489
|
+
// Match readJson's UTF-8 BOM handling while reusing the original bytes.
|
|
490
|
+
const manifest = JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/u, ''));
|
|
491
|
+
contentDigest = sha256(stableStringify({
|
|
492
|
+
...manifest,
|
|
493
|
+
version: '<plugin-version>',
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
else if (path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME) {
|
|
497
|
+
const receipt = JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/u, ''));
|
|
498
|
+
const plugin = receipt.plugin && typeof receipt.plugin === 'object' && !Array.isArray(receipt.plugin)
|
|
499
|
+
? receipt.plugin
|
|
500
|
+
: {};
|
|
501
|
+
contentDigest = sha256(stableStringify({
|
|
502
|
+
...receipt,
|
|
503
|
+
plugin: { ...plugin, version: '<plugin-version>' },
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
return { artifact, content: contentDigest === artifact.digest ? artifact : { ...artifact, digest: contentDigest } };
|
|
458
507
|
}
|
|
459
|
-
async function
|
|
508
|
+
async function calculateEntryDigests(root) {
|
|
460
509
|
const files = await listBundleFiles(root);
|
|
461
|
-
|
|
510
|
+
const executableFiles = await readCatalogExecutableFiles(root, files);
|
|
511
|
+
const records = await Promise.all(files.map((file) => catalogPathRecords(root, file, executableFiles)));
|
|
512
|
+
return {
|
|
513
|
+
artifactDigest: sha256(stableStringify(records.map((record) => record.artifact))),
|
|
514
|
+
contentDigest: sha256(stableStringify(records.map((record) => record.content))),
|
|
515
|
+
};
|
|
462
516
|
}
|
|
463
517
|
async function expectedCatalogEntry(plugin, pluginRoot) {
|
|
518
|
+
const bundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} bundle manifest`);
|
|
519
|
+
const digests = await calculateEntryDigests(pluginRoot);
|
|
464
520
|
return {
|
|
465
521
|
...createMarketplaceManifestEntry(plugin),
|
|
466
522
|
version: plugin.manifest.version,
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
523
|
+
dependencies: [...(plugin.manifest.dependencies ?? [])].sort(),
|
|
524
|
+
requires: {
|
|
525
|
+
aw: {
|
|
526
|
+
capabilities: normalizeAwCapabilities(plugin.manifest.requires?.aw.capabilities ?? []),
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
artifactDigest: digests.artifactDigest,
|
|
530
|
+
manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
|
|
531
|
+
contentDigest: digests.contentDigest,
|
|
471
532
|
};
|
|
472
533
|
}
|
|
473
|
-
function
|
|
534
|
+
async function validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot) {
|
|
535
|
+
const files = await listBundleFiles(pluginRoot);
|
|
536
|
+
const expectedReceiptPaths = new Set(files.flatMap((relative) => /^(?:claude|codex|agent-skills)\/skills\/[^/]+\/SKILL\.md$/u.test(relative)
|
|
537
|
+
? [path.posix.join(path.posix.dirname(relative), AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)]
|
|
538
|
+
: []));
|
|
539
|
+
const actualReceiptPaths = files.filter((relative) => path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME);
|
|
540
|
+
for (const relative of expectedReceiptPaths) {
|
|
541
|
+
if (!actualReceiptPaths.includes(relative)) {
|
|
542
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} Skill root is missing ${AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME}: ${relative}.`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const expected = createAwPluginRequirementsReceipt({ name: plugin.manifest.name, version: plugin.manifest.version }, plugin.manifest.requires?.aw.capabilities ?? []);
|
|
546
|
+
for (const relative of actualReceiptPaths) {
|
|
547
|
+
if (!expectedReceiptPaths.has(relative)) {
|
|
548
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a reserved runtime requirements receipt outside a Skill root: ${relative}.`);
|
|
549
|
+
}
|
|
550
|
+
const receipt = parseAwPluginRequirementsReceipt(await readRegularJson(path.join(pluginRoot, ...relative.split('/')), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} runtime requirements receipt`));
|
|
551
|
+
if (!isDeepStrictEqual(receipt, expected)) {
|
|
552
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a mismatched runtime requirements receipt: ${relative}.`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function normalizeCatalogRelease(raw) {
|
|
474
557
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
475
558
|
throw new Error('Invalid catalog manifest release: expected an object.');
|
|
476
559
|
}
|
|
@@ -481,33 +564,16 @@ function normalizeCatalogRelease(raw, catalogVersion) {
|
|
|
481
564
|
}
|
|
482
565
|
return { state: 'development' };
|
|
483
566
|
}
|
|
484
|
-
const allowed = new Set(['state', '
|
|
567
|
+
const allowed = new Set(['state', 'sourceHead']);
|
|
485
568
|
if (release.state !== 'published' || Object.keys(release).some((key) => !allowed.has(key))) {
|
|
486
569
|
throw new Error('Invalid published catalog release.');
|
|
487
570
|
}
|
|
488
|
-
const targetVersion = assertStrictSemVer(release.targetVersion, 'catalog release targetVersion');
|
|
489
|
-
if (targetVersion.includes('-') || targetVersion.includes('+')) {
|
|
490
|
-
throw new Error('Invalid catalog release targetVersion: expected a stable version.');
|
|
491
|
-
}
|
|
492
|
-
if (targetVersion !== catalogVersion.replace(/-beta\.[1-9]\d*$/u, '')) {
|
|
493
|
-
throw new Error('Invalid catalog release targetVersion: must match catalogVersion.');
|
|
494
|
-
}
|
|
495
|
-
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
496
|
-
if (typeof release.planId !== 'string' || !digestPattern.test(release.planId)) {
|
|
497
|
-
throw new Error('Invalid catalog release planId.');
|
|
498
|
-
}
|
|
499
|
-
if (typeof release.contentDigest !== 'string' || !digestPattern.test(release.contentDigest)) {
|
|
500
|
-
throw new Error('Invalid catalog release contentDigest.');
|
|
501
|
-
}
|
|
502
571
|
if (typeof release.sourceHead !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(release.sourceHead)) {
|
|
503
572
|
throw new Error('Invalid catalog release sourceHead.');
|
|
504
573
|
}
|
|
505
574
|
return {
|
|
506
575
|
state: 'published',
|
|
507
|
-
targetVersion,
|
|
508
|
-
planId: release.planId,
|
|
509
576
|
sourceHead: release.sourceHead,
|
|
510
|
-
contentDigest: release.contentDigest,
|
|
511
577
|
};
|
|
512
578
|
}
|
|
513
579
|
export async function buildCatalogWorkspace(options) {
|
|
@@ -521,7 +587,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
521
587
|
const catalogVersion = catalogWorkspace.metadata.catalog.version;
|
|
522
588
|
const channel = deriveCatalogChannel(catalogVersion);
|
|
523
589
|
const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
|
|
524
|
-
const identity =
|
|
590
|
+
const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
|
|
525
591
|
const marker = generatedOutputMarker({
|
|
526
592
|
kind: 'catalog',
|
|
527
593
|
workspaceIdentity: catalogWorkspace.workspaceIdentity,
|
|
@@ -532,6 +598,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
532
598
|
force: options.force === true,
|
|
533
599
|
build: async (stagingRoot) => {
|
|
534
600
|
const bundledPlugins = [];
|
|
601
|
+
await fs.ensureDir(path.join(stagingRoot, 'plugins'));
|
|
535
602
|
for (const plugin of plugins) {
|
|
536
603
|
const closure = dependencyClosure(plugin, plugins);
|
|
537
604
|
const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
|
|
@@ -543,6 +610,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
543
610
|
dependencies: closure.slice(0, -1),
|
|
544
611
|
outputRoot,
|
|
545
612
|
});
|
|
613
|
+
await writeCatalogExecutableFiles(outputRoot);
|
|
546
614
|
bundledPlugins.push(await readPluginManifest(outputRoot, { mode: 'bundle' }));
|
|
547
615
|
}
|
|
548
616
|
const byPlatform = (platform) => bundledPlugins
|
|
@@ -550,20 +618,14 @@ export async function buildCatalogWorkspace(options) {
|
|
|
550
618
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
|
|
551
619
|
await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('codex')), { spaces: 2 });
|
|
552
620
|
await fs.outputJson(path.join(stagingRoot, '.claude-plugin', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('claude')), { spaces: 2 });
|
|
553
|
-
let minVersion = '0.0.0';
|
|
554
|
-
for (const plugin of bundledPlugins) {
|
|
555
|
-
const candidate = plugin.manifest.compatibility.aw.minVersion;
|
|
556
|
-
if (compareStrictSemVer(candidate, minVersion) > 0)
|
|
557
|
-
minVersion = candidate;
|
|
558
|
-
}
|
|
559
621
|
const entries = await Promise.all([...bundledPlugins]
|
|
560
622
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
561
623
|
.map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
|
|
562
624
|
const manifest = {
|
|
563
|
-
schemaVersion:
|
|
625
|
+
schemaVersion: 7,
|
|
564
626
|
catalogVersion,
|
|
565
627
|
channel,
|
|
566
|
-
|
|
628
|
+
requires: { aw: { capabilities: ['aw.catalog-snapshot.v7'] } },
|
|
567
629
|
marketplace: {
|
|
568
630
|
owner: catalogWorkspace.metadata.owner,
|
|
569
631
|
interface: catalogWorkspace.metadata.interface,
|
|
@@ -574,9 +636,9 @@ export async function buildCatalogWorkspace(options) {
|
|
|
574
636
|
await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
|
|
575
637
|
await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
|
|
576
638
|
},
|
|
577
|
-
validate: async (stagingRoot) => { await
|
|
639
|
+
validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
|
|
578
640
|
assertReplaceable: async (existingRoot) => {
|
|
579
|
-
await
|
|
641
|
+
await validateCatalogSnapshotComplete(existingRoot);
|
|
580
642
|
await assertGeneratedOutput(existingRoot, marker);
|
|
581
643
|
},
|
|
582
644
|
});
|
|
@@ -596,16 +658,132 @@ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
|
|
|
596
658
|
const manifest = await fs.readJson(manifestPath);
|
|
597
659
|
await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
|
|
598
660
|
}
|
|
661
|
+
const pluginRoot = path.join(snapshotRoot, 'plugins', pluginName);
|
|
662
|
+
for (const relative of await listBundleFiles(pluginRoot)) {
|
|
663
|
+
if (path.posix.basename(relative) !== AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)
|
|
664
|
+
continue;
|
|
665
|
+
const receiptPath = path.join(pluginRoot, ...relative.split('/'));
|
|
666
|
+
const receipt = parseAwPluginRequirementsReceipt(await fs.readJson(receiptPath));
|
|
667
|
+
if (receipt.plugin.name !== pluginName) {
|
|
668
|
+
throw new Error(`Catalog plugin ${JSON.stringify(pluginName)} has a mismatched runtime requirements receipt.`);
|
|
669
|
+
}
|
|
670
|
+
await fs.writeFile(receiptPath, `${JSON.stringify(createAwPluginRequirementsReceipt({ name: pluginName, version }, receipt.requiredCapabilities), null, 2)}\n`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function assertStablePluginVersion(version, fieldName) {
|
|
675
|
+
const parsed = parseStrictSemVer(version, fieldName);
|
|
676
|
+
if (parsed.prerelease.length > 0 || parsed.build.length > 0) {
|
|
677
|
+
throw new Error(`Invalid ${fieldName}: expected a stable SemVer without prerelease or build metadata.`);
|
|
678
|
+
}
|
|
679
|
+
return version;
|
|
680
|
+
}
|
|
681
|
+
function stableVersionBase(version, fieldName) {
|
|
682
|
+
const parsed = parseStrictSemVer(version, fieldName);
|
|
683
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Assign versions for planning and snapshot preparation with the same content comparison.
|
|
687
|
+
* Official history includes removed Plugins. Baseline declarations come from main source,
|
|
688
|
+
* before generated candidate metadata changes.
|
|
689
|
+
*/
|
|
690
|
+
export function resolvePublishedPluginVersions(options) {
|
|
691
|
+
const official = options.officialPluginHistory !== undefined;
|
|
692
|
+
const previousEntries = new Map();
|
|
693
|
+
const historicalContents = new Map();
|
|
694
|
+
for (const entry of [...options.previousPluginEntries ?? [], ...options.officialPluginHistory ?? []]) {
|
|
695
|
+
if (official) {
|
|
696
|
+
assertCatalogVersion(entry.version, `${entry.name} previous published version`);
|
|
697
|
+
const key = `${entry.name}@${entry.version}`;
|
|
698
|
+
const content = historicalContents.get(key);
|
|
699
|
+
if (content !== undefined && content !== entry.contentDigest) {
|
|
700
|
+
throw new Error(`Plugin ${entry.name} version ${entry.version} maps to different content in published history.`);
|
|
701
|
+
}
|
|
702
|
+
historicalContents.set(key, entry.contentDigest);
|
|
703
|
+
const previous = previousEntries.get(entry.name);
|
|
704
|
+
if (previous && compareStrictSemVer(previous.version, entry.version) >= 0)
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
previousEntries.set(entry.name, entry);
|
|
708
|
+
}
|
|
709
|
+
const versions = {};
|
|
710
|
+
for (const entry of options.entries) {
|
|
711
|
+
const { name, contentDigest } = entry;
|
|
712
|
+
if (!contentDigest)
|
|
713
|
+
throw new Error(`Missing release content digest for plugin ${JSON.stringify(name)}.`);
|
|
714
|
+
const sourceVersion = assertStablePluginVersion(entry.version, `${name} source version`);
|
|
715
|
+
const previous = previousEntries.get(name);
|
|
716
|
+
if (!previous) {
|
|
717
|
+
versions[name] = options.channel === 'beta' ? `${sourceVersion}-beta.1` : sourceVersion;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const previousVersion = assertStrictSemVer(previous.version, `${name} previous published version`);
|
|
721
|
+
const previousBase = stableVersionBase(previousVersion, `${name} previous published version`);
|
|
722
|
+
const baseComparison = compareStrictSemVer(sourceVersion, previousBase);
|
|
723
|
+
const parsedPrevious = parseStrictSemVer(previousVersion, `${name} previous published version`);
|
|
724
|
+
const baselineSourceVersion = options.sourcePluginVersionsAtBaseline?.[name];
|
|
725
|
+
if (official && baselineSourceVersion !== undefined
|
|
726
|
+
&& sourceVersion !== assertStablePluginVersion(baselineSourceVersion, `${name} baseline source version`)) {
|
|
727
|
+
if (baseComparison < 0) {
|
|
728
|
+
throw new Error(`Plugin ${name} explicit source version ${sourceVersion} must not be lower than published base ${previousBase}.`);
|
|
729
|
+
}
|
|
730
|
+
if (baseComparison === 0 && parsedPrevious.prerelease.length === 0) {
|
|
731
|
+
throw new Error(`Plugin ${name} explicit source version ${sourceVersion} is already published as stable. Select a higher base.`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (options.channel === 'latest') {
|
|
735
|
+
const comparison = compareStrictSemVer(sourceVersion, previousVersion);
|
|
736
|
+
if (!official && comparison < 0) {
|
|
737
|
+
throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous ${previousVersion}.`);
|
|
738
|
+
}
|
|
739
|
+
if (!official && comparison === 0 && previous.contentDigest !== contentDigest) {
|
|
740
|
+
throw new Error(`Plugin ${name} version ${sourceVersion} maps to different content. Increase the source plugin version.`);
|
|
741
|
+
}
|
|
742
|
+
let version = sourceVersion;
|
|
743
|
+
if (official && baseComparison <= 0) {
|
|
744
|
+
version = previousBase;
|
|
745
|
+
if (parsedPrevious.prerelease.length === 0 && previous.contentDigest !== contentDigest) {
|
|
746
|
+
version = `${parsedPrevious.major}.${parsedPrevious.minor}.${parsedPrevious.patch + 1}`;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
versions[name] = version;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (!official && baseComparison < 0) {
|
|
753
|
+
throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous base ${previousBase}.`);
|
|
754
|
+
}
|
|
755
|
+
if (baseComparison > 0) {
|
|
756
|
+
versions[name] = `${sourceVersion}-beta.1`;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (parsedPrevious.prerelease.length === 0) {
|
|
760
|
+
if (previous.contentDigest !== contentDigest) {
|
|
761
|
+
if (!official) {
|
|
762
|
+
throw new Error(`Plugin ${name} stable version ${sourceVersion} maps to different content. Increase the source plugin version.`);
|
|
763
|
+
}
|
|
764
|
+
versions[name] = `${parsedPrevious.major}.${parsedPrevious.minor}.${parsedPrevious.patch + 1}-beta.1`;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
versions[name] = previousVersion;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (parsedPrevious.prerelease.length !== 2
|
|
771
|
+
|| parsedPrevious.prerelease[0] !== 'beta'
|
|
772
|
+
|| !/^[1-9]\d*$/u.test(parsedPrevious.prerelease[1])) {
|
|
773
|
+
throw new Error(`Plugin ${name} previous version ${previousVersion} is not a stable or beta publication version.`);
|
|
774
|
+
}
|
|
775
|
+
versions[name] = previous.contentDigest === contentDigest
|
|
776
|
+
? previousVersion
|
|
777
|
+
: `${previousBase}-beta.${Number(parsedPrevious.prerelease[1]) + 1}`;
|
|
599
778
|
}
|
|
779
|
+
return versions;
|
|
600
780
|
}
|
|
601
781
|
export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
|
|
602
|
-
const validation = await
|
|
782
|
+
const validation = await validateCatalogSnapshotComplete(snapshotDirectory);
|
|
603
783
|
if (validation.manifest.release.state !== 'published') {
|
|
604
784
|
throw new Error('Published Catalog snapshot must have release.state published.');
|
|
605
785
|
}
|
|
606
|
-
|
|
607
|
-
throw new Error('Published Catalog snapshot must include a plugin release ledger.');
|
|
608
|
-
return { ...validation, ledger: validation.ledger };
|
|
786
|
+
return validation;
|
|
609
787
|
}
|
|
610
788
|
export async function preparePublishedCatalogWorkspace(options) {
|
|
611
789
|
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(options.sourceHead)) {
|
|
@@ -620,13 +798,12 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
620
798
|
const previous = options.previousSnapshotRoot
|
|
621
799
|
? await validatePublishedCatalogSnapshot(options.previousSnapshotRoot)
|
|
622
800
|
: null;
|
|
801
|
+
if (!previous && (options.historicalPluginEntries?.length ?? 0) > 0) {
|
|
802
|
+
throw new Error('Published Plugin history requires a previous Catalog snapshot.');
|
|
803
|
+
}
|
|
623
804
|
if (previous && compareStrictSemVer(catalogVersion, previous.manifest.catalogVersion) <= 0) {
|
|
624
805
|
throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.manifest.catalogVersion}.`);
|
|
625
806
|
}
|
|
626
|
-
const previousLedger = previous?.ledger ?? {
|
|
627
|
-
schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
|
|
628
|
-
records: [],
|
|
629
|
-
};
|
|
630
807
|
const temporaryRelative = path.join('.aw', 'publish', `.catalog-preparation-${crypto.randomUUID()}`);
|
|
631
808
|
const finalRelative = options.outputPath
|
|
632
809
|
?? `${DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH}-v${catalogVersion}`;
|
|
@@ -639,15 +816,14 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
639
816
|
reservedImports: options.reservedImports,
|
|
640
817
|
});
|
|
641
818
|
temporaryRoot = built.outputRoot;
|
|
642
|
-
const development = await
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
: Object.fromEntries(catalogWorkspace.plugins.map((plugin) => [plugin.manifest.name, plugin.manifest.version]));
|
|
819
|
+
const development = await validateCatalogSnapshotComplete(temporaryRoot);
|
|
820
|
+
const overrides = resolvePublishedPluginVersions({
|
|
821
|
+
channel,
|
|
822
|
+
entries: development.manifest.entries,
|
|
823
|
+
previousPluginEntries: [...options.historicalPluginEntries ?? [], ...previous?.manifest.entries ?? []],
|
|
824
|
+
officialPluginHistory: options.officialPluginHistory,
|
|
825
|
+
sourcePluginVersionsAtBaseline: options.sourcePluginVersionsAtBaseline,
|
|
826
|
+
});
|
|
651
827
|
await rewritePublishedPluginVersions(temporaryRoot, overrides);
|
|
652
828
|
const bundledPlugins = await Promise.all(catalogWorkspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
|
|
653
829
|
const entries = await Promise.all(bundledPlugins.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
@@ -658,26 +834,14 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
658
834
|
channel,
|
|
659
835
|
entries,
|
|
660
836
|
};
|
|
661
|
-
const contentDigest = publishedContentDigest(manifestWithoutRelease);
|
|
662
837
|
const manifest = {
|
|
663
838
|
...manifestWithoutRelease,
|
|
664
839
|
release: {
|
|
665
840
|
state: 'published',
|
|
666
|
-
targetVersion: catalogVersion.replace(/-beta\.[1-9]\d*$/u, ''),
|
|
667
|
-
planId: contentDigest,
|
|
668
841
|
sourceHead: options.sourceHead,
|
|
669
|
-
contentDigest,
|
|
670
842
|
},
|
|
671
843
|
};
|
|
672
|
-
|
|
673
|
-
previous: previousLedger,
|
|
674
|
-
manifest,
|
|
675
|
-
contentDigests,
|
|
676
|
-
});
|
|
677
|
-
await Promise.all([
|
|
678
|
-
fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`),
|
|
679
|
-
fs.writeFile(path.join(temporaryRoot, PLUGIN_RELEASE_LEDGER_FILENAME), `${JSON.stringify(ledger, null, 2)}\n`),
|
|
680
|
-
]);
|
|
844
|
+
await fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
681
845
|
const prepared = await validatePublishedCatalogSnapshot(temporaryRoot);
|
|
682
846
|
const resolved = await resolveCatalogWorkspaceOutputPath({
|
|
683
847
|
workspaceRoot: catalogWorkspace.workspaceRoot,
|
|
@@ -701,8 +865,6 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
701
865
|
channel,
|
|
702
866
|
pluginNames: built.pluginNames,
|
|
703
867
|
snapshotDigest: prepared.snapshotDigest,
|
|
704
|
-
contentDigest,
|
|
705
|
-
planId: contentDigest,
|
|
706
868
|
};
|
|
707
869
|
}
|
|
708
870
|
finally {
|
|
@@ -828,7 +990,7 @@ async function digestDirectory(root) {
|
|
|
828
990
|
await walk(root);
|
|
829
991
|
return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
|
|
830
992
|
}
|
|
831
|
-
|
|
993
|
+
async function validateCatalogSnapshotTransportInternal(snapshotDirectory) {
|
|
832
994
|
const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
|
|
833
995
|
const allowed = new Set([
|
|
834
996
|
CATALOG_MANIFEST_FILENAME,
|
|
@@ -836,7 +998,6 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
836
998
|
'.agents',
|
|
837
999
|
'.claude-plugin',
|
|
838
1000
|
'plugins',
|
|
839
|
-
'plugin-release-ledger.json',
|
|
840
1001
|
]);
|
|
841
1002
|
const entries = await fs.readdir(rootDir);
|
|
842
1003
|
const unexpected = entries.filter((entry) => !allowed.has(entry));
|
|
@@ -846,19 +1007,7 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
846
1007
|
throw new Error(`Invalid catalog snapshot root layout: missing [${missing.join(', ')}], unexpected [${unexpected.join(', ')}].`);
|
|
847
1008
|
}
|
|
848
1009
|
if (entries.includes(GENERATED_OUTPUT_MARKER_FILENAME)) {
|
|
849
|
-
|
|
850
|
-
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
851
|
-
if (!isDeepStrictEqual(Object.keys(marker).sort(), [
|
|
852
|
-
'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
|
|
853
|
-
])
|
|
854
|
-
|| marker.schemaVersion !== 1
|
|
855
|
-
|| marker.kind !== 'catalog'
|
|
856
|
-
|| typeof marker.workspaceIdentity !== 'string'
|
|
857
|
-
|| marker.workspaceIdentity.trim().length === 0
|
|
858
|
-
|| typeof marker.sourceIdentity !== 'string'
|
|
859
|
-
|| !digestPattern.test(marker.sourceIdentity)) {
|
|
860
|
-
throw new Error('Invalid catalog generated output marker.');
|
|
861
|
-
}
|
|
1010
|
+
validateCatalogGeneratedOutputMarker(await readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker'));
|
|
862
1011
|
}
|
|
863
1012
|
const assertLayout = async (directory, expected) => {
|
|
864
1013
|
const real = await assertDirectoryWithoutSymlinks(directory, 'Catalog snapshot directory');
|
|
@@ -876,16 +1025,118 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
876
1025
|
await readRegularJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'), 'Codex marketplace');
|
|
877
1026
|
await readRegularJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'), 'Claude marketplace');
|
|
878
1027
|
const snapshotDigest = await digestDirectory(rootDir);
|
|
879
|
-
const
|
|
1028
|
+
const manifest = normalizeCatalogManifestV7(await readRegularJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME), 'Catalog manifest v7'));
|
|
1029
|
+
const normalizedEntries = manifest.entries;
|
|
1030
|
+
const marketplaceIdentity = manifest.marketplace;
|
|
1031
|
+
const pluginNames = normalizedEntries.map((entry) => entry.name);
|
|
1032
|
+
const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
|
|
1033
|
+
if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
|
|
1034
|
+
throw new Error('Invalid catalog snapshot plugin inventory.');
|
|
1035
|
+
const platformEntries = {
|
|
1036
|
+
claude: [],
|
|
1037
|
+
codex: [],
|
|
1038
|
+
};
|
|
1039
|
+
for (const [index, name] of pluginNames.entries()) {
|
|
1040
|
+
const pluginRoot = path.join(rootDir, 'plugins', name);
|
|
1041
|
+
await assertDirectoryWithoutSymlinks(pluginRoot, `Catalog plugin ${JSON.stringify(name)}`);
|
|
1042
|
+
const entry = normalizedEntries[index];
|
|
1043
|
+
const rawBundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(name)} bundle manifest`);
|
|
1044
|
+
const digests = await calculateEntryDigests(pluginRoot);
|
|
1045
|
+
const expectedDigests = {
|
|
1046
|
+
artifactDigest: digests.artifactDigest,
|
|
1047
|
+
manifestDigest: sha256(normalizeManifestDigestInput(rawBundleManifest)),
|
|
1048
|
+
contentDigest: digests.contentDigest,
|
|
1049
|
+
};
|
|
1050
|
+
for (const field of Object.keys(expectedDigests)) {
|
|
1051
|
+
if (entry[field] !== expectedDigests[field]) {
|
|
1052
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
for (const platform of ['claude', 'codex']) {
|
|
1056
|
+
const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
1057
|
+
const exists = await fs.pathExists(platformManifest);
|
|
1058
|
+
if (exists) {
|
|
1059
|
+
await readRegularJson(platformManifest, `${platform} plugin manifest`);
|
|
1060
|
+
platformEntries[platform].push(entry);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
|
|
1065
|
+
const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
|
|
1066
|
+
const marketplaceEntry = (entry) => ({
|
|
1067
|
+
name: entry.name,
|
|
1068
|
+
source: entry.source,
|
|
1069
|
+
description: entry.description,
|
|
1070
|
+
interface: entry.interface,
|
|
1071
|
+
category: entry.category,
|
|
1072
|
+
tags: entry.tags,
|
|
1073
|
+
strict: entry.strict,
|
|
1074
|
+
policy: entry.policy,
|
|
1075
|
+
});
|
|
1076
|
+
const expectedMarketplace = (platform) => ({
|
|
1077
|
+
name: 'aw',
|
|
1078
|
+
owner: marketplaceIdentity.owner,
|
|
1079
|
+
interface: marketplaceIdentity.interface,
|
|
1080
|
+
plugins: platformEntries[platform].map(marketplaceEntry),
|
|
1081
|
+
});
|
|
1082
|
+
const expectedAgents = expectedMarketplace('codex');
|
|
1083
|
+
const expectedClaude = expectedMarketplace('claude');
|
|
1084
|
+
const normalizeMarketplaceOrder = (marketplace) => {
|
|
1085
|
+
const canonical = JSON.parse(JSON.stringify(marketplace));
|
|
1086
|
+
return {
|
|
1087
|
+
...canonical,
|
|
1088
|
+
plugins: Array.isArray(canonical.plugins)
|
|
1089
|
+
? [...canonical.plugins].sort((left, right) => String(left?.name).localeCompare(String(right?.name)))
|
|
1090
|
+
: canonical.plugins,
|
|
1091
|
+
};
|
|
1092
|
+
};
|
|
1093
|
+
if (!isDeepStrictEqual(normalizeMarketplaceOrder(agentsMarketplace), normalizeMarketplaceOrder(expectedAgents))) {
|
|
1094
|
+
throw new Error('Codex marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
1095
|
+
}
|
|
1096
|
+
if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
|
|
1097
|
+
throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
1098
|
+
}
|
|
1099
|
+
return { rootDir, manifest, snapshotDigest };
|
|
1100
|
+
}
|
|
1101
|
+
function validateCatalogGeneratedOutputMarker(raw) {
|
|
1102
|
+
const marker = raw;
|
|
1103
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
1104
|
+
if (!marker
|
|
1105
|
+
|| typeof marker !== 'object'
|
|
1106
|
+
|| Array.isArray(marker)
|
|
1107
|
+
|| !isDeepStrictEqual(Object.keys(marker).sort(), [
|
|
1108
|
+
'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
|
|
1109
|
+
])
|
|
1110
|
+
|| marker.schemaVersion !== 1
|
|
1111
|
+
|| marker.kind !== 'catalog'
|
|
1112
|
+
|| typeof marker.workspaceIdentity !== 'string'
|
|
1113
|
+
|| marker.workspaceIdentity.trim().length === 0
|
|
1114
|
+
|| typeof marker.sourceIdentity !== 'string'
|
|
1115
|
+
|| !digestPattern.test(marker.sourceIdentity)) {
|
|
1116
|
+
throw new Error('Invalid catalog generated output marker.');
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
workspaceIdentity: marker.workspaceIdentity,
|
|
1120
|
+
sourceIdentity: marker.sourceIdentity,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function normalizeCatalogManifestV7(rawValue) {
|
|
1124
|
+
if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
1125
|
+
throw new Error('Invalid catalog manifest v7: expected an object.');
|
|
1126
|
+
}
|
|
1127
|
+
const raw = rawValue;
|
|
880
1128
|
const allowedManifestKeys = new Set([
|
|
881
|
-
'schemaVersion', 'catalogVersion', 'channel', '
|
|
1129
|
+
'schemaVersion', 'catalogVersion', 'channel', 'requires', 'marketplace', 'release', 'entries',
|
|
882
1130
|
]);
|
|
883
1131
|
for (const key of Object.keys(raw))
|
|
884
1132
|
if (!allowedManifestKeys.has(key))
|
|
885
1133
|
throw new Error(`Invalid catalog manifest: unknown field ${JSON.stringify(key)}.`);
|
|
886
|
-
if (raw.schemaVersion !==
|
|
887
|
-
throw new Error('Invalid catalog manifest schemaVersion: expected
|
|
888
|
-
const base = normalizeCatalogManifestBase(raw, 'catalog manifest
|
|
1134
|
+
if (raw.schemaVersion !== 7)
|
|
1135
|
+
throw new Error('Invalid catalog manifest schemaVersion: expected 7.');
|
|
1136
|
+
const base = normalizeCatalogManifestBase(raw, 'catalog manifest v7');
|
|
1137
|
+
if (!(base.requires.aw.capabilities ?? []).includes('aw.catalog-snapshot.v7')) {
|
|
1138
|
+
throw new Error('Invalid catalog manifest requires.aw.capabilities: expected aw.catalog-snapshot.v7.');
|
|
1139
|
+
}
|
|
889
1140
|
const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
|
|
890
1141
|
if (!Array.isArray(raw.entries))
|
|
891
1142
|
throw new Error('Invalid catalog manifest entries: expected an array.');
|
|
@@ -901,94 +1152,84 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
901
1152
|
delete copy.contentDigest;
|
|
902
1153
|
return copy;
|
|
903
1154
|
});
|
|
904
|
-
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest
|
|
1155
|
+
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v7')
|
|
905
1156
|
.map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
|
|
906
1157
|
const pluginNames = normalizedEntries.map((entry, index) => {
|
|
907
1158
|
if (typeof entry.name !== 'string')
|
|
908
1159
|
throw new Error(`Invalid catalog manifest entries[${index}].name.`);
|
|
1160
|
+
if (entry.source !== `./plugins/${entry.name}`) {
|
|
1161
|
+
throw new Error(`Catalog entry source for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
|
|
1162
|
+
}
|
|
909
1163
|
return entry.name;
|
|
910
1164
|
});
|
|
911
1165
|
if (new Set(pluginNames).size !== pluginNames.length)
|
|
912
1166
|
throw new Error('Invalid catalog manifest entries: duplicate plugin name.');
|
|
913
1167
|
if (pluginNames.join('\0') !== [...pluginNames].sort().join('\0'))
|
|
914
1168
|
throw new Error('Invalid catalog manifest entries: expected stable name order.');
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
1169
|
+
const release = normalizeCatalogRelease(raw.release);
|
|
1170
|
+
return {
|
|
1171
|
+
schemaVersion: 7,
|
|
1172
|
+
catalogVersion: base.catalogVersion,
|
|
1173
|
+
channel: base.channel,
|
|
1174
|
+
requires: base.requires,
|
|
1175
|
+
marketplace: marketplaceIdentity,
|
|
1176
|
+
release,
|
|
1177
|
+
entries: normalizedEntries,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
export async function validatePublishedCatalogSnapshotMetadata(snapshotDirectory) {
|
|
1181
|
+
const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot metadata');
|
|
1182
|
+
const [marker, manifest] = await Promise.all([
|
|
1183
|
+
readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker').then(validateCatalogGeneratedOutputMarker),
|
|
1184
|
+
readRegularJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME), 'Catalog manifest v7').then(normalizeCatalogManifestV7),
|
|
1185
|
+
]);
|
|
1186
|
+
if (manifest.release.state !== 'published') {
|
|
1187
|
+
throw new Error('Published Catalog snapshot must have release.state published.');
|
|
1188
|
+
}
|
|
1189
|
+
return { rootDir, workspaceIdentity: marker.workspaceIdentity, manifest };
|
|
1190
|
+
}
|
|
1191
|
+
export async function validateCatalogSnapshotTransport(snapshotDirectory) {
|
|
1192
|
+
return validateCatalogSnapshotTransportInternal(snapshotDirectory);
|
|
1193
|
+
}
|
|
1194
|
+
async function validateCatalogPluginEntriesComplete(validation, pluginNames) {
|
|
1195
|
+
const entriesByName = new Map(validation.manifest.entries.map((entry) => [entry.name, entry]));
|
|
918
1196
|
const plugins = [];
|
|
919
|
-
for (const
|
|
920
|
-
const
|
|
1197
|
+
for (const name of pluginNames) {
|
|
1198
|
+
const entry = entriesByName.get(name);
|
|
1199
|
+
if (!entry)
|
|
1200
|
+
throw new Error(`Catalog plugin ${JSON.stringify(name)} does not exist.`);
|
|
1201
|
+
const pluginRoot = path.join(validation.rootDir, 'plugins', entry.name);
|
|
921
1202
|
const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
|
|
1203
|
+
await validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot);
|
|
922
1204
|
for (const platform of ['claude', 'codex']) {
|
|
923
|
-
const
|
|
924
|
-
const exists = await fs.pathExists(
|
|
1205
|
+
const platformManifestPath = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
1206
|
+
const exists = await fs.pathExists(platformManifestPath);
|
|
925
1207
|
if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
|
|
926
|
-
throw new Error(`Catalog plugin ${JSON.stringify(name)} has an inconsistent ${platform} bundle projection.`);
|
|
1208
|
+
throw new Error(`Catalog plugin ${JSON.stringify(entry.name)} has an inconsistent ${platform} bundle projection.`);
|
|
927
1209
|
}
|
|
928
1210
|
if (exists) {
|
|
929
|
-
const rawPlatformManifest = await readRegularJson(
|
|
1211
|
+
const rawPlatformManifest = await readRegularJson(platformManifestPath, `${platform} plugin manifest`);
|
|
930
1212
|
await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
|
|
931
1213
|
}
|
|
932
1214
|
}
|
|
933
1215
|
const expected = await expectedCatalogEntry(plugin, pluginRoot);
|
|
934
1216
|
for (const field of Object.keys(expected)) {
|
|
935
|
-
if (!isDeepStrictEqual(
|
|
936
|
-
throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1217
|
+
if (!isDeepStrictEqual(entry[field], expected[field])) {
|
|
1218
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
|
|
937
1219
|
}
|
|
938
1220
|
}
|
|
939
1221
|
plugins.push(plugin);
|
|
940
1222
|
}
|
|
941
1223
|
assertDependencyGraph(plugins);
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const canonical = JSON.parse(JSON.stringify(marketplace));
|
|
953
|
-
return {
|
|
954
|
-
...canonical,
|
|
955
|
-
plugins: Array.isArray(canonical.plugins)
|
|
956
|
-
? [...canonical.plugins].sort((left, right) => String(left?.name).localeCompare(String(right?.name)))
|
|
957
|
-
: canonical.plugins,
|
|
958
|
-
};
|
|
959
|
-
};
|
|
960
|
-
if (!isDeepStrictEqual(normalizeMarketplaceOrder(agentsMarketplace), normalizeMarketplaceOrder(expectedAgents))) {
|
|
961
|
-
throw new Error('Codex marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
962
|
-
}
|
|
963
|
-
if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
|
|
964
|
-
throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
965
|
-
}
|
|
966
|
-
const release = normalizeCatalogRelease(raw.release, base.catalogVersion);
|
|
967
|
-
const manifest = {
|
|
968
|
-
schemaVersion: 4,
|
|
969
|
-
catalogVersion: base.catalogVersion,
|
|
970
|
-
channel: base.channel,
|
|
971
|
-
compatibility: base.compatibility,
|
|
972
|
-
marketplace: marketplaceIdentity,
|
|
973
|
-
release,
|
|
974
|
-
entries: normalizedEntries,
|
|
975
|
-
};
|
|
976
|
-
if (release.state === 'published') {
|
|
977
|
-
const contentDigest = publishedContentDigest(manifest);
|
|
978
|
-
if (release.contentDigest !== contentDigest) {
|
|
979
|
-
throw new Error('Published catalog release contentDigest does not match the snapshot.');
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
const hasLedger = entries.includes(PLUGIN_RELEASE_LEDGER_FILENAME);
|
|
983
|
-
if (release.state === 'published' && !hasLedger) {
|
|
984
|
-
throw new Error('Published Catalog snapshot must include plugin-release-ledger.json.');
|
|
985
|
-
}
|
|
986
|
-
if (release.state === 'development' && hasLedger) {
|
|
987
|
-
throw new Error('Development Catalog snapshot must not include plugin-release-ledger.json.');
|
|
988
|
-
}
|
|
989
|
-
const ledger = hasLedger
|
|
990
|
-
? validatePluginReleaseLedgerAgainstManifest(await readRegularJson(path.join(rootDir, PLUGIN_RELEASE_LEDGER_FILENAME), 'Plugin release ledger'), manifest)
|
|
991
|
-
: undefined;
|
|
992
|
-
return { rootDir, manifest, ...(ledger ? { ledger } : {}), snapshotDigest };
|
|
1224
|
+
}
|
|
1225
|
+
export async function validateCatalogSnapshotPluginEntriesComplete(snapshotDirectory, pluginNames) {
|
|
1226
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1227
|
+
await validateCatalogPluginEntriesComplete(validation, pluginNames);
|
|
1228
|
+
return validation;
|
|
1229
|
+
}
|
|
1230
|
+
export async function validateCatalogSnapshotComplete(snapshotDirectory) {
|
|
1231
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1232
|
+
await validateCatalogPluginEntriesComplete(validation, validation.manifest.entries.map((entry) => entry.name));
|
|
1233
|
+
return validation;
|
|
993
1234
|
}
|
|
994
1235
|
//# sourceMappingURL=workspace.js.map
|