@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.
Files changed (49) hide show
  1. package/dist/aw-capabilities.d.ts +27 -0
  2. package/dist/aw-capabilities.js +114 -0
  3. package/dist/aw-capabilities.js.map +1 -0
  4. package/dist/catalog-file-modes.d.ts +5 -0
  5. package/dist/catalog-file-modes.js +67 -0
  6. package/dist/catalog-file-modes.js.map +1 -0
  7. package/dist/catalog-workspace-resolution.d.ts +3 -0
  8. package/dist/catalog-workspace-resolution.js +34 -0
  9. package/dist/catalog-workspace-resolution.js.map +1 -0
  10. package/dist/generated/plugin-manifest-v3.d.ts +1350 -0
  11. package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v3.js} +1 -1
  12. package/dist/generated/plugin-manifest-v3.js.map +1 -0
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +2 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/manifest-contract.d.ts +3 -6
  17. package/dist/manifest-contract.js +224 -398
  18. package/dist/manifest-contract.js.map +1 -1
  19. package/dist/manifest-type-contract.d.ts +2 -2
  20. package/dist/manifest.d.ts +10 -8
  21. package/dist/manifest.js +16 -7
  22. package/dist/manifest.js.map +1 -1
  23. package/dist/plugin-authoring-capabilities.d.ts +1 -1
  24. package/dist/plugin-authoring-capabilities.js +60 -26
  25. package/dist/plugin-authoring-capabilities.js.map +1 -1
  26. package/dist/plugin-config.d.ts +39 -0
  27. package/dist/plugin-config.js +1320 -0
  28. package/dist/plugin-config.js.map +1 -0
  29. package/dist/plugin-requirements.d.ts +34 -0
  30. package/dist/plugin-requirements.js +289 -0
  31. package/dist/plugin-requirements.js.map +1 -0
  32. package/dist/plugin-script-contract.d.ts +11 -12
  33. package/dist/plugin-script-contract.js +44 -71
  34. package/dist/plugin-script-contract.js.map +1 -1
  35. package/dist/runtime-config.d.ts +38 -0
  36. package/dist/runtime-config.js +31 -0
  37. package/dist/runtime-config.js.map +1 -0
  38. package/dist/types.d.ts +249 -61
  39. package/dist/workspace.d.ts +37 -9
  40. package/dist/workspace.js +419 -191
  41. package/dist/workspace.js.map +1 -1
  42. package/package.json +13 -4
  43. package/schemas/plugin-manifest-v3.json +2122 -0
  44. package/dist/generated/plugin-manifest-v1.d.ts +0 -368
  45. package/dist/generated/plugin-manifest-v1.js.map +0 -1
  46. package/dist/plugin-release.d.ts +0 -30
  47. package/dist/plugin-release.js +0 -187
  48. package/dist/plugin-release.js.map +0 -1
  49. 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 { assertCatalogVersion, assertStrictSemVer, compareStrictSemVer, MARKETPLACE_SOURCE_PATH, SOURCE_MANIFEST_PATH, createMarketplaceManifest, createMarketplaceManifestEntry, normalizeCatalogManifestBase, normalizeCatalogMarketplaceIdentity, normalizeCatalogManifestEntries, normalizeManifestDigestInput, readMarketplaceSourceMeta, readPluginManifest, validatePlatformManifestProjection, } from './manifest-contract.js';
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 { appendCatalogToPluginReleaseLedger, assertStablePluginVersion, PLUGIN_RELEASE_LEDGER_FILENAME, PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION, resolveBetaPluginVersionOverrides, validatePluginReleaseLedgerAgainstManifest, } from './plugin-release.js';
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
- const marker = validateCatalogWorkspaceMarker(await readRegularJson(markerPath, 'Catalog Workspace marker'));
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
- function publishedContentDigest(manifest) {
402
- return sha256(stableStringify({
403
- schemaIdentity: { schemaVersion: manifest.schemaVersion },
404
- marketplaceMetadata: {
405
- name: 'aw',
406
- owner: manifest.marketplace.owner,
407
- interface: manifest.marketplace.interface,
408
- },
409
- compatibility: manifest.compatibility,
410
- plugins: manifest.entries.map((entry) => ({
411
- name: entry.name,
412
- targetVersion: entry.version.replace(/-beta\.[1-9]\d*$/u, ''),
413
- contentDigest: entry.contentDigest,
414
- })).sort((left, right) => left.name.localeCompare(right.name)),
415
- }));
416
- }
417
- function sourceIdentity(metadata, plugins) {
418
- return sha256(stableStringify({
419
- metadata,
420
- plugins: [...plugins]
421
- .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
422
- .map((plugin) => plugin.manifest),
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 listBundleFiles(root, relative = '') {
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
- const digest = versionNeutral && manifests.has(relative)
455
- ? sha256(stableStringify({ ...await fs.readJson(filePath), version: '<plugin-version>' }))
456
- : sha256(await fs.readFile(filePath));
457
- return { path: relative, kind: 'file', mode: (stat.mode & 0o100) === 0 ? '100644' : '100755', digest };
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
- return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral)))));
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
- compatibility: plugin.manifest.compatibility,
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(plugin.manifest)),
518
+ manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
470
519
  contentDigest: await calculateEntryDigest(pluginRoot, true),
