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

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 (37) hide show
  1. package/dist/aw-capabilities.d.ts +27 -0
  2. package/dist/aw-capabilities.js +106 -0
  3. package/dist/aw-capabilities.js.map +1 -0
  4. package/dist/catalog-workspace-resolution.d.ts +3 -0
  5. package/dist/catalog-workspace-resolution.js +34 -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} +44 -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 -2
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/manifest-contract.d.ts +3 -6
  14. package/dist/manifest-contract.js +54 -63
  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 +4 -3
  21. package/dist/plugin-authoring-capabilities.js.map +1 -1
  22. package/dist/plugin-requirements.d.ts +34 -0
  23. package/dist/plugin-requirements.js +248 -0
  24. package/dist/plugin-requirements.js.map +1 -0
  25. package/dist/plugin-script-contract.d.ts +8 -0
  26. package/dist/plugin-script-contract.js +38 -1
  27. package/dist/plugin-script-contract.js.map +1 -1
  28. package/dist/types.d.ts +25 -22
  29. package/dist/workspace.d.ts +17 -9
  30. package/dist/workspace.js +299 -138
  31. package/dist/workspace.js.map +1 -1
  32. package/package.json +2 -2
  33. package/schemas/{plugin-manifest-v1.json → plugin-manifest-v2.json} +41 -11
  34. package/dist/generated/plugin-manifest-v1.js.map +0 -1
  35. package/dist/plugin-release.d.ts +0 -30
  36. package/dist/plugin-release.js +0 -187
  37. package/dist/plugin-release.js.map +0 -1
