mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
refactor(install): prepare immutable standalone script assembly (#162556)
Prepare trusted install-smoke sealing to inline a fixed shared policy member from the immutable candidate archive. Preserve existing standalone sources and payload identity/integrity checks; never execute a candidate generator. Seven boundary tests passed on Linux Testbox. Changed checks and both import-cycle checks passed; independent review found no actionable issues.
This commit is contained in:
parent
3eaefa52af
commit
de7997edc3
3 changed files with 77 additions and 11 deletions
|
|
@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { assembleStandaloneInstaller } from "./lib/standalone-installers.mjs";
|
||||
|
||||
const MANIFEST_NAME = "install-smoke-candidate-payload.json";
|
||||
const SCHEMA = "openclaw.install-smoke-candidate-payload/v1";
|
||||
|
|
@ -239,16 +240,18 @@ export async function sealInstallSmokeCandidatePayload(
|
|||
|
||||
// Re-read installers from the immutable source archive after candidate execution. Candidate
|
||||
// build hooks never get a writable handle to the sealed scripts consumed by privileged jobs.
|
||||
const installScript = runPythonTarReader([
|
||||
"repo-file",
|
||||
options.archivePath,
|
||||
"scripts/install.sh",
|
||||
]);
|
||||
const cliInstallScript = runPythonTarReader([
|
||||
"repo-file",
|
||||
options.archivePath,
|
||||
"scripts/install-cli.sh",
|
||||
]);
|
||||
const readInstaller = (name: string) =>
|
||||
assembleStandaloneInstaller(
|
||||
runPythonTarReader(["repo-file", options.archivePath, `scripts/${name}`]).toString("utf8"),
|
||||
() =>
|
||||
runPythonTarReader([
|
||||
"repo-file",
|
||||
options.archivePath,
|
||||
"scripts/install-policy.sh",
|
||||
]).toString("utf8"),
|
||||
);
|
||||
const installScript = readInstaller("install.sh");
|
||||
const cliInstallScript = readInstaller("install-cli.sh");
|
||||
await fs.copyFile(sourceTarballPath, path.join(options.outputDir, "candidate.tgz"));
|
||||
await writeExclusive(
|
||||
path.join(options.outputDir, "candidate-pack.json"),
|
||||
|
|
|
|||
13
scripts/lib/standalone-installers.mjs
Normal file
13
scripts/lib/standalone-installers.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const POLICY_INCLUDE = 'source "${BASH_SOURCE[0]%${BASH_SOURCE[0]##*/}}./install-policy.sh"';
|
||||
|
||||
// The source can come from a sealed archive. Never evaluate candidate shell code
|
||||
// or load its generator while assembling a privileged install-smoke payload.
|
||||
export function assembleStandaloneInstaller(source, readPolicy) {
|
||||
if (!source.includes(POLICY_INCLUDE)) {
|
||||
return source;
|
||||
}
|
||||
if (source.indexOf(POLICY_INCLUDE) !== source.lastIndexOf(POLICY_INCLUDE)) {
|
||||
throw new Error("Installer must include shared policy exactly once");
|
||||
}
|
||||
return source.replace(POLICY_INCLUDE, () => readPolicy().trimEnd());
|
||||
}
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, readFileSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
|
|
@ -96,6 +104,29 @@ function verifyOptions(payloadDir: string, manifestSha256: string, sourceArchive
|
|||
}
|
||||
|
||||
describe("install smoke candidate payload", () => {
|
||||
it("inlines archived policy without executing candidate code or reading mutable sources", async () => {
|
||||
const fixture = createFixture();
|
||||
const scripts = path.join(fixture.root, "candidate-root/scripts");
|
||||
const marker = path.join(fixture.root, "executed");
|
||||
writeFileSync(
|
||||
path.join(scripts, "install.sh"),
|
||||
'#!/bin/bash\nsource "${BASH_SOURCE[0]%${BASH_SOURCE[0]##*/}}./install-policy.sh"\necho install\n',
|
||||
);
|
||||
writeFileSync(path.join(scripts, "install-policy.sh"), `touch '${marker}'\n`);
|
||||
createTarball(fixture.archivePath, fixture.root, ["candidate-root"]);
|
||||
writeFileSync(path.join(scripts, "install-policy.sh"), "mutable source must not be used\n");
|
||||
await sealInstallSmokeCandidatePayload({
|
||||
...IDENTITY,
|
||||
archivePath: fixture.archivePath,
|
||||
outputDir: fixture.payloadDir,
|
||||
packageDir: fixture.packageDir,
|
||||
});
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
expect(readFileSync(path.join(fixture.payloadDir, "install.sh"), "utf8")).toBe(
|
||||
`#!/bin/bash\ntouch '${marker}'\necho install\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it("seals source installers and package bytes into a fully bound payload", async () => {
|
||||
const fixture = await sealFixture();
|
||||
const verified = await verifyInstallSmokeCandidatePayload(
|
||||
|
|
@ -138,6 +169,25 @@ describe("install smoke candidate payload", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("rejects a policy include backed by an archive symlink", async () => {
|
||||
const fixture = createFixture();
|
||||
const scripts = path.join(fixture.root, "candidate-root/scripts");
|
||||
writeFileSync(
|
||||
path.join(scripts, "install.sh"),
|
||||
'#!/bin/bash\nsource "${BASH_SOURCE[0]%${BASH_SOURCE[0]##*/}}./install-policy.sh"\n',
|
||||
);
|
||||
symlinkSync("install-target.sh", path.join(scripts, "install-policy.sh"));
|
||||
createTarball(fixture.archivePath, fixture.root, ["candidate-root"]);
|
||||
await expect(
|
||||
sealInstallSmokeCandidatePayload({
|
||||
...IDENTITY,
|
||||
archivePath: fixture.archivePath,
|
||||
outputDir: fixture.payloadDir,
|
||||
packageDir: fixture.packageDir,
|
||||
}),
|
||||
).rejects.toThrow("scripts/install-policy.sh must be a regular file");
|
||||
});
|
||||
|
||||
it("rejects tampering with the final payload file after sealing", async () => {
|
||||
const fixture = await sealFixture();
|
||||
writeFileSync(path.join(fixture.payloadDir, "install-cli.sh"), "tampered\n");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue