@lesliechan721/aw-plugin-core 0.1.0-beta.7 → 0.1.0-beta.8

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