fix(verify): isolate package archives and published canary caches

This commit is contained in:
morluto 2026-10-09 03:26:25 +08:00
parent 90b06db6c0
commit 3eb7dbdc16
6 changed files with 118 additions and 28 deletions

View File

@ -6,7 +6,6 @@ import { exec, json, run } from "./lib/verify-package-core.mjs";
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
export async function verifyPackageInstall({
root,
tarball,
prefix,
workspace,
@ -14,14 +13,7 @@ export async function verifyPackageInstall({
}) {
await exec(
"npm",
[
"install",
"--global",
"--ignore-scripts",
"--prefix",
prefix,
join(root, tarball),
],
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
{ env: environment },
);
const cli = join(prefix, "bin", "rea");
@ -61,7 +53,7 @@ export async function verifyPackageInstall({
"--omit=optional",
"--prefix",
noOptionalPrefix,
join(root, tarball),
tarball,
],
{ env: environment },
);

View File

@ -1,3 +1,4 @@
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
@ -6,15 +7,23 @@ import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
/** Create a tarball and assert production packaging constraints. */
export async function verifyPackagePack({ root, workspace }) {
const tarball = (
await exec("npm", ["pack", "--silent"], { cwd: root })
).stdout.trim();
const packedFiles = (
await exec("tar", ["-tf", join(root, tarball)])
).stdout.split("\n");
const packDirectory = join(workspace, "pack");
await mkdir(packDirectory);
const tarball = join(
packDirectory,
(
await exec(
"npm",
["pack", "--silent", "--pack-destination", packDirectory],
{
cwd: root,
},
)
).stdout.trim(),
);
const packedFiles = (await exec("tar", ["-tf", tarball])).stdout.split("\n");
const packedManifest = JSON.parse(
(await exec("tar", ["-xOf", join(root, tarball), "package/package.json"]))
.stdout,
(await exec("tar", ["-xOf", tarball, "package/package.json"])).stdout,
);
if (
packedManifest.scripts?.postinstall !== undefined ||
@ -42,7 +51,7 @@ export async function verifyPackagePack({ root, workspace }) {
const skill = (
await exec("tar", [
"-xOf",
join(root, tarball),
tarball,
"package/skills/reverse-engineer-anything/SKILL.md",
])
).stdout;

View File

@ -44,7 +44,6 @@ try {
referenceRoot,
});
const { cli, packageRunnerCli } = await verifyPackageInstall({
root,
tarball,
prefix: environmentData.prefix,
workspace,
@ -58,7 +57,7 @@ try {
"node_modules",
PRODUCT_IDENTITY.packageName,
),
tarball: join(root, tarball),
tarball,
workspace,
environment: environmentData.environment,
});
@ -137,6 +136,5 @@ try {
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 4 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading })}\n`,
);
} finally {
if (tarball) await rm(join(root, tarball), { force: true });
await rm(workspace, { recursive: true, force: true });
}

View File

@ -18,7 +18,7 @@ export async function verifyPackedBridge({
throw new Error("package omitted the Hopper bridge");
if (!packedFiles.includes(packedGhidraBridge))
throw new Error("package omitted the Ghidra bridge");
await exec("tar", ["-xf", join(root, tarball), "-C", workspace]);
await exec("tar", ["-xf", tarball, "-C", workspace]);
const ghidraSource = await readFile(
join(workspace, packedGhidraBridge),
"utf8",

View File

@ -18,9 +18,12 @@ const version = process.argv[2];
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
const serverEnvironment = { ...process.env };
delete serverEnvironment.HOPPER_TARGET_PATH;
const canaryRoot = await mkdtemp(join(tmpdir(), "rea-published-canary-"));
const serverEnvironment = {
...process.env,
npm_config_cache: join(canaryRoot, "npm-cache"),
};
delete serverEnvironment.HOPPER_TARGET_PATH;
const transport = new StdioClientTransport({
command: "npm",
args: [
@ -91,7 +94,6 @@ process.stdout.write(
);
async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
const previousVersion = await findPreviousPublishedVersion(targetVersion);
const prefix = join(canaryRoot, "update-prefix");
const home = join(canaryRoot, "update-home");
const environment = {
@ -101,6 +103,11 @@ async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
npm_config_prefix: prefix,
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
};
const previousVersion = await findPreviousPublishedVersion(
targetVersion,
canaryRoot,
environment,
);
await execFileAsync(
"npm",
@ -193,11 +200,11 @@ if (result.status === "failed") process.exitCode = 1;
};
}
async function findPreviousPublishedVersion(targetVersion) {
async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
const { stdout } = await execFileAsync(
"npm",
["view", "rea-agents", "versions", "--json"],
{ maxBuffer: 16 * 1024 * 1024 },
{ cwd, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const versions = JSON.parse(stdout);
const previous = versions

View File

@ -0,0 +1,84 @@
import { execFile } from "node:child_process";
import {
access,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { pathToFileURL } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execFileAsync = promisify(execFile);
const expectedFailure =
"package did not retain its MCP Registry ownership marker";
it("keeps npm pack output in the owned workspace when package validation fails", async () => {
const testRoot = await createTestTempDirectory("rea-pack-isolation-");
const packageRoot = join(testRoot, "source");
const workspace = join(testRoot, "workspace");
const npmCache = join(testRoot, "npm-cache");
const archiveName = "rea-agents-6.0.0.tgz";
const archivePath = join(packageRoot, archiveName);
const sentinel = Buffer.from("caller-owned archive\n");
await Promise.all([mkdir(packageRoot), mkdir(workspace), mkdir(npmCache)]);
await Promise.all([
writeFile(
join(packageRoot, "package.json"),
JSON.stringify({
name: "rea-agents",
version: "6.0.0",
mcpName: "invalid-owner",
bin: { rea: "scripts/rea.mjs", "rea-agents": "scripts/rea.mjs" },
dependencies: { "@lydell/node-pty": "1.1.0" },
}),
),
writeFile(archivePath, sentinel),
]);
const packageContentsBefore = (await readdir(packageRoot)).sort();
const packerUrl = pathToFileURL(
resolve("scripts/verify-package-pack.mjs"),
).href;
const harness = `
import { rm } from "node:fs/promises";
const workspace = ${JSON.stringify(workspace)};
let failure;
try {
const { verifyPackagePack } = await import(${JSON.stringify(packerUrl)});
await verifyPackagePack({ root: ${JSON.stringify(packageRoot)}, workspace });
} catch (cause) {
if (!(cause instanceof Error) || cause.message !== ${JSON.stringify(expectedFailure)}) throw cause;
failure = cause.message;
} finally {
await rm(workspace, { recursive: true, force: true });
}
if (failure !== ${JSON.stringify(expectedFailure)}) throw new Error("Expected package metadata validation to fail after packing");
process.stdout.write(JSON.stringify({ failure }));
`;
const { stdout } = await execFileAsync(
process.execPath,
["--input-type=module", "--eval", harness],
{
cwd: process.cwd(),
env: {
...process.env,
npm_config_cache: npmCache,
TEMP: npmCache,
TMP: npmCache,
TMPDIR: npmCache,
},
maxBuffer: 1_024 * 1_024,
},
);
expect(JSON.parse(stdout)).toEqual({ failure: expectedFailure });
expect((await readdir(packageRoot)).sort()).toEqual(packageContentsBefore);
expect(await readFile(archivePath)).toEqual(sentinel);
await expect(access(workspace)).rejects.toMatchObject({ code: "ENOENT" });
});