package/dist/workspace.js CHANGED
@@ -2,9 +2,9 @@ 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 { assertCatalogVersion, assertStrictSemVer, compareStrictSemVer, parseStrictSemVer, 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 { appendCatalogToPluginReleaseLedger, assertStablePluginVersion, PLUGIN_RELEASE_LEDGER_FILENAME, PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION, resolveBetaPluginVersionOverrides, validatePluginReleaseLedgerAgainstManifest, } from './plugin-release.js';
7
+ import { createAwPluginRequirementsReceipt, normalizeAwCapabilities, parseAwPluginRequirementsReceipt, } from './aw-capabilities.js';
8
8
  export const DEFAULT_CATALOG_WORKSPACE_VERSION = '0.1.0';
9
9
  export const DEFAULT_CATALOG_OUTPUT_PATH = path.join('.aw', 'catalog');
10
10
  export const DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH = path.join('.aw', 'publish', 'catalog');
@@ -12,6 +12,7 @@ export const DEFAULT_PLUGIN_OUTPUT_PATH = path.join('.aw', 'plugins');
12
12
  export const CATALOG_MANIFEST_FILENAME = 'catalog-manifest.json';
13
13
  export const GENERATED_OUTPUT_MARKER_FILENAME = '.aw-generated.json';
14
14
  export const CATALOG_WORKSPACE_MARKER_FILENAME = '.aw-workspace.json';
15
+ const AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME = 'aw-plugin-requirements.json';
15
16
  function isInside(root, candidate) {
16
17
  const relative = path.relative(root, candidate);
17
18
  return relative === '' || (relative !== '..'
@@ -138,9 +139,18 @@ function validateCatalogWorkspaceMarker(value) {
138
139
  }
139
140
  return { workspaceIdentity: marker.workspaceIdentity };
140
141
  }
141
- async function readCatalogWorkspaceMarker(workspaceRoot) {
142
+ export async function readCatalogWorkspaceMarker(workspaceRoot) {
142
143
  const markerPath = path.join(workspaceRoot, CATALOG_WORKSPACE_MARKER_FILENAME);
143
- const marker = validateCatalogWorkspaceMarker(await readRegularJson(markerPath, 'Catalog Workspace marker'));
144
+ let value;
145
+ try {
146
+ value = await readRegularJson(markerPath, 'Catalog Workspace marker');
147
+ }
148
+ catch (error) {
149
+ if (error instanceof SyntaxError)
150
+ throw new Error('Invalid Catalog Workspace marker.');
151
+ throw error;
152
+ }
153
+ const marker = validateCatalogWorkspaceMarker(value);
144
154
  return { markerPath, workspaceIdentity: marker.workspaceIdentity };
145
155
  }
146
156
  async function readRegularJson(filePath, label) {
@@ -398,29 +408,54 @@ function stableStringify(value) {
398
408
  function sha256(value) {
399
409
  return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
400
410
  }
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
- }));
411
+ const SOURCE_IDENTITY_EXCLUDED_DIRECTORIES = new Set([
412
+ '.git', '.aw', 'node_modules', 'evals', 'reports',
413
+ ]);
414
+ const SOURCE_IDENTITY_EXCLUDED_FILES = new Set([
415
+ '.DS_Store', 'AGENTS.md', 'CLAUDE.md',
416
+ ]);
417
+ async function sourceIdentityRecords(root, relative = '') {
418
+ const records = [];
419
+ for (const entry of (await fs.readdir(path.join(root, relative), { withFileTypes: true }))
420
+ .sort((left, right) => left.name.localeCompare(right.name))) {
421
+ if (SOURCE_IDENTITY_EXCLUDED_DIRECTORIES.has(entry.name) || SOURCE_IDENTITY_EXCLUDED_FILES.has(entry.name)) {
422
+ continue;
423
+ }
424
+ const child = relative ? path.posix.join(relative, entry.name) : entry.name;
425
+ const filePath = path.join(root, child);
426
+ const stat = await fs.lstat(filePath);
427
+ if (stat.isSymbolicLink())
428
+ throw new Error(`Catalog source identity does not accept symbolic links: ${filePath}`);
429
+ if (stat.isDirectory())
430
+ records.push(...await sourceIdentityRecords(root, child));
431
+ else if (stat.isFile()) {
432
+ const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
433
+ records.push(`${child}\0${mode}\0${sha256(await fs.readFile(filePath))}`);
434
+ }
435
+ else {
436
+ throw new Error(`Catalog source identity only accepts regular files and directories: ${filePath}`);
437
+ }
438
+ }
439
+ return records;
440
+ }
441
+ export async function calculateCatalogWorkspaceSourceIdentity(workspace) {
442
+ const roots = [workspace.metadataPath, ...workspace.plugins.map((plugin) => plugin.rootDir)];
443
+ const records = [];
444
+ for (const sourceRoot of roots.sort((left, right) => left.localeCompare(right))) {
445
+ const relativeRoot = path.relative(workspace.workspaceRoot, sourceRoot).split(path.sep).join('/');
446
+ const stat = await fs.lstat(sourceRoot);
447
+ if (stat.isSymbolicLink())
448
+ throw new Error(`Catalog source identity does not accept symbolic links: ${sourceRoot}`);
449
+ if (stat.isFile()) {
450
+ const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
451
+ records.push(`${relativeRoot}\0${mode}\0${sha256(await fs.readFile(sourceRoot))}`);
452
+ }
453
+ else if (stat.isDirectory()) {
454
+ for (const record of await sourceIdentityRecords(sourceRoot))
455
+ records.push(`${relativeRoot}/${record}`);
456
+ }
457
+ }
458
+ return sha256(records.sort().join('\n'));
424
459
  }
425
460
  function generatedOutputMarker(options) {
426
461
  return {
@@ -451,9 +486,26 @@ async function catalogPathRecord(root, relative, versionNeutral) {
451
486
  if (!stat.isFile())
452
487
  throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
453
488
  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));
489
+ let digest;
490
+ if (versionNeutral && manifests.has(relative)) {
491
+ digest = sha256(stableStringify({
492
+ ...await fs.readJson(filePath),
493
+ version: '<plugin-version>',
494
+ }));
495
+ }
496
+ else if (versionNeutral && path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME) {
497
+ const receipt = await fs.readJson(filePath);
498
+ const plugin = receipt.plugin && typeof receipt.plugin === 'object' && !Array.isArray(receipt.plugin)
499
+ ? receipt.plugin
500
+ : {};
501
+ digest = sha256(stableStringify({
502
+ ...receipt,
503
+ plugin: { ...plugin, version: '<plugin-version>' },
504
+ }));
505
+ }
506
+ else {
507
+ digest = sha256(await fs.readFile(filePath));
508
+ }
457
509
  return { path: relative, kind: 'file', mode: (stat.mode & 0o100) === 0 ? '100644' : '100755', digest };
458
510
  }
459
511
  async function calculateEntryDigest(root, versionNeutral) {
@@ -461,16 +513,44 @@ async function calculateEntryDigest(root, versionNeutral) {
461
513
  return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral)))));
