@lesliechan721/aw-plugin-core 0.1.0-beta.6 → 0.1.0-beta.61

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