@lesliechan721/aw-plugin-core 0.1.0-beta.7 → 0.1.0-beta.8
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 +105 -0
- package/dist/aw-capabilities.js.map +1 -0
- package/dist/catalog-workspace-resolution.d.ts +10 -0
- package/dist/catalog-workspace-resolution.js +103 -0
- package/dist/catalog-workspace-resolution.js.map +1 -0
- package/dist/generated/{plugin-manifest-v1.d.ts → plugin-manifest-v2.d.ts} +31 -31
- package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v2.js} +1 -1
- package/dist/generated/plugin-manifest-v2.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest-contract.d.ts +3 -6
- package/dist/manifest-contract.js +34 -62
- package/dist/manifest-contract.js.map +1 -1
- package/dist/manifest-type-contract.d.ts +2 -2
- package/dist/manifest.d.ts +8 -8
- package/dist/manifest.js +9 -7
- package/dist/manifest.js.map +1 -1
- package/dist/plugin-authoring-capabilities.js +2 -2
- package/dist/plugin-authoring-capabilities.js.map +1 -1
- package/dist/plugin-release.d.ts +3 -3
- package/dist/plugin-requirements.d.ts +33 -0
- package/dist/plugin-requirements.js +388 -0
- package/dist/plugin-requirements.js.map +1 -0
- package/dist/plugin-script-contract.d.ts +8 -0
- package/dist/plugin-script-contract.js +38 -1
- package/dist/plugin-script-contract.js.map +1 -1
- package/dist/types.d.ts +18 -19
- package/dist/workspace.d.ts +20 -4
- package/dist/workspace.js +352 -58
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
- package/schemas/{plugin-manifest-v1.json → plugin-manifest-v2.json} +14 -8
- package/dist/generated/plugin-manifest-v1.js.map +0 -1
package/dist/workspace.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
5
|
import { assertCatalogVersion, assertStrictSemVer, compareStrictSemVer, MARKETPLACE_SOURCE_PATH, SOURCE_MANIFEST_PATH, createMarketplaceManifest, createMarketplaceManifestEntry, normalizeCatalogManifestBase, normalizeCatalogMarketplaceIdentity, normalizeCatalogManifestEntries, normalizeManifestDigestInput, readMarketplaceSourceMeta, readPluginManifest, validatePlatformManifestProjection, } from './manifest-contract.js';
|
|
6
6
|
import { resolveSupportedPlatforms } from './platform-support.js';
|
|
7
|
+
import { createAwPluginRequirementsReceipt, normalizeAwCapabilities, parseAwPluginRequirementsReceipt, } from './aw-capabilities.js';
|
|
7
8
|
import { appendCatalogToPluginReleaseLedger, assertStablePluginVersion, PLUGIN_RELEASE_LEDGER_FILENAME, PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION, resolveBetaPluginVersionOverrides, validatePluginReleaseLedgerAgainstManifest, } from './plugin-release.js';
|
|
8
9
|
export const DEFAULT_CATALOG_WORKSPACE_VERSION = '0.1.0';
|
|
9
10
|
export const DEFAULT_CATALOG_OUTPUT_PATH = path.join('.aw', '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 !== '..'
|
|
@@ -406,7 +408,7 @@ function publishedContentDigest(manifest) {
|
|
|
406
408
|
owner: manifest.marketplace.owner,
|
|
407
409
|
interface: manifest.marketplace.interface,
|
|
408
410
|
},
|
|
409
|
-
|
|
411
|
+
requires: manifest.requires,
|
|
410
412
|
plugins: manifest.entries.map((entry) => ({
|
|
411
413
|
name: entry.name,
|
|
412
414
|
targetVersion: entry.version.replace(/-beta\.[1-9]\d*$/u, ''),
|
|
@@ -414,13 +416,54 @@ function publishedContentDigest(manifest) {
|
|
|
414
416
|
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
415
417
|
}));
|
|
416
418
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
419
|
+
const SOURCE_IDENTITY_EXCLUDED_DIRECTORIES = new Set([
|
|
420
|
+
'.git', '.aw', 'node_modules', 'evals', 'reports',
|
|
421
|
+
]);
|
|
422
|
+
const SOURCE_IDENTITY_EXCLUDED_FILES = new Set([
|
|
423
|
+
'.DS_Store', 'AGENTS.md', 'CLAUDE.md',
|
|
424
|
+
]);
|
|
425
|
+
async function sourceIdentityRecords(root, relative = '') {
|
|
426
|
+
const records = [];
|
|
427
|
+
for (const entry of (await fs.readdir(path.join(root, relative), { withFileTypes: true }))
|
|
428
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
429
|
+
if (SOURCE_IDENTITY_EXCLUDED_DIRECTORIES.has(entry.name) || SOURCE_IDENTITY_EXCLUDED_FILES.has(entry.name)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const child = relative ? path.posix.join(relative, entry.name) : entry.name;
|
|
433
|
+
const filePath = path.join(root, child);
|
|
434
|
+
const stat = await fs.lstat(filePath);
|
|
435
|
+
if (stat.isSymbolicLink())
|
|
436
|
+
throw new Error(`Catalog source identity does not accept symbolic links: ${filePath}`);
|
|
437
|
+
if (stat.isDirectory())
|
|
438
|
+
records.push(...await sourceIdentityRecords(root, child));
|
|
439
|
+
else if (stat.isFile()) {
|
|
440
|
+
const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
|
|
441
|
+
records.push(`${child}\0${mode}\0${sha256(await fs.readFile(filePath))}`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
throw new Error(`Catalog source identity only accepts regular files and directories: ${filePath}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return records;
|
|
448
|
+
}
|
|
449
|
+
export async function calculateCatalogWorkspaceSourceIdentity(workspace) {
|
|
450
|
+
const roots = [workspace.metadataPath, ...workspace.plugins.map((plugin) => plugin.rootDir)];
|
|
451
|
+
const records = [];
|
|
452
|
+
for (const sourceRoot of roots.sort((left, right) => left.localeCompare(right))) {
|
|
453
|
+
const relativeRoot = path.relative(workspace.workspaceRoot, sourceRoot).split(path.sep).join('/');
|
|
454
|
+
const stat = await fs.lstat(sourceRoot);
|
|
455
|
+
if (stat.isSymbolicLink())
|
|
456
|
+
throw new Error(`Catalog source identity does not accept symbolic links: ${sourceRoot}`);
|
|
457
|
+
if (stat.isFile()) {
|
|
458
|
+
const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
|
|
459
|
+
records.push(`${relativeRoot}\0${mode}\0${sha256(await fs.readFile(sourceRoot))}`);
|
|
460
|
+
}
|
|
461
|
+
else if (stat.isDirectory()) {
|
|
462
|
+
for (const record of await sourceIdentityRecords(sourceRoot))
|
|
463
|
+
records.push(`${relativeRoot}/${record}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return sha256(records.sort().join('\n'));
|
|
424
467
|
}
|
|
425
468
|
function generatedOutputMarker(options) {
|
|
426
469
|
return {
|
|
@@ -451,9 +494,26 @@ async function catalogPathRecord(root, relative, versionNeutral) {
|
|
|
451
494
|
if (!stat.isFile())
|
|
452
495
|
throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
|
|
453
496
|
const manifests = new Set(['.agents-plugin/plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json']);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
497
|
+
let digest;
|
|
498
|
+
if (versionNeutral && manifests.has(relative)) {
|
|
499
|
+
digest = sha256(stableStringify({
|
|
500
|
+
...await fs.readJson(filePath),
|
|
501
|
+
version: '<plugin-version>',
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
else if (versionNeutral && path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME) {
|
|
505
|
+
const receipt = await fs.readJson(filePath);
|
|
506
|
+
const plugin = receipt.plugin && typeof receipt.plugin === 'object' && !Array.isArray(receipt.plugin)
|
|
507
|
+
? receipt.plugin
|
|
508
|
+
: {};
|
|
509
|
+
digest = sha256(stableStringify({
|
|
510
|
+
...receipt,
|
|
511
|
+
plugin: { ...plugin, version: '<plugin-version>' },
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
digest = sha256(await fs.readFile(filePath));
|
|
516
|
+
}
|
|
457
517
|
return { path: relative, kind: 'file', mode: (stat.mode & 0o100) === 0 ? '100644' : '100755', digest };
|
|
458
518
|
}
|
|
459
519
|
async function calculateEntryDigest(root, versionNeutral) {
|
|
@@ -461,15 +521,43 @@ async function calculateEntryDigest(root, versionNeutral) {
|
|
|
461
521
|
return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral)))));
|
|
462
522
|
}
|
|
463
523
|
async function expectedCatalogEntry(plugin, pluginRoot) {
|
|
524
|
+
const bundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} bundle manifest`);
|
|
464
525
|
return {
|
|
465
526
|
...createMarketplaceManifestEntry(plugin),
|
|
466
527
|
version: plugin.manifest.version,
|
|
467
|
-
|
|
528
|
+
dependencies: [...(plugin.manifest.dependencies ?? [])].sort(),
|
|
529
|
+
requires: {
|
|
530
|
+
aw: {
|
|
531
|
+
capabilities: normalizeAwCapabilities(plugin.manifest.requires?.aw.capabilities ?? []),
|
|
532
|
+
},
|
|
533
|
+
},
|
|
468
534
|
artifactDigest: await calculateEntryDigest(pluginRoot, false),
|
|
469
|
-
manifestDigest: sha256(normalizeManifestDigestInput(
|
|
535
|
+
manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
|
|
470
536
|
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
471
537
|
};
|
|
472
538
|
}
|
|
539
|
+
async function validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot) {
|
|
540
|
+
const files = await listBundleFiles(pluginRoot);
|
|
541
|
+
const expectedReceiptPaths = new Set(files.flatMap((relative) => /^(?:claude|codex|agent-skills)\/skills\/[^/]+\/SKILL\.md$/u.test(relative)
|
|
542
|
+
? [path.posix.join(path.posix.dirname(relative), AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)]
|
|
543
|
+
: []));
|
|
544
|
+
const actualReceiptPaths = files.filter((relative) => path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME);
|
|
545
|
+
for (const relative of expectedReceiptPaths) {
|
|
546
|
+
if (!actualReceiptPaths.includes(relative)) {
|
|
547
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} Skill root is missing ${AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME}: ${relative}.`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const expected = createAwPluginRequirementsReceipt({ name: plugin.manifest.name, version: plugin.manifest.version }, plugin.manifest.requires?.aw.capabilities ?? []);
|
|
551
|
+
for (const relative of actualReceiptPaths) {
|
|
552
|
+
if (!expectedReceiptPaths.has(relative)) {
|
|
553
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a reserved runtime requirements receipt outside a Skill root: ${relative}.`);
|
|
554
|
+
}
|
|
555
|
+
const receipt = parseAwPluginRequirementsReceipt(await readRegularJson(path.join(pluginRoot, ...relative.split('/')), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} runtime requirements receipt`));
|
|
556
|
+
if (!isDeepStrictEqual(receipt, expected)) {
|
|
557
|
+
throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a mismatched runtime requirements receipt: ${relative}.`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
473
561
|
function normalizeCatalogRelease(raw, catalogVersion) {
|
|
474
562
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
475
563
|
throw new Error('Invalid catalog manifest release: expected an object.');
|
|
@@ -521,7 +609,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
521
609
|
const catalogVersion = catalogWorkspace.metadata.catalog.version;
|
|
522
610
|
const channel = deriveCatalogChannel(catalogVersion);
|
|
523
611
|
const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
|
|
524
|
-
const identity =
|
|
612
|
+
const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
|
|
525
613
|
const marker = generatedOutputMarker({
|
|
526
614
|
kind: 'catalog',
|
|
527
615
|
workspaceIdentity: catalogWorkspace.workspaceIdentity,
|
|
@@ -532,6 +620,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
532
620
|
force: options.force === true,
|
|
533
621
|
build: async (stagingRoot) => {
|
|
534
622
|
const bundledPlugins = [];
|
|
623
|
+
await fs.ensureDir(path.join(stagingRoot, 'plugins'));
|
|
535
624
|
for (const plugin of plugins) {
|
|
536
625
|
const closure = dependencyClosure(plugin, plugins);
|
|
537
626
|
const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
|
|
@@ -550,20 +639,14 @@ export async function buildCatalogWorkspace(options) {
|
|
|
550
639
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
|
|
551
640
|
await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('codex')), { spaces: 2 });
|
|
552
641
|
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
642
|
const entries = await Promise.all([...bundledPlugins]
|
|
560
643
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
561
644
|
.map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
|
|
562
645
|
const manifest = {
|
|
563
|
-
schemaVersion:
|
|
646
|
+
schemaVersion: 5,
|
|
564
647
|
catalogVersion,
|
|
565
648
|
channel,
|
|
566
|
-
|
|
649
|
+
requires: { aw: { capabilities: ['aw.catalog-snapshot.v5'] } },
|
|
567
650
|
marketplace: {
|
|
568
651
|
owner: catalogWorkspace.metadata.owner,
|
|
569
652
|
interface: catalogWorkspace.metadata.interface,
|
|
@@ -574,9 +657,9 @@ export async function buildCatalogWorkspace(options) {
|
|
|
574
657
|
await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
|
|
575
658
|
await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
|
|
576
659
|
},
|
|
577
|
-
validate: async (stagingRoot) => { await
|
|
660
|
+
validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
|
|
578
661
|
assertReplaceable: async (existingRoot) => {
|
|
579
|
-
await
|
|
662
|
+
await validateCatalogSnapshotComplete(existingRoot);
|
|
580
663
|
await assertGeneratedOutput(existingRoot, marker);
|
|
581
664
|
},
|
|
582
665
|
});
|
|
@@ -596,10 +679,21 @@ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
|
|
|
596
679
|
const manifest = await fs.readJson(manifestPath);
|
|
597
680
|
await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
|
|
598
681
|
}
|
|
682
|
+
const pluginRoot = path.join(snapshotRoot, 'plugins', pluginName);
|
|
683
|
+
for (const relative of await listBundleFiles(pluginRoot)) {
|
|
684
|
+
if (path.posix.basename(relative) !== AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)
|
|
685
|
+
continue;
|
|
686
|
+
const receiptPath = path.join(pluginRoot, ...relative.split('/'));
|
|
687
|
+
const receipt = parseAwPluginRequirementsReceipt(await fs.readJson(receiptPath));
|
|
688
|
+
if (receipt.plugin.name !== pluginName) {
|
|
689
|
+
throw new Error(`Catalog plugin ${JSON.stringify(pluginName)} has a mismatched runtime requirements receipt.`);
|
|
690
|
+
}
|
|
691
|
+
await fs.writeFile(receiptPath, `${JSON.stringify(createAwPluginRequirementsReceipt({ name: pluginName, version }, receipt.requiredCapabilities), null, 2)}\n`);
|
|
692
|
+
}
|
|
599
693
|
}
|
|
600
694
|
}
|
|
601
695
|
export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
|
|
602
|
-
const validation = await
|
|
696
|
+
const validation = await validateCatalogSnapshotComplete(snapshotDirectory);
|
|
603
697
|
if (validation.manifest.release.state !== 'published') {
|
|
604
698
|
throw new Error('Published Catalog snapshot must have release.state published.');
|
|
605
699
|
}
|
|
@@ -618,10 +712,10 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
618
712
|
const catalogVersion = catalogWorkspace.metadata.catalog.version;
|
|
619
713
|
const channel = deriveCatalogChannel(catalogVersion);
|
|
620
714
|
const previous = options.previousSnapshotRoot
|
|
621
|
-
? await
|
|
715
|
+
? await validatePublishedCatalogPublicationParent(options.previousSnapshotRoot)
|
|
622
716
|
: null;
|
|
623
|
-
if (previous && compareStrictSemVer(catalogVersion, previous.
|
|
624
|
-
throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.
|
|
717
|
+
if (previous && compareStrictSemVer(catalogVersion, previous.catalogVersion) <= 0) {
|
|
718
|
+
throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.catalogVersion}.`);
|
|
625
719
|
}
|
|
626
720
|
const previousLedger = previous?.ledger ?? {
|
|
627
721
|
schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
|
|
@@ -639,7 +733,7 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
639
733
|
reservedImports: options.reservedImports,
|
|
640
734
|
});
|
|
641
735
|
temporaryRoot = built.outputRoot;
|
|
642
|
-
const development = await
|
|
736
|
+
const development = await validateCatalogSnapshotComplete(temporaryRoot);
|
|
643
737
|
const contentDigests = new Map(development.manifest.entries.map((entry) => [entry.name, entry.contentDigest]));
|
|
644
738
|
const overrides = channel === 'beta'
|
|
645
739
|
? resolveBetaPluginVersionOverrides({
|
|
@@ -828,7 +922,7 @@ async function digestDirectory(root) {
|
|
|
828
922
|
await walk(root);
|
|
829
923
|
return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
|
|
830
924
|
}
|
|
831
|
-
|
|
925
|
+
async function validateCatalogSnapshotTransportInternal(snapshotDirectory, options = {}) {
|
|
832
926
|
const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
|
|
833
927
|
const allowed = new Set([
|
|
834
928
|
CATALOG_MANIFEST_FILENAME,
|
|
@@ -876,16 +970,20 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
876
970
|
await readRegularJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'), 'Codex marketplace');
|
|
877
971
|
await readRegularJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'), 'Claude marketplace');
|
|
878
972
|
const snapshotDigest = await digestDirectory(rootDir);
|
|
879
|
-
const raw =
|
|
973
|
+
const raw = options.rawManifest
|
|
974
|
+
?? await fs.readJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME));
|
|
880
975
|
const allowedManifestKeys = new Set([
|
|
881
|
-
'schemaVersion', 'catalogVersion', 'channel', '
|
|
976
|
+
'schemaVersion', 'catalogVersion', 'channel', 'requires', 'marketplace', 'release', 'entries',
|
|
882
977
|
]);
|
|
883
978
|
for (const key of Object.keys(raw))
|
|
884
979
|
if (!allowedManifestKeys.has(key))
|
|
885
980
|
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
|
|
981
|
+
if (raw.schemaVersion !== 5)
|
|
982
|
+
throw new Error('Invalid catalog manifest schemaVersion: expected 5.');
|
|
983
|
+
const base = normalizeCatalogManifestBase(raw, 'catalog manifest v5');
|
|
984
|
+
if (!(base.requires.aw.capabilities ?? []).includes('aw.catalog-snapshot.v5')) {
|
|
985
|
+
throw new Error('Invalid catalog manifest requires.aw.capabilities: expected aw.catalog-snapshot.v5.');
|
|
986
|
+
}
|
|
889
987
|
const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
|
|
890
988
|
if (!Array.isArray(raw.entries))
|
|
891
989
|
throw new Error('Invalid catalog manifest entries: expected an array.');
|
|
@@ -901,7 +999,7 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
901
999
|
delete copy.contentDigest;
|
|
902
1000
|
return copy;
|
|
903
1001
|
});
|
|
904
|
-
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest
|
|
1002
|
+
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v5')
|
|
905
1003
|
.map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
|
|
906
1004
|
const pluginNames = normalizedEntries.map((entry, index) => {
|
|
907
1005
|
if (typeof entry.name !== 'string')
|
|
@@ -915,39 +1013,61 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
915
1013
|
const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
|
|
916
1014
|
if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
|
|
917
1015
|
throw new Error('Invalid catalog snapshot plugin inventory.');
|
|
918
|
-
const
|
|
1016
|
+
const platformEntries = {
|
|
1017
|
+
claude: [],
|
|
1018
|
+
codex: [],
|
|
1019
|
+
};
|
|
919
1020
|
for (const [index, name] of pluginNames.entries()) {
|
|
920
1021
|
const pluginRoot = path.join(rootDir, 'plugins', name);
|
|
921
|
-
|
|
1022
|
+
await assertDirectoryWithoutSymlinks(pluginRoot, `Catalog plugin ${JSON.stringify(name)}`);
|
|
1023
|
+
const entry = normalizedEntries[index];
|
|
1024
|
+
if (entry.source !== `./plugins/${name}`) {
|
|
1025
|
+
throw new Error(`Catalog entry source for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1026
|
+
}
|
|
1027
|
+
const rawBundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(name)} bundle manifest`);
|
|
1028
|
+
const expectedDigests = {
|
|
1029
|
+
artifactDigest: await calculateEntryDigest(pluginRoot, false),
|
|
1030
|
+
manifestDigest: sha256(normalizeManifestDigestInput(rawBundleManifest)),
|
|
1031
|
+
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
1032
|
+
};
|
|
1033
|
+
for (const field of Object.keys(expectedDigests)) {
|
|
1034
|
+
// V4 used a normalized Manifest v1 digest. The artifact digest still binds the raw manifest bytes,
|
|
1035
|
+
// and the release ledger must still match the preserved v4 manifestDigest.
|
|
1036
|
+
if (field === 'manifestDigest' && options.legacyV4PublicationParent)
|
|
1037
|
+
continue;
|
|
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
|
+
}
|
|
922
1042
|
for (const platform of ['claude', 'codex']) {
|
|
923
1043
|
const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
924
1044
|
const exists = await fs.pathExists(platformManifest);
|
|
925
|
-
if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
|
|
926
|
-
throw new Error(`Catalog plugin ${JSON.stringify(name)} has an inconsistent ${platform} bundle projection.`);
|
|
927
|
-
}
|
|
928
1045
|
if (exists) {
|
|
929
|
-
|
|
930
|
-
|
|
1046
|
+
await readRegularJson(platformManifest, `${platform} plugin manifest`);
|
|
1047
|
+
platformEntries[platform].push(entry);
|
|
931
1048
|
}
|
|
932
1049
|
}
|
|
933
|
-
const expected = await expectedCatalogEntry(plugin, pluginRoot);
|
|
934
|
-
for (const field of Object.keys(expected)) {
|
|
935
|
-
if (!isDeepStrictEqual(normalizedEntries[index][field], expected[field])) {
|
|
936
|
-
throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
plugins.push(plugin);
|
|
940
1050
|
}
|
|
941
|
-
assertDependencyGraph(plugins);
|
|
942
1051
|
const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
|
|
943
1052
|
const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
|
|
944
|
-
const
|
|
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',
|
|
945
1065
|
owner: marketplaceIdentity.owner,
|
|
946
1066
|
interface: marketplaceIdentity.interface,
|
|
947
|
-
|
|
948
|
-
};
|
|
949
|
-
const expectedAgents =
|
|
950
|
-
const expectedClaude =
|
|
1067
|
+
plugins: platformEntries[platform].map(marketplaceEntry),
|
|
1068
|
+
});
|
|
1069
|
+
const expectedAgents = expectedMarketplace('codex');
|
|
1070
|
+
const expectedClaude = expectedMarketplace('claude');
|
|
951
1071
|
const normalizeMarketplaceOrder = (marketplace) => {
|
|
952
1072
|
const canonical = JSON.parse(JSON.stringify(marketplace));
|
|
953
1073
|
return {
|
|
@@ -965,10 +1085,10 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
965
1085
|
}
|
|
966
1086
|
const release = normalizeCatalogRelease(raw.release, base.catalogVersion);
|
|
967
1087
|
const manifest = {
|
|
968
|
-
schemaVersion:
|
|
1088
|
+
schemaVersion: 5,
|
|
969
1089
|
catalogVersion: base.catalogVersion,
|
|
970
1090
|
channel: base.channel,
|
|
971
|
-
|
|
1091
|
+
requires: base.requires,
|
|
972
1092
|
marketplace: marketplaceIdentity,
|
|
973
1093
|
release,
|
|
974
1094
|
entries: normalizedEntries,
|
|
@@ -991,4 +1111,178 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
991
1111
|
: undefined;
|
|
992
1112
|
return { rootDir, manifest, ...(ledger ? { ledger } : {}), snapshotDigest };
|
|
993
1113
|
}
|
|
1114
|
+
export async function validateCatalogSnapshotTransport(snapshotDirectory) {
|
|
1115
|
+
return validateCatalogSnapshotTransportInternal(snapshotDirectory);
|
|
1116
|
+
}
|
|
1117
|
+
function normalizeLegacyCatalogCompatibility(value, fieldName) {
|
|
1118
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1119
|
+
throw new Error(`Invalid ${fieldName}: expected an object.`);
|
|
1120
|
+
}
|
|
1121
|
+
const compatibility = value;
|
|
1122
|
+
if (!isDeepStrictEqual(Object.keys(compatibility), ['aw'])) {
|
|
1123
|
+
throw new Error(`Invalid ${fieldName}: unknown field.`);
|
|
1124
|
+
}
|
|
1125
|
+
if (!compatibility.aw || typeof compatibility.aw !== 'object' || Array.isArray(compatibility.aw)) {
|
|
1126
|
+
throw new Error(`Invalid ${fieldName}.aw: expected an object.`);
|
|
1127
|
+
}
|
|
1128
|
+
const aw = compatibility.aw;
|
|
1129
|
+
if (!isDeepStrictEqual(Object.keys(aw), ['minVersion'])) {
|
|
1130
|
+
throw new Error(`Invalid ${fieldName}.aw: unknown field.`);
|
|
1131
|
+
}
|
|
1132
|
+
return { aw: { minVersion: assertStrictSemVer(aw.minVersion, `${fieldName}.aw.minVersion`) } };
|
|
1133
|
+
}
|
|
1134
|
+
function normalizeLegacyPublishedCatalogManifestV4(raw) {
|
|
1135
|
+
const allowedManifestKeys = new Set([
|
|
1136
|
+
'schemaVersion', 'catalogVersion', 'channel', 'compatibility', 'marketplace', 'release', 'entries',
|
|
1137
|
+
]);
|
|
1138
|
+
for (const key of Object.keys(raw)) {
|
|
1139
|
+
if (!allowedManifestKeys.has(key))
|
|
1140
|
+
throw new Error(`Invalid legacy catalog manifest: unknown field ${JSON.stringify(key)}.`);
|
|
1141
|
+
}
|
|
1142
|
+
if (raw.schemaVersion !== 4)
|
|
1143
|
+
throw new Error('Invalid legacy catalog manifest schemaVersion: expected 4.');
|
|
1144
|
+
const catalogVersion = assertCatalogVersion(raw.catalogVersion, 'legacy catalog manifest v4.catalogVersion');
|
|
1145
|
+
if (raw.channel !== 'latest' && raw.channel !== 'beta') {
|
|
1146
|
+
throw new Error('Invalid legacy catalog manifest v4.channel: expected "latest" or "beta".');
|
|
1147
|
+
}
|
|
1148
|
+
const channel = raw.channel;
|
|
1149
|
+
const isBetaVersion = catalogVersion.includes('-beta.');
|
|
1150
|
+
if ((channel === 'latest' && isBetaVersion) || (channel === 'beta' && !isBetaVersion)) {
|
|
1151
|
+
throw new Error('Invalid legacy catalog manifest v4 catalogVersion channel.');
|
|
1152
|
+
}
|
|
1153
|
+
const compatibility = normalizeLegacyCatalogCompatibility(raw.compatibility, 'legacy catalog manifest v4.compatibility');
|
|
1154
|
+
const marketplace = normalizeCatalogMarketplaceIdentity(raw.marketplace, 'legacy catalog manifest v4.marketplace');
|
|
1155
|
+
if (!Array.isArray(raw.entries))
|
|
1156
|
+
throw new Error('Invalid legacy catalog manifest v4.entries: expected an array.');
|
|
1157
|
+
const allowedEntryKeys = new Set([
|
|
1158
|
+
'name', 'version', 'description', 'interface', 'category', 'tags', 'strict', 'policy',
|
|
1159
|
+
'compatibility', 'source', 'artifactDigest', 'manifestDigest', 'contentDigest',
|
|
1160
|
+
]);
|
|
1161
|
+
const translatedEntryInputs = raw.entries.map((value, index) => {
|
|
1162
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1163
|
+
throw new Error(`Invalid legacy catalog manifest v4.entries[${index}]: expected an object.`);
|
|
1164
|
+
}
|
|
1165
|
+
const entry = value;
|
|
1166
|
+
for (const key of Object.keys(entry)) {
|
|
1167
|
+
if (!allowedEntryKeys.has(key)) {
|
|
1168
|
+
throw new Error(`Invalid legacy catalog manifest v4.entries[${index}]: unknown field ${JSON.stringify(key)}.`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const entryCompatibility = normalizeLegacyCatalogCompatibility(entry.compatibility, `legacy catalog manifest v4.entries[${index}].compatibility`);
|
|
1172
|
+
if (compareStrictSemVer(entryCompatibility.aw.minVersion, compatibility.aw.minVersion) < 0) {
|
|
1173
|
+
throw new Error(`Invalid legacy catalog manifest v4.entries[${index}].compatibility.aw.minVersion.`);
|
|
1174
|
+
}
|
|
1175
|
+
const translated = { ...entry };
|
|
1176
|
+
delete translated.compatibility;
|
|
1177
|
+
delete translated.contentDigest;
|
|
1178
|
+
return {
|
|
1179
|
+
...translated,
|
|
1180
|
+
dependencies: [],
|
|
1181
|
+
requires: { aw: { capabilities: [] } },
|
|
1182
|
+
};
|
|
1183
|
+
});
|
|
1184
|
+
const translatedBase = normalizeCatalogManifestBase({
|
|
1185
|
+
catalogVersion,
|
|
1186
|
+
channel,
|
|
1187
|
+
requires: { aw: { capabilities: ['aw.catalog-snapshot.v5'] } },
|
|
1188
|
+
}, 'legacy catalog manifest v4 translation');
|
|
1189
|
+
const normalizedEntries = normalizeCatalogManifestEntries(translatedEntryInputs, translatedBase, 'legacy catalog manifest v4 translation').map((entry, index) => {
|
|
1190
|
+
const contentDigest = raw.entries[index].contentDigest;
|
|
1191
|
+
if (typeof contentDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(contentDigest)) {
|
|
1192
|
+
throw new Error(`Invalid legacy catalog manifest v4.entries[${index}].contentDigest.`);
|
|
1193
|
+
}
|
|
1194
|
+
return { ...entry, contentDigest };
|
|
1195
|
+
});
|
|
1196
|
+
const release = normalizeCatalogRelease(raw.release, catalogVersion);
|
|
1197
|
+
if (release.state !== 'published') {
|
|
1198
|
+
throw new Error('Legacy Catalog publication parent must have release.state published.');
|
|
1199
|
+
}
|
|
1200
|
+
const legacyContentDigest = sha256(stableStringify({
|
|
1201
|
+
schemaIdentity: { schemaVersion: 4 },
|
|
1202
|
+
marketplaceMetadata: { name: 'aw', owner: marketplace.owner, interface: marketplace.interface },
|
|
1203
|
+
compatibility,
|
|
1204
|
+
plugins: normalizedEntries.map((entry) => ({
|
|
1205
|
+
name: entry.name,
|
|
1206
|
+
targetVersion: entry.version.replace(/-beta\.[1-9]\d*$/u, ''),
|
|
1207
|
+
contentDigest: entry.contentDigest,
|
|
1208
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
1209
|
+
}));
|
|
1210
|
+
if (release.contentDigest !== legacyContentDigest) {
|
|
1211
|
+
throw new Error('Published legacy catalog release contentDigest does not match the snapshot.');
|
|
1212
|
+
}
|
|
1213
|
+
const translatedManifest = {
|
|
1214
|
+
schemaVersion: 5,
|
|
1215
|
+
catalogVersion,
|
|
1216
|
+
channel,
|
|
1217
|
+
requires: translatedBase.requires,
|
|
1218
|
+
marketplace,
|
|
1219
|
+
release,
|
|
1220
|
+
entries: normalizedEntries,
|
|
1221
|
+
};
|
|
1222
|
+
return {
|
|
1223
|
+
...translatedManifest,
|
|
1224
|
+
release: { ...release, contentDigest: publishedContentDigest(translatedManifest) },
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
export async function validatePublishedCatalogPublicationParent(snapshotDirectory) {
|
|
1228
|
+
const raw = await readRegularJson(path.join(snapshotDirectory, CATALOG_MANIFEST_FILENAME), 'Catalog publication parent manifest');
|
|
1229
|
+
const validation = raw.schemaVersion === 4
|
|
1230
|
+
? await validateCatalogSnapshotTransportInternal(snapshotDirectory, {
|
|
1231
|
+
rawManifest: normalizeLegacyPublishedCatalogManifestV4(raw),
|
|
1232
|
+
legacyV4PublicationParent: true,
|
|
1233
|
+
})
|
|
1234
|
+
: await validatePublishedCatalogSnapshot(snapshotDirectory);
|
|
1235
|
+
if (validation.manifest.release.state !== 'published') {
|
|
1236
|
+
throw new Error('Catalog publication parent must have release.state published.');
|
|
1237
|
+
}
|
|
1238
|
+
if (!validation.ledger)
|
|
1239
|
+
throw new Error('Catalog publication parent must include a plugin release ledger.');
|
|
1240
|
+
return {
|
|
1241
|
+
rootDir: validation.rootDir,
|
|
1242
|
+
catalogVersion: validation.manifest.catalogVersion,
|
|
1243
|
+
ledger: validation.ledger,
|
|
1244
|
+
snapshotDigest: validation.snapshotDigest,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
async function validateCatalogPluginEntriesComplete(validation, pluginNames) {
|
|
1248
|
+
const entriesByName = new Map(validation.manifest.entries.map((entry) => [entry.name, entry]));
|
|
1249
|
+
const plugins = [];
|
|
1250
|
+
for (const name of pluginNames) {
|
|
1251
|
+
const entry = entriesByName.get(name);
|
|
1252
|
+
if (!entry)
|
|
1253
|
+
throw new Error(`Catalog plugin ${JSON.stringify(name)} does not exist.`);
|
|
1254
|
+
const pluginRoot = path.join(validation.rootDir, 'plugins', entry.name);
|
|
1255
|
+
const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
|
|
1256
|
+
await validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot);
|
|
1257
|
+
for (const platform of ['claude', 'codex']) {
|
|
1258
|
+
const platformManifestPath = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
1259
|
+
const exists = await fs.pathExists(platformManifestPath);
|
|
1260
|
+
if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
|
|
1261
|
+
throw new Error(`Catalog plugin ${JSON.stringify(entry.name)} has an inconsistent ${platform} bundle projection.`);
|
|
1262
|
+
}
|
|
1263
|
+
if (exists) {
|
|
1264
|
+
const rawPlatformManifest = await readRegularJson(platformManifestPath, `${platform} plugin manifest`);
|
|
1265
|
+
await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const expected = await expectedCatalogEntry(plugin, pluginRoot);
|
|
1269
|
+
for (const field of Object.keys(expected)) {
|
|
1270
|
+
if (!isDeepStrictEqual(entry[field], expected[field])) {
|
|
1271
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
plugins.push(plugin);
|
|
1275
|
+
}
|
|
1276
|
+
assertDependencyGraph(plugins);
|
|
1277
|
+
}
|
|
1278
|
+
export async function validateCatalogSnapshotPluginEntriesComplete(snapshotDirectory, pluginNames) {
|
|
1279
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1280
|
+
await validateCatalogPluginEntriesComplete(validation, pluginNames);
|
|
1281
|
+
return validation;
|
|
1282
|
+
}
|
|
1283
|
+
export async function validateCatalogSnapshotComplete(snapshotDirectory) {
|
|
1284
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1285
|
+
await validateCatalogPluginEntriesComplete(validation, validation.manifest.entries.map((entry) => entry.name));
|
|
1286
|
+
return validation;
|
|
1287
|
+
}
|
|
994
1288
|
//# sourceMappingURL=workspace.js.map
|