462
514
  }
463
515
  async function expectedCatalogEntry(plugin, pluginRoot) {
516
+ const bundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} bundle manifest`);
464
517
  return {
465
518
  ...createMarketplaceManifestEntry(plugin),
466
519
  version: plugin.manifest.version,
467
- compatibility: plugin.manifest.compatibility,
520
+ dependencies: [...(plugin.manifest.dependencies ?? [])].sort(),
521
+ requires: {
522
+ aw: {
523
+ capabilities: normalizeAwCapabilities(plugin.manifest.requires?.aw.capabilities ?? []),
524
+ },
525
+ },
468
526
  artifactDigest: await calculateEntryDigest(pluginRoot, false),
469
- manifestDigest: sha256(normalizeManifestDigestInput(plugin.manifest)),
527
+ manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
470
528
  contentDigest: await calculateEntryDigest(pluginRoot, true),
471
529
  };
472
530
  }
473
- function normalizeCatalogRelease(raw, catalogVersion) {
531
+ async function validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot) {
532
+ const files = await listBundleFiles(pluginRoot);
533
+ const expectedReceiptPaths = new Set(files.flatMap((relative) => /^(?:claude|codex|agent-skills)\/skills\/[^/]+\/SKILL\.md$/u.test(relative)
534
+ ? [path.posix.join(path.posix.dirname(relative), AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)]
535
+ : []));
536
+ const actualReceiptPaths = files.filter((relative) => path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME);
537
+ for (const relative of expectedReceiptPaths) {
538
+ if (!actualReceiptPaths.includes(relative)) {
539
+ throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} Skill root is missing ${AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME}: ${relative}.`);
540
+ }
541
+ }
542
+ const expected = createAwPluginRequirementsReceipt({ name: plugin.manifest.name, version: plugin.manifest.version }, plugin.manifest.requires?.aw.capabilities ?? []);
543
+ for (const relative of actualReceiptPaths) {
544
+ if (!expectedReceiptPaths.has(relative)) {
545
+ throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a reserved runtime requirements receipt outside a Skill root: ${relative}.`);
546
+ }
547
+ const receipt = parseAwPluginRequirementsReceipt(await readRegularJson(path.join(pluginRoot, ...relative.split('/')), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} runtime requirements receipt`));
548
+ if (!isDeepStrictEqual(receipt, expected)) {
549
+ throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a mismatched runtime requirements receipt: ${relative}.`);
550
+ }
551
+ }
552
+ }
553
+ function normalizeCatalogRelease(raw) {
474
554
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
475
555
  throw new Error('Invalid catalog manifest release: expected an object.');
476
556
  }
@@ -481,33 +561,16 @@ function normalizeCatalogRelease(raw, catalogVersion) {
481
561
  }
482
562
  return { state: 'development' };
483
563
  }
484
- const allowed = new Set(['state', 'targetVersion', 'planId', 'sourceHead', 'contentDigest']);
564
+ const allowed = new Set(['state', 'sourceHead']);
485
565
  if (release.state !== 'published' || Object.keys(release).some((key) => !allowed.has(key))) {
486
566
  throw new Error('Invalid published catalog release.');
487
567
  }
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
568
  if (typeof release.sourceHead !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(release.sourceHead)) {
503
569
  throw new Error('Invalid catalog release sourceHead.');
504
570
  }
505
571
  return {
506
572
  state: 'published',
507
- targetVersion,
508
- planId: release.planId,
509
573
  sourceHead: release.sourceHead,
510
- contentDigest: release.contentDigest,
511
574
  };
512
575
  }
513
576
  export async function buildCatalogWorkspace(options) {
@@ -521,7 +584,7 @@ export async function buildCatalogWorkspace(options) {
521
584
  const catalogVersion = catalogWorkspace.metadata.catalog.version;
522
585
  const channel = deriveCatalogChannel(catalogVersion);
523
586
  const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
524
- const identity = sourceIdentity(catalogWorkspace.metadata, plugins);
587
+ const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
525
588
  const marker = generatedOutputMarker({
526
589
  kind: 'catalog',
527
590
  workspaceIdentity: catalogWorkspace.workspaceIdentity,
@@ -532,6 +595,7 @@ export async function buildCatalogWorkspace(options) {
532
595
  force: options.force === true,
533
596
  build: async (stagingRoot) => {
534
597
  const bundledPlugins = [];
598
+ await fs.ensureDir(path.join(stagingRoot, 'plugins'));
535
599
  for (const plugin of plugins) {
536
600
  const closure = dependencyClosure(plugin, plugins);
537
601
  const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
@@ -550,20 +614,14 @@ export async function buildCatalogWorkspace(options) {
550
614
  .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
551
615
  await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('codex')), { spaces: 2 });
552
616
  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
617
  const entries = await Promise.all([...bundledPlugins]
560
618
  .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
561
619
  .map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
562
620
  const manifest = {
563
- schemaVersion: 4,
621
+ schemaVersion: 6,
564
622
  catalogVersion,
565
623
  channel,
566
- compatibility: { aw: { minVersion } },
624
+ requires: { aw: { capabilities: ['aw.catalog-snapshot.v6'] } },
567
625
  marketplace: {
568
626
  owner: catalogWorkspace.metadata.owner,
569
627
  interface: catalogWorkspace.metadata.interface,
@@ -574,9 +632,9 @@ export async function buildCatalogWorkspace(options) {
574
632
  await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
575
633
  await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
576
634
  },
577
- validate: async (stagingRoot) => { await validateCatalogSnapshot(stagingRoot); },
635
+ validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
578
636
  assertReplaceable: async (existingRoot) => {
579
- await validateCatalogSnapshot(existingRoot);
637
+ await validateCatalogSnapshotComplete(existingRoot);
580
638
  await assertGeneratedOutput(existingRoot, marker);
581
639
  },
582
640
  });
@@ -596,16 +654,90 @@ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
596
654
  const manifest = await fs.readJson(manifestPath);
597
655
  await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
598
656
  }
657
+ const pluginRoot = path.join(snapshotRoot, 'plugins', pluginName);
658
+ for (const relative of await listBundleFiles(pluginRoot)) {
659
+ if (path.posix.basename(relative) !== AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)
660
+ continue;
661
+ const receiptPath = path.join(pluginRoot, ...relative.split('/'));
662
+ const receipt = parseAwPluginRequirementsReceipt(await fs.readJson(receiptPath));
663
+ if (receipt.plugin.name !== pluginName) {
664
+ throw new Error(`Catalog plugin ${JSON.stringify(pluginName)} has a mismatched runtime requirements receipt.`);
665
+ }
666
+ await fs.writeFile(receiptPath, `${JSON.stringify(createAwPluginRequirementsReceipt({ name: pluginName, version }, receipt.requiredCapabilities), null, 2)}\n`);
667
+ }
668
+ }
669
+ }
670
+ function assertStablePluginVersion(version, fieldName) {
671
+ const parsed = parseStrictSemVer(version, fieldName);
672
+ if (parsed.prerelease.length > 0 || parsed.build.length > 0) {
673
+ throw new Error(`Invalid ${fieldName}: expected a stable SemVer without prerelease or build metadata.`);
674
+ }
675
+ return version;
676
+ }
677
+ function stableVersionBase(version, fieldName) {
678
+ const parsed = parseStrictSemVer(version, fieldName);
679
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
680
+ }
681
+ function resolvePublishedPluginVersions(options) {
682
+ const previousEntries = new Map(options.previous?.entries.map((entry) => [entry.name, entry]) ?? []);
683
+ const versions = {};
684
+ for (const plugin of options.plugins) {
685
+ const name = plugin.manifest.name;
686
+ const sourceVersion = assertStablePluginVersion(plugin.manifest.version, `${name} source version`);
687
+ const contentDigest = options.contentDigests.get(name);
688
+ if (!contentDigest)
689
+ throw new Error(`Missing release content digest for plugin ${JSON.stringify(name)}.`);
690
+ const previous = previousEntries.get(name);
691
+ if (!previous) {
692
+ versions[name] = options.channel === 'beta' ? `${sourceVersion}-beta.1` : sourceVersion;
693
+ continue;
694
+ }
695
+ const previousVersion = assertStrictSemVer(previous.version, `${name} previous published version`);
696
+ if (options.channel === 'latest') {
697
+ const comparison = compareStrictSemVer(sourceVersion, previousVersion);
698
+ if (comparison < 0) {
699
+ throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous ${previousVersion}.`);
700
+ }
701
+ if (comparison === 0 && previous.contentDigest !== contentDigest) {
702
+ throw new Error(`Plugin ${name} version ${sourceVersion} maps to different content. Increase the source plugin version.`);
703
+ }
704
+ versions[name] = sourceVersion;
705
+ continue;
706
+ }
707
+ const previousBase = stableVersionBase(previousVersion, `${name} previous published version`);
708
+ const baseComparison = compareStrictSemVer(sourceVersion, previousBase);
709
+ if (baseComparison < 0) {
710
+ throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous base ${previousBase}.`);
711
+ }
712
+ if (baseComparison > 0) {
713
+ versions[name] = `${sourceVersion}-beta.1`;
714
+ continue;
715
+ }
716
+ const parsedPrevious = parseStrictSemVer(previousVersion, `${name} previous published version`);
717
+ if (parsedPrevious.prerelease.length === 0) {
718
+ if (previous.contentDigest !== contentDigest) {
719
+ throw new Error(`Plugin ${name} stable version ${sourceVersion} maps to different content. Increase the source plugin version.`);
720
+ }
721
+ versions[name] = previousVersion;
722
+ continue;
723
+ }
724
+ if (parsedPrevious.prerelease.length !== 2
725
+ || parsedPrevious.prerelease[0] !== 'beta'
726
+ || !/^[1-9]\d*$/u.test(parsedPrevious.prerelease[1])) {
727
+ throw new Error(`Plugin ${name} previous version ${previousVersion} is not a stable or beta publication version.`);
728
+ }
729
+ versions[name] = previous.contentDigest === contentDigest
730
+ ? previousVersion
731
+ : `${sourceVersion}-beta.${Number(parsedPrevious.prerelease[1]) + 1}`;
599
732
  }
733
+ return versions;
600
734
  }
601
735
  export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
602
- const validation = await validateCatalogSnapshot(snapshotDirectory);
736
+ const validation = await validateCatalogSnapshotComplete(snapshotDirectory);
603
737
  if (validation.manifest.release.state !== 'published') {
604
738
  throw new Error('Published Catalog snapshot must have release.state published.');
605
739
  }
606
- if (!validation.ledger)
607
- throw new Error('Published Catalog snapshot must include a plugin release ledger.');
608
- return { ...validation, ledger: validation.ledger };
740
+ return validation;
609
741
  }
610
742
  export async function preparePublishedCatalogWorkspace(options) {
611
743
  if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(options.sourceHead)) {
@@ -623,10 +755,6 @@ export async function preparePublishedCatalogWorkspace(options) {
623
755
  if (previous && compareStrictSemVer(catalogVersion, previous.manifest.catalogVersion) <= 0) {
624
756
  throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.manifest.catalogVersion}.`);
