@lesliechan721/aw-plugin-core 0.1.0-beta.5 → 0.1.0-beta.51
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 +114 -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 +1350 -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/manifest-contract.d.ts +3 -6
- package/dist/manifest-contract.js +224 -398
- package/dist/manifest-contract.js.map +1 -1
- package/dist/manifest-type-contract.d.ts +2 -2
- package/dist/manifest.d.ts +10 -8
- package/dist/manifest.js +16 -7
- package/dist/manifest.js.map +1 -1
- package/dist/plugin-authoring-capabilities.d.ts +1 -1
- package/dist/plugin-authoring-capabilities.js +60 -26
- package/dist/plugin-authoring-capabilities.js.map +1 -1
- package/dist/plugin-config.d.ts +39 -0
- package/dist/plugin-config.js +1320 -0
- package/dist/plugin-config.js.map +1 -0
- package/dist/plugin-requirements.d.ts +34 -0
- package/dist/plugin-requirements.js +289 -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 +249 -61
- package/dist/workspace.d.ts +37 -9
- package/dist/workspace.js +419 -191
- package/dist/workspace.js.map +1 -1
- package/package.json +13 -4
- package/schemas/plugin-manifest-v3.json +2122 -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,18 +467,7 @@ 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 catalogPathRecord(root, relative, versionNeutral, executableFiles) {
|
|
446
471
|
const filePath = path.join(root, ...relative.split('/'));
|
|
447
472
|
const stat = await fs.lstat(filePath);
|
|
448
473
|
if (stat.isSymbolicLink()) {
|
|
@@ -451,26 +476,72 @@ async function catalogPathRecord(root, relative, versionNeutral) {
|
|
|
451
476
|
if (!stat.isFile())
|
|
452
477
|
throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
|
|
453
478
|
const manifests = new Set(['.agents-plugin/plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json']);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
479
|
+
let digest;
|
|
480
|
+
if (versionNeutral && manifests.has(relative)) {
|
|
481
|
+
digest = sha256(stableStringify({
|
|
482
|
+
...await fs.readJson(filePath),
|
|
483
|
+
version: '<plugin-version>',
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
else if (versionNeutral && path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME) {
|
|
487
|
+
const receipt = await fs.readJson(filePath);
|
|
488
|
+
const plugin = receipt.plugin && typeof receipt.plugin === 'object' && !Array.isArray(receipt.plugin)
|
|
489
|
+
? receipt.plugin
|
|
490
|
+
: {};
|
|
491
|
+
digest = sha256(stableStringify({
|
|
492
|
+
...receipt,
|
|
493
|
+
plugin: { ...plugin, version: '<plugin-version>' },
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
digest = sha256(await fs.readFile(filePath));
|
|
498
|
+
}
|
|
499
|
+
return { path: relative, kind: 'file', mode: catalogFileMode(relative, stat.mode, executableFiles), digest };
|
|
458
500
|
}
|
|
459
501
|
async function calculateEntryDigest(root, versionNeutral) {
|
|
460
502
|
const files = await listBundleFiles(root);
|
|
461
|
-
|
|
503
|
+
const executableFiles = await readCatalogExecutableFiles(root, files);
|
|
504
|
+
return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral, executableFiles)))));
|
|
462
505
|
}
|
|
463
506
|
async function expectedCatalogEntry(plugin, pluginRoot) {
|
|
507
|
+
const bundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} bundle manifest`);
|
|
464
508
|
return {
|
|
465
509
|
...createMarketplaceManifestEntry(plugin),
|
|
466
510
|
version: plugin.manifest.version,
|
|
467
|
-
|
|
511
|
+
dependencies: [...(plugin.manifest.dependencies ?? [])].sort(),
|
|
512
|
+
requires: {
|
|
513
|
+
aw: {
|
|
514
|
+
capabilities: normalizeAwCapabilities(plugin.manifest.requires?.aw.capabilities ?? []),
|
|
515
|
+
},
|
|
516
|
+
},
|
|
468
517
|
artifactDigest: await calculateEntryDigest(pluginRoot, false),
|
|
469
|
-
manifestDigest: sha256(normalizeManifestDigestInput(
|
|
518
|
+
manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
|
|
470
519
|
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
471
520
|
};
|
|
472
521
|
}
|
|
473
|
-
function
|
|
522
|
+
async function validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot) {
|
|
523
|
+
const files = await listBundleFiles(pluginRoot);
|
|
524
|
+
const expectedReceiptPaths = new Set(files.flatMap((relative) => /^(?:claude|codex|agent-skills)\/skills\/[^/]+\/SKILL\.md$/u.test(relative)
|
|
525
|
+
? [path.posix.join(path.posix.dirname(relative), AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)]
|
|
526
|
+
: []));
|
|
527
|
+
const actualReceiptPaths = files.filter((relative) => path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME);
|
|
528
|
+
for (const relative of expectedReceiptPaths) {
|
|
529
|
+
if (!actualReceiptPaths.includes(relative)) {
|
|
530
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} Skill root is missing ${AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME}: ${relative}.`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const expected = createAwPluginRequirementsReceipt({ name: plugin.manifest.name, version: plugin.manifest.version }, plugin.manifest.requires?.aw.capabilities ?? []);
|
|
534
|
+
for (const relative of actualReceiptPaths) {
|
|
535
|
+
if (!expectedReceiptPaths.has(relative)) {
|
|
536
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a reserved runtime requirements receipt outside a Skill root: ${relative}.`);
|
|
537
|
+
}
|
|
538
|
+
const receipt = parseAwPluginRequirementsReceipt(await readRegularJson(path.join(pluginRoot, ...relative.split('/')), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} runtime requirements receipt`));
|
|
539
|
+
if (!isDeepStrictEqual(receipt, expected)) {
|
|
540
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a mismatched runtime requirements receipt: ${relative}.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function normalizeCatalogRelease(raw) {
|
|
474
545
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
475
546
|
throw new Error('Invalid catalog manifest release: expected an object.');
|
|
476
547
|
}
|
|
@@ -481,33 +552,16 @@ function normalizeCatalogRelease(raw, catalogVersion) {
|
|
|
481
552
|
}
|
|
482
553
|
return { state: 'development' };
|
|
483
554
|
}
|
|
484
|
-
const allowed = new Set(['state', '
|
|
555
|
+
const allowed = new Set(['state', 'sourceHead']);
|
|
485
556
|
if (release.state !== 'published' || Object.keys(release).some((key) => !allowed.has(key))) {
|
|
486
557
|
throw new Error('Invalid published catalog release.');
|
|
487
558
|
}
|
|
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
559
|
if (typeof release.sourceHead !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(release.sourceHead)) {
|
|
503
560
|
throw new Error('Invalid catalog release sourceHead.');
|
|
504
561
|
}
|
|
505
562
|
return {
|
|
506
563
|
state: 'published',
|
|
507
|
-
targetVersion,
|
|
508
|
-
planId: release.planId,
|
|
509
564
|
sourceHead: release.sourceHead,
|
|
510
|
-
contentDigest: release.contentDigest,
|
|
511
565
|
};
|
|
512
566
|
}
|
|
513
567
|
export async function buildCatalogWorkspace(options) {
|
|
@@ -521,7 +575,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
521
575
|
const catalogVersion = catalogWorkspace.metadata.catalog.version;
|
|
522
576
|
const channel = deriveCatalogChannel(catalogVersion);
|
|
523
577
|
const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
|
|
524
|
-
const identity =
|
|
578
|
+
const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
|
|
525
579
|
const marker = generatedOutputMarker({
|
|
526
580
|
kind: 'catalog',
|
|
527
581
|
workspaceIdentity: catalogWorkspace.workspaceIdentity,
|
|
@@ -532,6 +586,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
532
586
|
force: options.force === true,
|
|
533
587
|
build: async (stagingRoot) => {
|
|
534
588
|
const bundledPlugins = [];
|
|
589
|
+
await fs.ensureDir(path.join(stagingRoot, 'plugins'));
|
|
535
590
|
for (const plugin of plugins) {
|
|
536
591
|
const closure = dependencyClosure(plugin, plugins);
|
|
537
592
|
const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
|
|
@@ -543,6 +598,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
543
598
|
dependencies: closure.slice(0, -1),
|
|
544
599
|
outputRoot,
|
|
545
600
|
});
|
|
601
|
+
await writeCatalogExecutableFiles(outputRoot);
|
|
546
602
|
bundledPlugins.push(await readPluginManifest(outputRoot, { mode: 'bundle' }));
|
|
547
603
|
}
|
|
548
604
|
const byPlatform = (platform) => bundledPlugins
|
|
@@ -550,20 +606,14 @@ export async function buildCatalogWorkspace(options) {
|
|
|
550
606
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
|
|
551
607
|
await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('codex')), { spaces: 2 });
|
|
552
608
|
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
609
|
const entries = await Promise.all([...bundledPlugins]
|
|
560
610
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
561
611
|
.map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
|
|
562
612
|
const manifest = {
|
|
563
|
-
schemaVersion:
|
|
613
|
+
schemaVersion: 7,
|
|
564
614
|
catalogVersion,
|
|
565
615
|
channel,
|
|
566
|
-
|
|
616
|
+
requires: { aw: { capabilities: ['aw.catalog-snapshot.v7'] } },
|
|
567
617
|
marketplace: {
|
|
568
618
|
owner: catalogWorkspace.metadata.owner,
|
|
569
619
|
interface: catalogWorkspace.metadata.interface,
|
|
@@ -574,9 +624,9 @@ export async function buildCatalogWorkspace(options) {
|
|
|
574
624
|
await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
|
|
575
625
|
await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
|
|
576
626
|
},
|
|
577
|
-
validate: async (stagingRoot) => { await
|
|
627
|
+
validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
|
|
578
628
|
assertReplaceable: async (existingRoot) => {
|
|
579
|
-
await
|
|
629
|
+
await validateCatalogSnapshotComplete(existingRoot);
|
|
580
630
|
await assertGeneratedOutput(existingRoot, marker);
|
|
581
631
|
},
|
|
582
632
|
});
|
|
@@ -596,16 +646,132 @@ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
|
|
|
596
646
|
const manifest = await fs.readJson(manifestPath);
|
|
597
647
|
await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
|
|
598
648
|
}
|
|
649
|
+
const pluginRoot = path.join(snapshotRoot, 'plugins', pluginName);
|
|
650
|
+
for (const relative of await listBundleFiles(pluginRoot)) {
|
|
651
|
+
if (path.posix.basename(relative) !== AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)
|
|
652
|
+
continue;
|
|
653
|
+
const receiptPath = path.join(pluginRoot, ...relative.split('/'));
|
|
654
|
+
const receipt = parseAwPluginRequirementsReceipt(await fs.readJson(receiptPath));
|
|
655
|
+
if (receipt.plugin.name !== pluginName) {
|
|
656
|
+
throw new Error(`Catalog plugin ${JSON.stringify(pluginName)} has a mismatched runtime requirements receipt.`);
|
|
657
|
+
}
|
|
658
|
+
await fs.writeFile(receiptPath, `${JSON.stringify(createAwPluginRequirementsReceipt({ name: pluginName, version }, receipt.requiredCapabilities), null, 2)}\n`);
|
|
659
|
+
}
|
|
599
660
|
}
|
|
600
661
|
}
|
|
662
|
+
function assertStablePluginVersion(version, fieldName) {
|
|
663
|
+
const parsed = parseStrictSemVer(version, fieldName);
|
|
664
|
+
if (parsed.prerelease.length > 0 || parsed.build.length > 0) {
|
|
665
|
+
throw new Error(`Invalid ${fieldName}: expected a stable SemVer without prerelease or build metadata.`);
|
|
666
|
+
}
|
|
667
|
+
return version;
|
|
668
|
+
}
|
|
669
|
+
function stableVersionBase(version, fieldName) {
|
|
670
|
+
const parsed = parseStrictSemVer(version, fieldName);
|
|
671
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Assign versions for planning and snapshot preparation with the same content comparison.
|
|
675
|
+
* Official history includes removed Plugins. Baseline declarations come from main source,
|
|
676
|
+
* before generated candidate metadata changes.
|
|
677
|
+
*/
|
|
678
|
+
export function resolvePublishedPluginVersions(options) {
|
|
679
|
+
const official = options.officialPluginHistory !== undefined;
|
|
680
|
+
const previousEntries = new Map();
|
|
681
|
+
const historicalContents = new Map();
|
|
682
|
+
for (const entry of [...options.previousPluginEntries ?? [], ...options.officialPluginHistory ?? []]) {
|
|
683
|
+
if (official) {
|
|
684
|
+
assertCatalogVersion(entry.version, `${entry.name} previous published version`);
|
|
685
|
+
const key = `${entry.name}@${entry.version}`;
|
|
686
|
+
const content = historicalContents.get(key);
|
|
687
|
+
if (content !== undefined && content !== entry.contentDigest) {
|
|
688
|
+
throw new Error(`Plugin ${entry.name} version ${entry.version} maps to different content in published history.`);
|
|
689
|
+
}
|
|
690
|
+
historicalContents.set(key, entry.contentDigest);
|
|
691
|
+
const previous = previousEntries.get(entry.name);
|
|
692
|
+
if (previous && compareStrictSemVer(previous.version, entry.version) >= 0)
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
previousEntries.set(entry.name, entry);
|
|
696
|
+
}
|
|
697
|
+
const versions = {};
|
|
698
|
+
for (const entry of options.entries) {
|
|
699
|
+
const { name, contentDigest } = entry;
|
|
700
|
+
if (!contentDigest)
|
|
701
|
+
throw new Error(`Missing release content digest for plugin ${JSON.stringify(name)}.`);
|
|
702
|
+
const sourceVersion = assertStablePluginVersion(entry.version, `${name} source version`);
|
|
703
|
+
const previous = previousEntries.get(name);
|
|
704
|
+
if (!previous) {
|
|
705
|
+
versions[name] = options.channel === 'beta' ? `${sourceVersion}-beta.1` : sourceVersion;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const previousVersion = assertStrictSemVer(previous.version, `${name} previous published version`);
|
|
709
|
+
const previousBase = stableVersionBase(previousVersion, `${name} previous published version`);
|
|
710
|
+
const baseComparison = compareStrictSemVer(sourceVersion, previousBase);
|
|
711
|
+
const parsedPrevious = parseStrictSemVer(previousVersion, `${name} previous published version`);
|
|
712
|
+
const baselineSourceVersion = options.sourcePluginVersionsAtBaseline?.[name];
|
|
713
|
+
if (official && baselineSourceVersion !== undefined
|
|
714
|
+
&& sourceVersion !== assertStablePluginVersion(baselineSourceVersion, `${name} baseline source version`)) {
|
|
715
|
+
if (baseComparison < 0) {
|
|
716
|
+
throw new Error(`Plugin ${name} explicit source version ${sourceVersion} must not be lower than published base ${previousBase}.`);
|
|
717
|
+
}
|
|
718
|
+
if (baseComparison === 0 && parsedPrevious.prerelease.length === 0) {
|
|
719
|
+
throw new Error(`Plugin ${name} explicit source version ${sourceVersion} is already published as stable. Select a higher base.`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (options.channel === 'latest') {
|
|
723
|
+
const comparison = compareStrictSemVer(sourceVersion, previousVersion);
|
|
724
|
+
if (!official && comparison < 0) {
|
|
725
|
+
throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous ${previousVersion}.`);
|
|
726
|
+
}
|
|
727
|
+
if (!official && comparison === 0 && previous.contentDigest !== contentDigest) {
|
|
728
|
+
throw new Error(`Plugin ${name} version ${sourceVersion} maps to different content. Increase the source plugin version.`);
|
|
729
|
+
}
|
|
730
|
+
let version = sourceVersion;
|
|
731
|
+
if (official && baseComparison <= 0) {
|
|
732
|
+
version = previousBase;
|
|
733
|
+
if (parsedPrevious.prerelease.length === 0 && previous.contentDigest !== contentDigest) {
|
|
734
|
+
version = `${parsedPrevious.major}.${parsedPrevious.minor}.${parsedPrevious.patch + 1}`;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
versions[name] = version;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (!official && baseComparison < 0) {
|
|
741
|
+
throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous base ${previousBase}.`);
|
|
742
|
+
}
|
|
743
|
+
if (baseComparison > 0) {
|
|
744
|
+
versions[name] = `${sourceVersion}-beta.1`;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (parsedPrevious.prerelease.length === 0) {
|
|
748
|
+
if (previous.contentDigest !== contentDigest) {
|
|
749
|
+
if (!official) {
|
|
750
|
+
throw new Error(`Plugin ${name} stable version ${sourceVersion} maps to different content. Increase the source plugin version.`);
|
|
751
|
+
}
|
|
752
|
+
versions[name] = `${parsedPrevious.major}.${parsedPrevious.minor}.${parsedPrevious.patch + 1}-beta.1`;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
versions[name] = previousVersion;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (parsedPrevious.prerelease.length !== 2
|
|
759
|
+
|| parsedPrevious.prerelease[0] !== 'beta'
|
|
760
|
+
|| !/^[1-9]\d*$/u.test(parsedPrevious.prerelease[1])) {
|
|
761
|
+
throw new Error(`Plugin ${name} previous version ${previousVersion} is not a stable or beta publication version.`);
|
|
762
|
+
}
|
|
763
|
+
versions[name] = previous.contentDigest === contentDigest
|
|
764
|
+
? previousVersion
|
|
765
|
+
: `${previousBase}-beta.${Number(parsedPrevious.prerelease[1]) + 1}`;
|
|
766
|
+
}
|
|
767
|
+
return versions;
|
|
768
|
+
}
|
|
601
769
|
export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
|
|
602
|
-
const validation = await
|
|
770
|
+
const validation = await validateCatalogSnapshotComplete(snapshotDirectory);
|
|
603
771
|
if (validation.manifest.release.state !== 'published') {
|
|
604
772
|
throw new Error('Published Catalog snapshot must have release.state published.');
|
|
605
773
|
}
|
|
606
|
-
|
|
607
|
-
throw new Error('Published Catalog snapshot must include a plugin release ledger.');
|
|
608
|
-
return { ...validation, ledger: validation.ledger };
|
|
774
|
+
return validation;
|
|
609
775
|
}
|
|
610
776
|
export async function preparePublishedCatalogWorkspace(options) {
|
|
611
777
|
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(options.sourceHead)) {
|
|
@@ -620,13 +786,12 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
620
786
|
const previous = options.previousSnapshotRoot
|
|
621
787
|
? await validatePublishedCatalogSnapshot(options.previousSnapshotRoot)
|
|
622
788
|
: null;
|
|
789
|
+
if (!previous && (options.historicalPluginEntries?.length ?? 0) > 0) {
|
|
790
|
+
throw new Error('Published Plugin history requires a previous Catalog snapshot.');
|
|
791
|
+
}
|
|
623
792
|
if (previous && compareStrictSemVer(catalogVersion, previous.manifest.catalogVersion) <= 0) {
|
|
624
793
|
throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.manifest.catalogVersion}.`);
|
|
625
794
|
}
|
|
626
|
-
const previousLedger = previous?.ledger ?? {
|
|
627
|
-
schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
|
|
628
|
-
records: [],
|
|
629
|
-
};
|
|
630
795
|
const temporaryRelative = path.join('.aw', 'publish', `.catalog-preparation-${crypto.randomUUID()}`);
|
|
631
796
|
const finalRelative = options.outputPath
|
|
632
797
|
?? `${DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH}-v${catalogVersion}`;
|
|
@@ -639,15 +804,14 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
639
804
|
reservedImports: options.reservedImports,
|
|
640
805
|
});
|
|
641
806
|
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]));
|
|
807
|
+
const development = await validateCatalogSnapshotComplete(temporaryRoot);
|
|
808
|
+
const overrides = resolvePublishedPluginVersions({
|
|
809
|
+
channel,
|
|
810
|
+
entries: development.manifest.entries,
|
|
811
|
+
previousPluginEntries: [...options.historicalPluginEntries ?? [], ...previous?.manifest.entries ?? []],
|
|
812
|
+
officialPluginHistory: options.officialPluginHistory,
|
|
813
|
+
sourcePluginVersionsAtBaseline: options.sourcePluginVersionsAtBaseline,
|
|
814
|
+
});
|
|
651
815
|
await rewritePublishedPluginVersions(temporaryRoot, overrides);
|
|
652
816
|
const bundledPlugins = await Promise.all(catalogWorkspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
|
|
653
817
|
const entries = await Promise.all(bundledPlugins.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
@@ -658,26 +822,14 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
658
822
|
channel,
|
|
659
823
|
entries,
|
|
660
824
|
};
|
|
661
|
-
const contentDigest = publishedContentDigest(manifestWithoutRelease);
|
|
662
825
|
const manifest = {
|
|
663
826
|
...manifestWithoutRelease,
|
|
664
827
|
release: {
|
|
665
828
|
state: 'published',
|
|
666
|
-
targetVersion: catalogVersion.replace(/-beta\.[1-9]\d*$/u, ''),
|
|
667
|
-
planId: contentDigest,
|
|
668
829
|
sourceHead: options.sourceHead,
|
|
669
|
-
contentDigest,
|
|
670
830
|
},
|
|
671
831
|
};
|
|
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
|
-
]);
|
|
832
|
+
await fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
681
833
|
const prepared = await validatePublishedCatalogSnapshot(temporaryRoot);
|
|
682
834
|
const resolved = await resolveCatalogWorkspaceOutputPath({
|
|
683
835
|
workspaceRoot: catalogWorkspace.workspaceRoot,
|
|
@@ -701,8 +853,6 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
701
853
|
channel,
|
|
702
854
|
pluginNames: built.pluginNames,
|
|
703
855
|
snapshotDigest: prepared.snapshotDigest,
|
|
704
|
-
contentDigest,
|
|
705
|
-
planId: contentDigest,
|
|
706
856
|
};
|
|
707
857
|
}
|
|
708
858
|
finally {
|
|
@@ -828,7 +978,7 @@ async function digestDirectory(root) {
|
|
|
828
978
|
await walk(root);
|
|
829
979
|
return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
|
|
830
980
|
}
|
|
831
|
-
|
|
981
|
+
async function validateCatalogSnapshotTransportInternal(snapshotDirectory) {
|
|
832
982
|
const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
|
|
833
983
|
const allowed = new Set([
|
|
834
984
|
CATALOG_MANIFEST_FILENAME,
|
|
@@ -836,7 +986,6 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
836
986
|
'.agents',
|
|
837
987
|
'.claude-plugin',
|
|
838
988
|
'plugins',
|
|
839
|
-
'plugin-release-ledger.json',
|
|
840
989
|
]);
|
|
841
990
|
const entries = await fs.readdir(rootDir);
|
|
842
991
|
const unexpected = entries.filter((entry) => !allowed.has(entry));
|
|
@@ -846,19 +995,7 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
846
995
|
throw new Error(`Invalid catalog snapshot root layout: missing [${missing.join(', ')}], unexpected [${unexpected.join(', ')}].`);
|
|
847
996
|
}
|
|
848
997
|
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
|
-
}
|
|
998
|
+
validateCatalogGeneratedOutputMarker(await readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker'));
|
|
862
999
|
}
|
|
863
1000
|
const assertLayout = async (directory, expected) => {
|
|
864
1001
|
const real = await assertDirectoryWithoutSymlinks(directory, 'Catalog snapshot directory');
|
|
@@ -876,16 +1013,117 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
876
1013
|
await readRegularJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'), 'Codex marketplace');
|
|
877
1014
|
await readRegularJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'), 'Claude marketplace');
|
|
878
1015
|
const snapshotDigest = await digestDirectory(rootDir);
|
|
879
|
-
const
|
|
1016
|
+
const manifest = normalizeCatalogManifestV7(await readRegularJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME), 'Catalog manifest v7'));
|
|
1017
|
+
const normalizedEntries = manifest.entries;
|
|
1018
|
+
const marketplaceIdentity = manifest.marketplace;
|
|
1019
|
+
const pluginNames = normalizedEntries.map((entry) => entry.name);
|
|
1020
|
+
const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
|
|
1021
|
+
if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
|
|
1022
|
+
throw new Error('Invalid catalog snapshot plugin inventory.');
|
|
1023
|
+
const platformEntries = {
|
|
1024
|
+
claude: [],
|
|
1025
|
+
codex: [],
|
|
1026
|
+
};
|
|
1027
|
+
for (const [index, name] of pluginNames.entries()) {
|
|
1028
|
+
const pluginRoot = path.join(rootDir, 'plugins', name);
|
|
1029
|
+
await assertDirectoryWithoutSymlinks(pluginRoot, `Catalog plugin ${JSON.stringify(name)}`);
|
|
1030
|
+
const entry = normalizedEntries[index];
|
|
1031
|
+
const rawBundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(name)} bundle manifest`);
|
|
1032
|
+
const expectedDigests = {
|
|
1033
|
+
artifactDigest: await calculateEntryDigest(pluginRoot, false),
|
|
1034
|
+
manifestDigest: sha256(normalizeManifestDigestInput(rawBundleManifest)),
|
|
1035
|
+
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
1036
|
+
};
|
|
1037
|
+
for (const field of Object.keys(expectedDigests)) {
|
|
1038
|
+
if (entry[field] !== expectedDigests[field]) {
|
|
1039
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const platform of ['claude', 'codex']) {
|
|
1043
|
+
const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
1044
|
+
const exists = await fs.pathExists(platformManifest);
|
|
1045
|
+
if (exists) {
|
|
1046
|
+
await readRegularJson(platformManifest, `${platform} plugin manifest`);
|
|
1047
|
+
platformEntries[platform].push(entry);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
|
|
1052
|
+
const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
|
|
1053
|
+
const marketplaceEntry = (entry) => ({
|
|
1054
|
+
name: entry.name,
|
|
1055
|
+
source: entry.source,
|
|
1056
|
+
description: entry.description,
|
|
1057
|
+
interface: entry.interface,
|
|
1058
|
+
category: entry.category,
|
|
1059
|
+
tags: entry.tags,
|
|
1060
|
+
strict: entry.strict,
|
|
1061
|
+
policy: entry.policy,
|
|
1062
|
+
});
|
|
1063
|
+
const expectedMarketplace = (platform) => ({
|
|
1064
|
+
name: 'aw',
|
|
1065
|
+
owner: marketplaceIdentity.owner,
|
|
1066
|
+
interface: marketplaceIdentity.interface,
|
|
1067
|
+
plugins: platformEntries[platform].map(marketplaceEntry),
|
|
1068
|
+
});
|
|
1069
|
+
const expectedAgents = expectedMarketplace('codex');
|
|
1070
|
+
const expectedClaude = expectedMarketplace('claude');
|
|
1071
|
+
const normalizeMarketplaceOrder = (marketplace) => {
|
|
1072
|
+
const canonical = JSON.parse(JSON.stringify(marketplace));
|
|
1073
|
+
return {
|
|
1074
|
+
...canonical,
|
|
1075
|
+
plugins: Array.isArray(canonical.plugins)
|
|
1076
|
+
? [...canonical.plugins].sort((left, right) => String(left?.name).localeCompare(String(right?.name)))
|
|
1077
|
+
: canonical.plugins,
|
|
1078
|
+
};
|
|
1079
|
+
};
|
|
1080
|
+
if (!isDeepStrictEqual(normalizeMarketplaceOrder(agentsMarketplace), normalizeMarketplaceOrder(expectedAgents))) {
|
|
1081
|
+
throw new Error('Codex marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
1082
|
+
}
|
|
1083
|
+
if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
|
|
1084
|
+
throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
1085
|
+
}
|
|
1086
|
+
return { rootDir, manifest, snapshotDigest };
|
|
1087
|
+
}
|
|
1088
|
+
function validateCatalogGeneratedOutputMarker(raw) {
|
|
1089
|
+
const marker = raw;
|
|
1090
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
1091
|
+
if (!marker
|
|
1092
|
+
|| typeof marker !== 'object'
|
|
1093
|
+
|| Array.isArray(marker)
|
|
1094
|
+
|| !isDeepStrictEqual(Object.keys(marker).sort(), [
|
|
1095
|
+
'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
|
|
1096
|
+
])
|
|
1097
|
+
|| marker.schemaVersion !== 1
|
|
1098
|
+
|| marker.kind !== 'catalog'
|
|
1099
|
+
|| typeof marker.workspaceIdentity !== 'string'
|
|
1100
|
+
|| marker.workspaceIdentity.trim().length === 0
|
|
1101
|
+
|| typeof marker.sourceIdentity !== 'string'
|
|
1102
|
+
|| !digestPattern.test(marker.sourceIdentity)) {
|
|
1103
|
+
throw new Error('Invalid catalog generated output marker.');
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
workspaceIdentity: marker.workspaceIdentity,
|
|
1107
|
+
sourceIdentity: marker.sourceIdentity,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function normalizeCatalogManifestV7(rawValue) {
|
|
1111
|
+
if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
1112
|
+
throw new Error('Invalid catalog manifest v7: expected an object.');
|
|
1113
|
+
}
|
|
1114
|
+
const raw = rawValue;
|
|
880
1115
|
const allowedManifestKeys = new Set([
|
|
881
|
-
'schemaVersion', 'catalogVersion', 'channel', '
|
|
1116
|
+
'schemaVersion', 'catalogVersion', 'channel', 'requires', 'marketplace', 'release', 'entries',
|
|
882
1117
|
]);
|
|
883
1118
|
for (const key of Object.keys(raw))
|
|
884
1119
|
if (!allowedManifestKeys.has(key))
|
|
885
1120
|
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
|
|
1121
|
+
if (raw.schemaVersion !== 7)
|
|
1122
|
+
throw new Error('Invalid catalog manifest schemaVersion: expected 7.');
|
|
1123
|
+
const base = normalizeCatalogManifestBase(raw, 'catalog manifest v7');
|
|
1124
|
+
if (!(base.requires.aw.capabilities ?? []).includes('aw.catalog-snapshot.v7')) {
|
|
1125
|
+
throw new Error('Invalid catalog manifest requires.aw.capabilities: expected aw.catalog-snapshot.v7.');
|
|
1126
|
+
}
|
|
889
1127
|
const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
|
|
890
1128
|
if (!Array.isArray(raw.entries))
|
|
891
1129
|
throw new Error('Invalid catalog manifest entries: expected an array.');
|
|
@@ -901,94 +1139,84 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
901
1139
|
delete copy.contentDigest;
|
|
902
1140
|
return copy;
|
|
903
1141
|
});
|
|
904
|
-
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest
|
|
1142
|
+
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v7')
|
|
905
1143
|
.map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
|
|
906
1144
|
const pluginNames = normalizedEntries.map((entry, index) => {
|
|
907
1145
|
if (typeof entry.name !== 'string')
|
|
908
1146
|
throw new Error(`Invalid catalog manifest entries[${index}].name.`);
|
|
1147
|
+
if (entry.source !== `./plugins/${entry.name}`) {
|
|
1148
|
+
throw new Error(`Catalog entry source for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
|
|
1149
|
+
}
|
|
909
1150
|
return entry.name;
|
|
910
1151
|
});
|
|
911
1152
|
if (new Set(pluginNames).size !== pluginNames.length)
|
|
912
1153
|
throw new Error('Invalid catalog manifest entries: duplicate plugin name.');
|
|
913
1154
|
if (pluginNames.join('\0') !== [...pluginNames].sort().join('\0'))
|
|
914
1155
|
throw new Error('Invalid catalog manifest entries: expected stable name order.');
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
1156
|
+
const release = normalizeCatalogRelease(raw.release);
|
|
1157
|
+
return {
|
|
1158
|
+
schemaVersion: 7,
|
|
1159
|
+
catalogVersion: base.catalogVersion,
|
|
1160
|
+
channel: base.channel,
|
|
1161
|
+
requires: base.requires,
|
|
1162
|
+
marketplace: marketplaceIdentity,
|
|
1163
|
+
release,
|
|
1164
|
+
entries: normalizedEntries,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
export async function validatePublishedCatalogSnapshotMetadata(snapshotDirectory) {
|
|
1168
|
+
const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot metadata');
|
|
1169
|
+
const [marker, manifest] = await Promise.all([
|
|
1170
|
+
readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker').then(validateCatalogGeneratedOutputMarker),
|
|
1171
|
+
readRegularJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME), 'Catalog manifest v7').then(normalizeCatalogManifestV7),
|
|
1172
|
+
]);
|
|
1173
|
+
if (manifest.release.state !== 'published') {
|
|
1174
|
+
throw new Error('Published Catalog snapshot must have release.state published.');
|
|
1175
|
+
}
|
|
1176
|
+
return { rootDir, workspaceIdentity: marker.workspaceIdentity, manifest };
|
|
1177
|
+
}
|
|
1178
|
+
export async function validateCatalogSnapshotTransport(snapshotDirectory) {
|
|
1179
|
+
return validateCatalogSnapshotTransportInternal(snapshotDirectory);
|
|
1180
|
+
}
|
|
1181
|
+
async function validateCatalogPluginEntriesComplete(validation, pluginNames) {
|
|
1182
|
+
const entriesByName = new Map(validation.manifest.entries.map((entry) => [entry.name, entry]));
|
|
918
1183
|
const plugins = [];
|
|
919
|
-
for (const
|
|
920
|
-
const
|
|
1184
|
+
for (const name of pluginNames) {
|
|
1185
|
+
const entry = entriesByName.get(name);
|
|
1186
|
+
if (!entry)
|
|
1187
|
+
throw new Error(`Catalog plugin ${JSON.stringify(name)} does not exist.`);
|
|
1188
|
+
const pluginRoot = path.join(validation.rootDir, 'plugins', entry.name);
|
|
921
1189
|
const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
|
|
1190
|
+
await validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot);
|
|
922
1191
|
for (const platform of ['claude', 'codex']) {
|
|
923
|
-
const
|
|
924
|
-
const exists = await fs.pathExists(
|
|
1192
|
+
const platformManifestPath = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
1193
|
+
const exists = await fs.pathExists(platformManifestPath);
|
|
925
1194
|
if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
|
|
926
|
-
throw new Error(`Catalog plugin ${JSON.stringify(name)} has an inconsistent ${platform} bundle projection.`);
|
|
1195
|
+
throw new Error(`Catalog plugin ${JSON.stringify(entry.name)} has an inconsistent ${platform} bundle projection.`);
|
|
927
1196
|
}
|
|
928
1197
|
if (exists) {
|
|
929
|
-
const rawPlatformManifest = await readRegularJson(
|
|
1198
|
+
const rawPlatformManifest = await readRegularJson(platformManifestPath, `${platform} plugin manifest`);
|
|
930
1199
|
await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
|
|
931
1200
|
}
|
|
932
1201
|
}
|
|
933
1202
|
const expected = await expectedCatalogEntry(plugin, pluginRoot);
|
|
934
1203
|
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.`);
|
|
1204
|
+
if (!isDeepStrictEqual(entry[field], expected[field])) {
|
|
1205
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
|
|
937
1206
|
}
|
|
938
1207
|
}
|
|
939
1208
|
plugins.push(plugin);
|
|
940
1209
|
}
|
|
941
1210
|
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 };
|
|
1211
|
+
}
|
|
1212
|
+
export async function validateCatalogSnapshotPluginEntriesComplete(snapshotDirectory, pluginNames) {
|
|
1213
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1214
|
+
await validateCatalogPluginEntriesComplete(validation, pluginNames);
|
|
1215
|
+
return validation;
|
|
1216
|
+
}
|
|
1217
|
+
export async function validateCatalogSnapshotComplete(snapshotDirectory) {
|
|
1218
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1219
|
+
await validateCatalogPluginEntriesComplete(validation, validation.manifest.entries.map((entry) => entry.name));
|
|
1220
|
+
return validation;
|
|
993
1221
|
}
|
|
994
1222
|
//# sourceMappingURL=workspace.js.map
|