From c02893f10a2ce9f4422242f1bbc66cee62e28b6f Mon Sep 17 00:00:00 2001 From: smackgg Date: Tue, 28 Jul 2026 19:35:10 +0800 Subject: [PATCH 1/2] fix(release): verify selected package subset --- bun.lock | 2 +- .../plugins/nexus-service/convax-package.json | 2 +- packages/plugins/nexus-service/package.json | 2 +- .../nexus-service/package/manifest.json | 2 +- tooling/marketplace-output.test.js | 37 +++++++++++ tooling/verify-marketplace-output.mjs | 64 ++++++++++++++++++- 6 files changed, 102 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 8e4aee2..2b58f1c 100644 --- a/bun.lock +++ b/bun.lock @@ -56,7 +56,7 @@ }, "packages/plugins/nexus-service": { "name": "@microvoid/convax-plugin-nexus-service", - "version": "0.3.10", + "version": "0.3.11", "dependencies": { "@microvoid/convax-nexus-mcp": "workspace:*", }, diff --git a/packages/plugins/nexus-service/convax-package.json b/packages/plugins/nexus-service/convax-package.json index acb0400..4501426 100644 --- a/packages/plugins/nexus-service/convax-package.json +++ b/packages/plugins/nexus-service/convax-package.json @@ -4,7 +4,7 @@ "id": "nexus-service", "name": "Nexus · OpenRouter", "description": "Connects Convax to the Convax Workspace in Nexus for OpenRouter chat and live image-model generation through short-lived Data Tokens.", - "version": "0.3.10", + "version": "0.3.11", "license": "MIT", "compatibility": { "pluginSchema": "convax.plugin/7", diff --git a/packages/plugins/nexus-service/package.json b/packages/plugins/nexus-service/package.json index 3a990c0..8d4df31 100644 --- a/packages/plugins/nexus-service/package.json +++ b/packages/plugins/nexus-service/package.json @@ -1,6 +1,6 @@ { "name": "@microvoid/convax-plugin-nexus-service", - "version": "0.3.10", + "version": "0.3.11", "private": true, "type": "module", "dependencies": { diff --git a/packages/plugins/nexus-service/package/manifest.json b/packages/plugins/nexus-service/package/manifest.json index 1d89e9f..709bec3 100644 --- a/packages/plugins/nexus-service/package/manifest.json +++ b/packages/plugins/nexus-service/package/manifest.json @@ -3,7 +3,7 @@ "id": "nexus-service", "name": "Nexus · OpenRouter", "description": "Connects Convax to the Convax Workspace in Nexus for OpenRouter chat and live image-model generation through short-lived Data Tokens.", - "version": "0.3.10", + "version": "0.3.11", "contributes": { "generation": { "models": [ diff --git a/tooling/marketplace-output.test.js b/tooling/marketplace-output.test.js index 3596b91..20889e2 100644 --- a/tooling/marketplace-output.test.js +++ b/tooling/marketplace-output.test.js @@ -259,6 +259,43 @@ describe("published Marketplace output closure", () => { .rejects.toThrow("release-plan tags differ from Registry v2") }) + test("verifies only selected package Releases while retaining the complete Registry", async () => { + const fixture = await writeFixture() + const selected = fixture.releasePlan[0] + const metadata = fixture.releasePlan.at(-1) + await fs.writeFile( + path.join(fixture.catalogDirectory, "release-plan.json"), + `${JSON.stringify({ + schema: "convax.release-plan/1", + releases: [selected, metadata], + })}\n`, + ) + const selectedVersions = [{ + id: "fixture-plugin", + itemKey: sha256(Buffer.from("plugin\0fixture-plugin", "utf8")), + kind: "plugin", + previousVersion: "0.9.0", + releaseTag: "plugin-fixture-plugin-v1.0.0", + version: "1.0.0", + }] + + await expect(verifyMarketplaceOutput( + fixture.catalogDirectory, + { selectedVersions }, + )).resolves.toEqual({ + packages: 3, + releaseAssets: 4, + releaseTags: 2, + v1Packages: 2, + }) + + selectedVersions[0].releaseTag = "plugin-fixture-plugin-v9.9.9" + await expect(verifyMarketplaceOutput( + fixture.catalogDirectory, + { selectedVersions }, + )).rejects.toThrow("selected version change plugin/fixture-plugin differs from Registry v2") + }) + test("rejects a descriptor or Showcase that changes the Official publication identity", async () => { const descriptorFixture = await writeFixture() const descriptor = JSON.parse(await fs.readFile( diff --git a/tooling/verify-marketplace-output.mjs b/tooling/verify-marketplace-output.mjs index 7e11e64..6ba53a5 100644 --- a/tooling/verify-marketplace-output.mjs +++ b/tooling/verify-marketplace-output.mjs @@ -81,6 +81,52 @@ function expectedReleaseTag(entry) { return `${entry.kind}-${entry.id}-v${entry.version}` } +function selectedPackageTags(registryPackages, selectedVersions) { + const packageTags = new Set(registryPackages.map(expectedReleaseTag)) + if (selectedVersions === undefined) return packageTags + if (!Array.isArray(selectedVersions)) { + throw new Error("selected version changes must be an array") + } + + const registryByIdentity = new Map( + registryPackages.map((entry) => [`${entry.kind}\0${entry.id}`, entry]), + ) + const selectedIdentities = new Set() + const selectedTags = new Set() + for (const entry of selectedVersions) { + if ( + !entry || + typeof entry !== "object" || + Array.isArray(entry) || + typeof entry.kind !== "string" || + typeof entry.id !== "string" || + typeof entry.version !== "string" || + typeof entry.itemKey !== "string" || + typeof entry.releaseTag !== "string" || + (entry.previousVersion !== undefined && typeof entry.previousVersion !== "string") + ) { + throw new Error("selected version change is incomplete") + } + const identity = `${entry.kind}\0${entry.id}` + const registryEntry = registryByIdentity.get(identity) + const expectedItemKey = sha256(Buffer.from(identity, "utf8")) + if ( + !registryEntry || + registryEntry.version !== entry.version || + entry.itemKey !== expectedItemKey || + entry.releaseTag !== expectedReleaseTag(registryEntry) + ) { + throw new Error(`selected version change ${entry.kind}/${entry.id} differs from Registry v2`) + } + if (selectedIdentities.has(identity) || selectedTags.has(entry.releaseTag)) { + throw new Error(`selected version change duplicates ${entry.kind}/${entry.id}`) + } + selectedIdentities.add(identity) + selectedTags.add(entry.releaseTag) + } + return selectedTags +} + function releaseReference(value, label) { if ( !value || @@ -230,7 +276,10 @@ async function readExactCatalogFile( return fs.readFile(real) } -export async function verifyMarketplaceOutput(catalogDirectory) { +export async function verifyMarketplaceOutput( + catalogDirectory, + { selectedVersions } = {}, +) { const [descriptor, registryV2, registryV1, showcaseV2, releasePlan] = await Promise.all([ readJson(path.join(catalogDirectory, "marketplace.json"), "Marketplace descriptor"), readJson(path.join(catalogDirectory, "registry-v2.json"), "Registry v2"), @@ -271,8 +320,9 @@ export async function verifyMarketplaceOutput(catalogDirectory) { ) const packageTags = new Set(registryV2.packages.map(expectedReleaseTag)) + const publishedPackageTags = selectedPackageTags(registryV2.packages, selectedVersions) const metadataTag = `registry-v2-${registryV2.revision}` - const admittedPlanTags = new Set([...packageTags, metadataTag]) + const admittedPlanTags = new Set([...publishedPackageTags, metadataTag]) const assetsByUrl = new Map() const actualTags = new Set() const catalogRealPath = await fs.realpath(catalogDirectory) @@ -361,6 +411,8 @@ export async function verifyMarketplaceOutput(catalogDirectory) { for (const reference of collectReleaseReferences(registryV2)) { await readExactLocalRelease(catalogDirectory, catalogRealPath, reference.url, reference) + const { tag } = parseReleaseUrl(reference.url, "Registry") + if (!publishedPackageTags.has(tag)) continue const asset = assetsByUrl.get(reference.url) if (!asset) { throw new Error(`${reference.url} is absent from the release-plan`) @@ -391,7 +443,13 @@ export async function verifyMarketplaceOutput(catalogDirectory) { async function main(argv) { if (argv.length > 1) throw new Error("Usage: verify-marketplace-output [catalog-directory]") const directory = path.resolve(argv[0] ?? "dist/catalog") - const result = await verifyMarketplaceOutput(directory) + const selectedVersions = process.env.CONVAX_MARKETPLACE_CHANGED + ? await readJson( + path.resolve(process.env.CONVAX_MARKETPLACE_CHANGED), + "selected version changes", + ) + : undefined + const result = await verifyMarketplaceOutput(directory, { selectedVersions }) console.log( `Verified ${result.packages} packages, ${result.v1Packages} v1 identities, ` + `${result.releaseTags} immutable Releases, and ${result.releaseAssets} exact assets.`, From 025ef583a5bfa3040215d594c6151d0273d430c9 Mon Sep 17 00:00:00 2001 From: smackgg Date: Tue, 28 Jul 2026 19:37:41 +0800 Subject: [PATCH 2/2] fix(release): retain unselected product locks --- tooling/product-lock-output.test.js | 27 +++++++++++++++++++++ tooling/verify-product-lock-input.mjs | 35 ++++++++++++++++++--------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/tooling/product-lock-output.test.js b/tooling/product-lock-output.test.js index 2e3fe15..cc152c8 100644 --- a/tooling/product-lock-output.test.js +++ b/tooling/product-lock-output.test.js @@ -190,6 +190,33 @@ describe("Convax product lock input closure", () => { }) }) + test("verifies an existing preinstall outside a selected package release-plan", async () => { + const fixture = await writeFixture() + const planPath = path.join(fixture.root, "catalog", "release-plan.json") + const plan = JSON.parse(await fs.readFile(planPath, "utf8")) + plan.releases = plan.releases.filter((release) => + release.tag.startsWith("registry-v2-")) + await fs.writeFile(planPath, `${JSON.stringify(plan)}\n`) + const lockPath = path.join(fixture.root, "product-lock-input.json") + + await expect(verifyProductLockInput(lockPath)).resolves.toEqual({ + builtinReservations: 1, + preinstalledPackages: 1, + verifiedArtifacts: 7, + }) + + const pluginPath = path.join( + fixture.root, + "catalog", + "releases", + "plugin-ffmpeg-tools-v0.3.1", + "convax-plugin-ffmpeg-tools-0.3.1.zip", + ) + await fs.writeFile(pluginPath, "PLUGIN") + await expect(verifyProductLockInput(lockPath)) + .rejects.toThrow("preinstalled Plugin artifact bytes differ from Registry v2") + }) + test("rejects a source swap or widened preinstall policy", async () => { const fixture = await writeFixture() const lockPath = path.join(fixture.root, "product-lock-input.json") diff --git a/tooling/verify-product-lock-input.mjs b/tooling/verify-product-lock-input.mjs index 2b1d9e3..eb46f39 100644 --- a/tooling/verify-product-lock-input.mjs +++ b/tooling/verify-product-lock-input.mjs @@ -205,6 +205,7 @@ async function verifyLockedArtifact({ releaseAssets, label, expected, + allowExistingRelease = false, }) { exactKeys(lock, ["path", "url"], label) const relativePath = validateRelativePath(lock.path, `${label}.path`) @@ -216,15 +217,19 @@ async function verifyLockedArtifact({ throw new Error(`${label} path and URL identify different immutable bytes`) } const releaseAsset = releaseAssets.get(lock.url) - if (!releaseAsset) throw new Error(`${label} is absent from the ${area} release-plan`) - if (releaseAsset.path !== `releases/${tag}/${name}`) { - throw new Error(`${label} release-plan path differs from its URL`) - } - if (expected && ( - releaseAsset.size !== expected.size || - releaseAsset.sha256 !== expected.sha256 - )) { - throw new Error(`${label} differs from Registry v2`) + if (!releaseAsset && (!allowExistingRelease || !expected)) { + throw new Error(`${label} is absent from the ${area} release-plan`) + } + if (releaseAsset) { + if (releaseAsset.path !== `releases/${tag}/${name}`) { + throw new Error(`${label} release-plan path differs from its URL`) + } + if (expected && ( + releaseAsset.size !== expected.size || + releaseAsset.sha256 !== expected.sha256 + )) { + throw new Error(`${label} differs from Registry v2`) + } } const maximumSize = area === "builtin" ? builtinLimit @@ -238,8 +243,13 @@ async function verifyLockedArtifact({ label, maximumSize, ) - if (bytes.length !== releaseAsset.size || sha256(bytes) !== releaseAsset.sha256) { - throw new Error(`${label} bytes differ from the immutable release-plan`) + const admitted = releaseAsset ?? expected + if (bytes.length !== admitted.size || sha256(bytes) !== admitted.sha256) { + throw new Error( + releaseAsset + ? `${label} bytes differ from the immutable release-plan` + : `${label} bytes differ from Registry v2`, + ) } return bytes } @@ -461,6 +471,7 @@ export async function verifyProductLockInput(inputFile) { releaseAssets: catalogAssets, label: "preinstalled Plugin artifact", expected: registryArtifact(plugin, "ffmpeg-tools"), + allowExistingRelease: true, }) const declaredOwnedSkills = plugin.manifest?.contributes?.skills @@ -482,6 +493,7 @@ export async function verifyProductLockInput(inputFile) { releaseAssets: catalogAssets, label: "preinstalled owned Skill artifact", expected: registryArtifact(ownedSkill, "ffmpeg-canvas"), + allowExistingRelease: true, }) const lockedCompanion = preinstalled.companions[0] @@ -503,6 +515,7 @@ export async function verifyProductLockInput(inputFile) { releaseAssets: catalogAssets, label: "preinstalled companion artifact", expected: companionTarget.artifact, + allowExistingRelease: true, }) return {