mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(release): attest release plan parser tree
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { builtinModules, createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
||||
@@ -81,7 +90,10 @@ const TOOLING_LOCKFILE_PATH = "pnpm-lock.yaml";
|
||||
const YAML_PACKAGE_VERSION = "2.9.0";
|
||||
const YAML_PACKAGE_INTEGRITY =
|
||||
"sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==";
|
||||
const YAML_PACKAGE_JSON_SHA256 = "20b8b197cbd10dad245d45e463dfe58e4c8c25a47e24bc4256ad9ab58bf35683";
|
||||
const YAML_PACKAGE_TREE_SHA256 = "610ccacfe592d226ac1eb04842d1f591c5381f2a68b9f785643101d10db52c27";
|
||||
const YAML_PACKAGE_MAX_FILES = 512;
|
||||
const YAML_PACKAGE_MAX_ENTRIES = 1024;
|
||||
const YAML_PACKAGE_MAX_BYTES = 4 * 1024 * 1024;
|
||||
const BUILTIN_IMPORTS = new Set([
|
||||
...builtinModules,
|
||||
...builtinModules.map((specifier) => `node:${specifier}`),
|
||||
@@ -313,6 +325,87 @@ function verifyYamlLockfile(lockfileText: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertYamlPackagePath(path: string) {
|
||||
if (
|
||||
!path ||
|
||||
!/^[\x20-\x7e]+$/u.test(path) ||
|
||||
path.includes("\\") ||
|
||||
posix.isAbsolute(path) ||
|
||||
path.split("/").some((component) => component === "." || component === "..")
|
||||
) {
|
||||
throw new Error(`installed yaml package contains an unsafe path: ${JSON.stringify(path)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyInstalledYamlPackageTree(packageRoot: string) {
|
||||
const rootStat = lstatSync(packageRoot);
|
||||
if (!rootStat.isDirectory()) {
|
||||
throw new Error("installed yaml package root must be a directory");
|
||||
}
|
||||
|
||||
const records: string[] = [];
|
||||
let entryCount = 0;
|
||||
let fileCount = 0;
|
||||
let totalBytes = 0;
|
||||
const walk = (directory: string, relativeDirectory = "") => {
|
||||
for (const name of readdirSync(directory).toSorted(compareAscii)) {
|
||||
const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
||||
assertYamlPackagePath(relativePath);
|
||||
entryCount += 1;
|
||||
if (entryCount > YAML_PACKAGE_MAX_ENTRIES) {
|
||||
throw new Error(
|
||||
`installed yaml package exceeds ${YAML_PACKAGE_MAX_ENTRIES} filesystem entries`,
|
||||
);
|
||||
}
|
||||
|
||||
const absolutePath = join(directory, name);
|
||||
const stat = lstatSync(absolutePath);
|
||||
if (stat.isSymbolicLink()) {
|
||||
throw new Error(`installed yaml package must not contain symbolic links: ${relativePath}`);
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
records.push(JSON.stringify(["directory", relativePath]));
|
||||
walk(absolutePath, relativePath);
|
||||
continue;
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
throw new Error(`installed yaml package must contain only directories and files`);
|
||||
}
|
||||
if (stat.nlink !== 1) {
|
||||
throw new Error(`installed yaml package files must have one link: ${relativePath}`);
|
||||
}
|
||||
|
||||
fileCount += 1;
|
||||
if (fileCount > YAML_PACKAGE_MAX_FILES) {
|
||||
throw new Error(`installed yaml package exceeds ${YAML_PACKAGE_MAX_FILES} files`);
|
||||
}
|
||||
totalBytes += stat.size;
|
||||
if (totalBytes > YAML_PACKAGE_MAX_BYTES) {
|
||||
throw new Error(`installed yaml package exceeds ${YAML_PACKAGE_MAX_BYTES} bytes`);
|
||||
}
|
||||
const bytes = readFileSync(absolutePath);
|
||||
if (bytes.byteLength !== stat.size) {
|
||||
throw new Error(`installed yaml package file changed while being read: ${relativePath}`);
|
||||
}
|
||||
records.push(
|
||||
JSON.stringify([
|
||||
"file",
|
||||
relativePath,
|
||||
bytes.byteLength,
|
||||
createHash("sha256").update(bytes).digest("hex"),
|
||||
]),
|
||||
);
|
||||
}
|
||||
};
|
||||
walk(packageRoot);
|
||||
|
||||
const manifest = `${records.toSorted(compareAscii).join("\n")}\n`;
|
||||
const digest = createHash("sha256").update(manifest, "ascii").digest("hex");
|
||||
if (digest !== YAML_PACKAGE_TREE_SHA256) {
|
||||
throw new Error(`installed yaml package tree must match yaml@${YAML_PACKAGE_VERSION}`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadVerifiedYamlParser(repoRoot: string, toolingSha: string): ParseYaml {
|
||||
const packageJsonBytes = readVerifiedToolingRootBytes(
|
||||
repoRoot,
|
||||
@@ -335,7 +428,7 @@ function loadVerifiedYamlParser(repoRoot: string, toolingSha: string): ParseYaml
|
||||
|
||||
const toolingRequire = createRequire(resolve(EXECUTION_ROOT, TOOLING_PACKAGE_JSON_PATH));
|
||||
const packageJsonPath = realpathSync(toolingRequire.resolve("yaml/package.json"));
|
||||
const packageRoot = dirname(packageJsonPath);
|
||||
const packageRoot = realpathSync(dirname(packageJsonPath));
|
||||
const modulePath = realpathSync(toolingRequire.resolve("yaml"));
|
||||
const moduleRelativePath = relative(packageRoot, modulePath);
|
||||
if (
|
||||
@@ -345,13 +438,17 @@ function loadVerifiedYamlParser(repoRoot: string, toolingSha: string): ParseYaml
|
||||
) {
|
||||
throw new Error("resolved yaml module must be owned by its installed package");
|
||||
}
|
||||
const installedPackageJsonBytes = readFileSync(packageJsonPath);
|
||||
const installedPackageJsonSha = createHash("sha256")
|
||||
.update(installedPackageJsonBytes)
|
||||
.digest("hex");
|
||||
if (installedPackageJsonSha !== YAML_PACKAGE_JSON_SHA256) {
|
||||
throw new Error(`installed yaml package.json must match yaml@${YAML_PACKAGE_VERSION}`);
|
||||
const packageJsonRelativePath = relative(packageRoot, packageJsonPath);
|
||||
if (
|
||||
packageJsonRelativePath === ".." ||
|
||||
packageJsonRelativePath.startsWith(`..${sep}`) ||
|
||||
isAbsolute(packageJsonRelativePath)
|
||||
) {
|
||||
throw new Error("resolved yaml package.json must be owned by its installed package");
|
||||
}
|
||||
verifyInstalledYamlPackageTree(packageRoot);
|
||||
|
||||
const installedPackageJsonBytes = readFileSync(packageJsonPath);
|
||||
const installedPackageJson = JSON.parse(installedPackageJsonBytes.toString("utf8")) as {
|
||||
name?: unknown;
|
||||
version?: unknown;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -266,6 +276,50 @@ function trustedToolingGh(toolingFullRef: string, toolingSha: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function runYamlPackageSubprocess(
|
||||
mutate?: (params: { packageRoot: string; sentinelPath: string }) => void,
|
||||
) {
|
||||
const fixture = createFixtureRepo();
|
||||
const packageRoot = join(fixture.root, "node_modules/yaml");
|
||||
cpSync(realpathSync(resolve("node_modules/yaml")), packageRoot, { recursive: true });
|
||||
const sentinelPath = join(fixture.root, "yaml-executed");
|
||||
mutate?.({ packageRoot, sentinelPath });
|
||||
writeFixture(
|
||||
fixture.root,
|
||||
"yaml-package-harness.mts",
|
||||
`
|
||||
import { produceReleasePlan } from "./scripts/release-plan-producer.mts";
|
||||
|
||||
const toolingFullRef = ${JSON.stringify(fixture.toolingFullRef)};
|
||||
const toolingSha = ${JSON.stringify(fixture.toolingSha)};
|
||||
produceReleasePlan({
|
||||
repoRoot: ${JSON.stringify(fixture.root)},
|
||||
intent: "publish",
|
||||
candidateSha: ${JSON.stringify(fixture.candidateSha)},
|
||||
candidateRef: ${JSON.stringify(fixture.candidateRef)},
|
||||
toolingSha,
|
||||
toolingFullRef,
|
||||
runGh: () => JSON.stringify({
|
||||
ref: toolingFullRef,
|
||||
object: { type: "commit", sha: toolingSha },
|
||||
}),
|
||||
});
|
||||
`,
|
||||
);
|
||||
const tsxImport = pathToFileURL(createRequire(import.meta.url).resolve("tsx")).href;
|
||||
return {
|
||||
result: spawnSync(
|
||||
process.execPath,
|
||||
["--import", tsxImport, join(fixture.root, "yaml-package-harness.mts")],
|
||||
{
|
||||
cwd: fixture.root,
|
||||
encoding: "utf8",
|
||||
},
|
||||
),
|
||||
sentinelPath,
|
||||
};
|
||||
}
|
||||
|
||||
describe("release plan producer", () => {
|
||||
it("derives purpose, profile, tag, and soak from the canonical version parser", () => {
|
||||
expect(deriveReleasePlanPolicy("publish", "2026.8.1-beta.2")).toEqual({
|
||||
@@ -603,7 +657,66 @@ produceReleasePlan({
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("installed yaml package.json must match yaml@2.9.0");
|
||||
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
|
||||
});
|
||||
|
||||
it("accepts the complete installed yaml package tree", () => {
|
||||
const { result } = runYamlPackageSubprocess();
|
||||
expect(result.stderr).toBe("");
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects a changed yaml entry before executing it", () => {
|
||||
const { result, sentinelPath } = runYamlPackageSubprocess(({ packageRoot, sentinelPath }) => {
|
||||
const entryPath = join(packageRoot, "dist/index.js");
|
||||
writeFileSync(
|
||||
entryPath,
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "executed");\n${readFileSync(entryPath, "utf8")}`,
|
||||
);
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
|
||||
expect(existsSync(sentinelPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a changed yaml transitive module before execution", () => {
|
||||
const { result, sentinelPath } = runYamlPackageSubprocess(({ packageRoot, sentinelPath }) => {
|
||||
const transitivePath = join(packageRoot, "dist/public-api.js");
|
||||
writeFileSync(
|
||||
transitivePath,
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "executed");\n${readFileSync(transitivePath, "utf8")}`,
|
||||
);
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
|
||||
expect(existsSync(sentinelPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects internal yaml package symlinks", () => {
|
||||
const { result } = runYamlPackageSubprocess(({ packageRoot }) => {
|
||||
const transitivePath = join(packageRoot, "dist/public-api.js");
|
||||
unlinkSync(transitivePath);
|
||||
symlinkSync("index.js", transitivePath);
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("installed yaml package must not contain symbolic links");
|
||||
});
|
||||
|
||||
it("rejects extra yaml package files", () => {
|
||||
const { result } = runYamlPackageSubprocess(({ packageRoot }) => {
|
||||
writeFileSync(join(packageRoot, "unexpected.js"), "module.exports = {};\n");
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
|
||||
});
|
||||
|
||||
it("rejects missing yaml transitive files through tree attestation", () => {
|
||||
const { result } = runYamlPackageSubprocess(({ packageRoot }) => {
|
||||
rmSync(join(packageRoot, "dist/public-api.js"));
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
|
||||
expect(result.stderr).not.toContain("MODULE_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("rejects malformed publishable plugins while producing the plan", () => {
|
||||
|
||||
Reference in New Issue
Block a user