625
757
  }
626
- const previousLedger = previous?.ledger ?? {
627
- schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
628
- records: [],
629
- };
630
758
  const temporaryRelative = path.join('.aw', 'publish', `.catalog-preparation-${crypto.randomUUID()}`);
631
759
  const finalRelative = options.outputPath
632
760
  ?? `${DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH}-v${catalogVersion}`;
@@ -639,15 +767,14 @@ export async function preparePublishedCatalogWorkspace(options) {
639
767
  reservedImports: options.reservedImports,
640
768
  });
641
769
  temporaryRoot = built.outputRoot;
642
- const development = await validateCatalogSnapshot(temporaryRoot);
770
+ const development = await validateCatalogSnapshotComplete(temporaryRoot);
643
771
  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]));
772
+ const overrides = resolvePublishedPluginVersions({
773
+ channel,
774
+ plugins: catalogWorkspace.plugins,
775
+ contentDigests,
776
+ previous: previous?.manifest ?? null,
777
+ });
651
778
  await rewritePublishedPluginVersions(temporaryRoot, overrides);
652
779
  const bundledPlugins = await Promise.all(catalogWorkspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
653
780
  const entries = await Promise.all(bundledPlugins.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
@@ -658,26 +785,14 @@ export async function preparePublishedCatalogWorkspace(options) {
658
785
  channel,
659
786
  entries,
660
787
  };
661
- const contentDigest = publishedContentDigest(manifestWithoutRelease);
662
788
  const manifest = {
663
789
  ...manifestWithoutRelease,
664
790
  release: {
665
791
  state: 'published',
666
- targetVersion: catalogVersion.replace(/-beta\.[1-9]\d*$/u, ''),
667
- planId: contentDigest,
668
792
  sourceHead: options.sourceHead,
669
- contentDigest,
670
793
  },
671
794
  };
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
- ]);
795
+ await fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`);
681
796
  const prepared = await validatePublishedCatalogSnapshot(temporaryRoot);
682
797
  const resolved = await resolveCatalogWorkspaceOutputPath({
683
798
  workspaceRoot: catalogWorkspace.workspaceRoot,
@@ -701,8 +816,6 @@ export async function preparePublishedCatalogWorkspace(options) {
701
816
  channel,
702
817
  pluginNames: built.pluginNames,
703
818
  snapshotDigest: prepared.snapshotDigest,
704
- contentDigest,
705
- planId: contentDigest,
706
819
  };
707
820
  }
708
821
  finally {
@@ -828,7 +941,7 @@ async function digestDirectory(root) {
828
941
  await walk(root);
829
942
  return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
830
943
  }
831
- export async function validateCatalogSnapshot(snapshotDirectory) {
944
+ async function validateCatalogSnapshotTransportInternal(snapshotDirectory) {
832
945
  const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
833
946
  const allowed = new Set([
834
947
  CATALOG_MANIFEST_FILENAME,
@@ -836,7 +949,6 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
836
949
  '.agents',
837
950
  '.claude-plugin',
838
951
  'plugins',
839
- 'plugin-release-ledger.json',
840
952
  ]);
841
953
  const entries = await fs.readdir(rootDir);
842
954
  const unexpected = entries.filter((entry) => !allowed.has(entry));
@@ -878,14 +990,17 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
878
990
  const snapshotDigest = await digestDirectory(rootDir);
879
991
  const raw = await fs.readJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME));
880
992
  const allowedManifestKeys = new Set([
881
- 'schemaVersion', 'catalogVersion', 'channel', 'compatibility', 'marketplace', 'release', 'entries',
993
+ 'schemaVersion', 'catalogVersion', 'channel', 'requires', 'marketplace', 'release', 'entries',
882
994
  ]);
883
995
  for (const key of Object.keys(raw))
884
996
  if (!allowedManifestKeys.has(key))
885
997
  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');
998
+ if (raw.schemaVersion !== 6)
999
+ throw new Error('Invalid catalog manifest schemaVersion: expected 6.');
1000
+ const base = normalizeCatalogManifestBase(raw, 'catalog manifest v6');
1001
+ if (!(base.requires.aw.capabilities ?? []).includes('aw.catalog-snapshot.v6')) {
1002
+ throw new Error('Invalid catalog manifest requires.aw.capabilities: expected aw.catalog-snapshot.v6.');
1003
+ }
889
1004
  const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
890
1005
  if (!Array.isArray(raw.entries))
891
1006
  throw new Error('Invalid catalog manifest entries: expected an array.');
@@ -901,7 +1016,7 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
901
1016
  delete copy.contentDigest;
902
1017
  return copy;
903
1018
  });
904
- const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v4')
1019
+ const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v6')
905
1020
  .map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
906
1021
  const pluginNames = normalizedEntries.map((entry, index) => {
907
1022
  if (typeof entry.name !== 'string')
@@ -915,39 +1030,57 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
915
1030
  const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
916
1031
  if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
917
1032
  throw new Error('Invalid catalog snapshot plugin inventory.');
918
- const plugins = [];
1033
+ const platformEntries = {
1034
+ claude: [],
1035
+ codex: [],
1036
+ };
919
1037
  for (const [index, name] of pluginNames.entries()) {
920
1038
  const pluginRoot = path.join(rootDir, 'plugins', name);
921
- const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
1039
+ await assertDirectoryWithoutSymlinks(pluginRoot, `Catalog plugin ${JSON.stringify(name)}`);
1040
+ const entry = normalizedEntries[index];
1041
+ if (entry.source !== `./plugins/${name}`) {
1042
+ throw new Error(`Catalog entry source for ${JSON.stringify(name)} does not match its plugin bundle.`);
1043
+ }
1044
+ const rawBundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(name)} bundle manifest`);
1045
+ const expectedDigests = {
1046
+ artifactDigest: await calculateEntryDigest(pluginRoot, false),
1047
+ manifestDigest: sha256(normalizeManifestDigestInput(rawBundleManifest)),
1048
+ contentDigest: await calculateEntryDigest(pluginRoot, true),
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
+ }
922
1055
  for (const platform of ['claude', 'codex']) {
923
1056
  const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
924
1057
  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
1058
  if (exists) {
929
- const rawPlatformManifest = await readRegularJson(platformManifest, `${platform} plugin manifest`);
930
- await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
931
- }
932
- }
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.`);
1059
+ await readRegularJson(platformManifest, `${platform} plugin manifest`);
1060
+ platformEntries[platform].push(entry);
937
1061
  }
938
1062
  }
939
- plugins.push(plugin);
940
1063
  }
941
- assertDependencyGraph(plugins);
942
1064
  const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
943
1065
  const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
944
- const marketplaceMetadata = {
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',
945
1078
  owner: marketplaceIdentity.owner,
946
1079
  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')));
1080
+ plugins: platformEntries[platform].map(marketplaceEntry),
1081
+ });
1082
+ const expectedAgents = expectedMarketplace('codex');
1083
+ const expectedClaude = expectedMarketplace('claude');
951
1084
  const normalizeMarketplaceOrder = (marketplace) => {
952
1085
  const canonical = JSON.parse(JSON.stringify(marketplace));
953
1086
  return {
@@ -963,32 +1096,60 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
963
1096
  if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
964
1097
  throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
965
1098
  }
966
- const release = normalizeCatalogRelease(raw.release, base.catalogVersion);
1099
+ const release = normalizeCatalogRelease(raw.release);
967
1100
  const manifest = {
968
- schemaVersion: 4,
1101
+ schemaVersion: 6,
969
1102
  catalogVersion: base.catalogVersion,
970
1103
  channel: base.channel,
971
- compatibility: base.compatibility,
1104
+ requires: base.requires,
972
1105
  marketplace: marketplaceIdentity,
973
1106
  release,
974
1107
  entries: normalizedEntries,
975
1108
  };
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.');
1109
+ return { rootDir, manifest, snapshotDigest };
1110
+ }
1111
+ export async function validateCatalogSnapshotTransport(snapshotDirectory) {
1112
+ return validateCatalogSnapshotTransportInternal(snapshotDirectory);
1113
+ }
1114
+ async function validateCatalogPluginEntriesComplete(validation, pluginNames) {
1115
+ const entriesByName = new Map(validation.manifest.entries.map((entry) => [entry.name, entry]));
1116
+ const plugins = [];
1117
+ for (const name of pluginNames) {
1118
+ const entry = entriesByName.get(name);
1119
+ if (!entry)
1120
+ throw new Error(`Catalog plugin ${JSON.stringify(name)} does not exist.`);
1121
+ const pluginRoot = path.join(validation.rootDir, 'plugins', entry.name);
1122
+ const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
1123
+ await validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot);
1124
+ for (const platform of ['claude', 'codex']) {
1125
+ const platformManifestPath = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
1126
+ const exists = await fs.pathExists(platformManifestPath);
1127
+ if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
1128
+ throw new Error(`Catalog plugin ${JSON.stringify(entry.name)} has an inconsistent ${platform} bundle projection.`);
1129
+ }
1130
+ if (exists) {
1131
+ const rawPlatformManifest = await readRegularJson(platformManifestPath, `${platform} plugin manifest`);
1132
+ await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
1133
+ }
980
1134
  }
1135
+ const expected = await expectedCatalogEntry(plugin, pluginRoot);
1136
+ for (const field of Object.keys(expected)) {
1137
+ if (!isDeepStrictEqual(entry[field], expected[field])) {
1138
+ throw new Error(`Catalog entry ${field} for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
1139
+ }
1140
+ }
1141
+ plugins.push(plugin);
981
1142
  }
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 };
1143
+ assertDependencyGraph(plugins);
1144
+ }
1145
+ export async function validateCatalogSnapshotPluginEntriesComplete(snapshotDirectory, pluginNames) {
1146
+ const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
1147
+ await validateCatalogPluginEntriesComplete(validation, pluginNames);
1148
+ return validation;
1149
+ }
1150
+ export async function validateCatalogSnapshotComplete(snapshotDirectory) {
1151
+ const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
1152
+ await validateCatalogPluginEntriesComplete(validation, validation.manifest.entries.map((entry) => entry.name));
1153
+ return validation;
993
1154
  }
994
1155
  //# sourceMappingURL=workspace.js.map