471
520
  };
472
521
  }
473
- function normalizeCatalogRelease(raw, catalogVersion) {
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', 'targetVersion', 'planId', 'sourceHead', 'contentDigest']);
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 = sourceIdentity(catalogWorkspace.metadata, plugins);
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: 4,
613
+ schemaVersion: 7,
564
614
  catalogVersion,
565
615
  channel,
566
- compatibility: { aw: { minVersion } },
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 validateCatalogSnapshot(stagingRoot); },
627
+ validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
578
628
  assertReplaceable: async (existingRoot) => {
579
- await validateCatalogSnapshot(existingRoot);
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 validateCatalogSnapshot(snapshotDirectory);
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
- if (!validation.ledger)
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 validateCatalogSnapshot(temporaryRoot);
643
- const contentDigests = new Map(development.manifest.entries.map((entry) => [entry.name, entry.contentDigest]));
644
- const overrides = channel === 'beta'
645
- ? resolveBetaPluginVersionOverrides({
646
- plugins: catalogWorkspace.plugins,
647
- contentDigests,
648
- ledger: previousLedger,
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
- const ledger = appendCatalogToPluginReleaseLedger({
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
- export async function validateCatalogSnapshot(snapshotDirectory) {
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
- const marker = await readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker');
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 raw = await fs.readJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME));
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', 'compatibility', 'marketplace', 'release', 'entries',
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 !== 4)
887
- throw new Error('Invalid catalog manifest schemaVersion: expected 4.');
888
- const base = normalizeCatalogManifestBase(raw, 'catalog manifest v4');
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 v4')
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 diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
916
- if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
917
- throw new Error('Invalid catalog snapshot plugin inventory.');
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 [index, name] of pluginNames.entries()) {
920
- const pluginRoot = path.join(rootDir, 'plugins', name);
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 platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
924
- const exists = await fs.pathExists(platformManifest);
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(platformManifest, `${platform} plugin manifest`);
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(normalizedEntries[index][field], expected[field])) {
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
- const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
943
- const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
944
- const marketplaceMetadata = {
945
- owner: marketplaceIdentity.owner,
946
- interface: marketplaceIdentity.interface,
947
- catalog: { version: base.catalogVersion },
948
- };
949
- const expectedAgents = createMarketplaceManifest(marketplaceMetadata, plugins.filter((plugin) => resolveSupportedPlatforms(plugin.manifest).has('codex')));
950
- const expectedClaude = createMarketplaceManifest(marketplaceMetadata, plugins.filter((plugin) => resolveSupportedPlatforms(plugin.manifest).has('claude')));
951
- const normalizeMarketplaceOrder = (marketplace) => {
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