openclaw/scripts/verify-mac-runtime.mjs
Peter Steinberger 16be7f0f27
feat(macos): run the private app runtime on the OpenClaw Bun fork (#161603)
* feat(macos): run the private app runtime on the OpenClaw Bun fork

* fix(macos): let the bundled runtime load unsigned plugin addons

* test(macos): remove obsolete worker pruning closure tests

* test(macos): drop stale packaging test imports

* test(macos): migrate CI scope cases to runtime paths
2026-09-30 01:27:03 -07:00

622 lines
23 KiB
JavaScript
Executable file

#!/usr/bin/env bun
import assert from "node:assert/strict";
import { execFileSync, fork, spawnSync } from "node:child_process";
// Package proof: relocation, native load dependencies, provenance, and actual
// JSONL worker readiness. Never admits or opens the operator's live state.
import { createHash } from "node:crypto";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { createInterface } from "node:readline";
import { fileURLToPath, pathToFileURL } from "node:url";
import { auditMacRuntimePortability } from "./lib/mac-runtime-portability.mjs";
import { runManagedCommand, terminateManagedChild } from "./lib/managed-child-process.mts";
const [runtimeArg, expectedInfoPath, appArg] = process.argv.slice(2);
if (!runtimeArg || !expectedInfoPath) {
throw new Error("Usage: verify-mac-runtime.mjs <runtime> <expected-build-info.json> [app]");
}
const runtime = fs.realpathSync(runtimeArg);
const bun = path.join(runtime, "bin/bun");
const sqliteLibrary = path.join(runtime, "lib/libsqlite3.dylib");
const packageRoot = path.join(runtime, "lib/node_modules/openclaw");
assert.deepEqual(
fs.readdirSync(path.join(runtime, "bin")),
["bun"],
"Private runtime bin must contain only Bun",
);
assert(
fs.existsSync(path.join(packageRoot, "dist/control-ui/index.html")),
"Bundled Control UI is missing",
);
assert(fs.existsSync(path.join(packageRoot, "openclaw.mjs")), "Bundled CLI is missing");
assert(fs.existsSync(sqliteLibrary), "Bundled SQLite library is missing");
const expected = JSON.parse(fs.readFileSync(expectedInfoPath, "utf8"));
const actual = JSON.parse(fs.readFileSync(path.join(packageRoot, "dist/build-info.json"), "utf8"));
for (const key of ["version", "commit", "builtAt", "buildId"]) {
if (!expected[key] || expected[key] !== actual[key]) {
throw new Error(`Private runtime build mismatch: ${key}`);
}
}
if (!process.versions.bun || fs.realpathSync(process.execPath) !== bun) {
throw new Error("Runtime proof must execute the bundled Bun for the requested architecture");
}
const pin = JSON.parse(
fs.readFileSync(new URL("./lib/openclaw-bun-macos.json", import.meta.url), "utf8"),
);
const revision = execFileSync(bun, ["--revision"], { encoding: "utf8" }).trim();
assert.equal(revision, pin.revision, "Bundled Bun revision does not match the pinned fork");
assert(
pin.commit.startsWith(revision.split("+").at(-1)),
"Bun revision does not identify the pinned fork commit",
);
assert.equal(
execFileSync(bun, ["-p", "Bun.revision"], { encoding: "utf8" }).trim(),
pin.commit,
"Bundled Bun fork commit mismatch",
);
const nativeFiles = auditMacRuntimePortability(runtime, bun);
// Staging checks the runtime; the packager supplies the app to check every payload.
const noNodeRoot = appArg ? fs.realpathSync(appArg) : runtime;
assert.equal(
execFileSync("/usr/bin/find", [noNodeRoot, "-type", "f", "-name", "node", "-print"], {
encoding: "utf8",
}),
"",
"App bundle contains a Node executable",
);
function proofEnvironment(home) {
return {
HOME: home,
TMPDIR: home,
PATH: `${path.dirname(bun)}:/usr/bin:/bin:/usr/sbin:/sbin`,
OPENCLAW_SQLITE_LIBRARY: sqliteLibrary,
OPENCLAW_STATE_DIR: path.join(home, "state"),
OPENCLAW_CONFIG_PATH: path.join(home, "openclaw.json"),
OPENCLAW_NO_RESPAWN: "1",
};
}
async function proveServiceChildRuntime(home) {
const relayPath = path.join(packageRoot, "dist/process/supervisor/service-child-relay.js");
const anchorPath = path.join(
packageRoot,
"dist/process/supervisor/service-child-group-anchor.js",
);
assert(fs.existsSync(anchorPath), "Bundled service-child group anchor is missing");
await new Promise((resolve, reject) => {
const child = fork(relayPath, [], {
cwd: home,
env: proofEnvironment(home),
execPath: bun,
stdio: ["pipe", "pipe", "pipe", "pipe", "pipe", "ipc"],
});
const control = child.stdio[3];
const lineage = child.stdio[4];
let controlBuffer = "";
let output = "";
let rootSucceeded = false;
let closed = false;
let hostSequence = 0;
let lineageReported = false;
const timeout = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error("Bundled service-child relay proof timed out"));
}, 20_000);
const fail = (error) => {
if (closed) {
return;
}
closed = true;
clearTimeout(timeout);
child.kill("SIGKILL");
reject(error instanceof Error ? error : new Error(String(error)));
};
child.on("error", fail);
child.on("message", (message) => {
if (message?.type === "relay-error") {
fail(new Error(`Bundled service-child relay failed: ${message.error}`));
}
});
control.on("data", (chunk) => {
controlBuffer += chunk.toString();
for (;;) {
const newline = controlBuffer.indexOf("\n");
if (newline < 0) {
break;
}
const line = controlBuffer.slice(0, newline);
controlBuffer = controlBuffer.slice(newline + 1);
const message = JSON.parse(line);
if (message.type === "root-result") {
rootSucceeded = message.code === 0 && message.signal === null;
} else if (message.type === "closing") {
control.write(
`${JSON.stringify({
type: "closing-ack",
generation: message.generation,
sequence: ++hostSequence,
closingSequence: message.sequence,
})}\n`,
);
}
}
});
child.stdout.on("data", (chunk) => {
output += chunk.toString();
});
control.on("error", fail);
lineage.on("error", fail);
const reportLineageClosed = () => {
if (lineageReported) {
return;
}
lineageReported = true;
control.write(
`${JSON.stringify({
type: "lineage-closed",
generation: "mac-worker-relay-proof",
sequence: ++hostSequence,
})}\n`,
);
};
lineage.once("end", reportLineageClosed);
lineage.once("close", reportLineageClosed);
child.once("spawn", () => {
child.send({
type: "start",
generation: "mac-worker-relay-proof",
command: bun,
args: ["-e", 'process.stdout.write("mcp-relay-proof")'],
cwd: home,
env: proofEnvironment(home),
stdinMode: "pipe-closed",
controlFd: 3,
lineageFd: 4,
acknowledgeClosing: true,
});
});
child.once("exit", (code, signal) => {
if (closed) {
return;
}
closed = true;
clearTimeout(timeout);
if (code === 0 && signal === null && rootSucceeded && output === "mcp-relay-proof") {
resolve();
} else {
reject(
new Error(
`Bundled service-child relay proof failed (${code}/${signal}): ${JSON.stringify(output)}`,
),
);
}
});
});
}
async function proveGitWorkerRuntime(home) {
const repository = path.join(home, "git-worker-proof");
fs.mkdirSync(repository);
for (const args of [
["init", "-q"],
["config", "user.email", "proof@openclaw.invalid"],
["config", "user.name", "OpenClaw Proof"],
]) {
execFileSync("/usr/bin/git", args, { cwd: repository, env: proofEnvironment(home) });
}
fs.writeFileSync(path.join(repository, "proof.txt"), "before\n");
execFileSync("/usr/bin/git", ["add", "proof.txt"], {
cwd: repository,
env: proofEnvironment(home),
});
execFileSync("/usr/bin/git", ["commit", "-qm", "proof"], {
cwd: repository,
env: proofEnvironment(home),
});
fs.writeFileSync(path.join(repository, "proof.txt"), "after\n");
const { WorkerTaskPool } = await import(
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/process-runtime.js")).href
);
const pool = new WorkerTaskPool({
workerUrl: pathToFileURL(path.join(packageRoot, "dist/infra/git-operation.worker.js")),
maxWorkers: 1,
idleTimeoutMs: 1_000,
});
try {
const reply = await pool.run(
{ type: "checkout.diff", input: { cwd: repository, scope: "uncommitted" } },
{
timeoutMs: 30_000,
onRequest: async (request) => {
assert.equal(request?.type, "git.batch");
const replies = request.input.requests.map((operation) => {
assert(operation.type === "git.text" || operation.type === "git.buffer");
const result = spawnSync(
"/usr/bin/git",
["-C", operation.input.cwd, ...operation.input.args],
{
env: { ...proofEnvironment(home), ...operation.input.options.env },
input: operation.input.options.input,
encoding: null,
},
);
return {
ok: true,
value: {
stdout: new Uint8Array(result.stdout ?? Buffer.alloc(0)),
stderr: new Uint8Array(result.stderr ?? Buffer.alloc(0)),
windowsEncoding: null,
code: result.status,
signal: result.signal,
killed: false,
cleanup: "normal",
termination: result.signal ? "signal" : "exit",
timeoutMs: 30_000,
},
};
});
return { input: replies, timeoutMs: 30_000 };
},
},
);
assert(reply.ok, `Bundled Git worker failed: ${JSON.stringify(reply.error)}`);
assert(reply.value.files.some((file) => file.path === "proof.txt"));
} finally {
await pool.close();
}
}
function proveBrowserSetupRuntime(home) {
const proofHome = path.join(home, "browser-setup");
const stateDir = path.join(proofHome, "state");
const configPath = path.join(stateDir, "openclaw.json");
fs.mkdirSync(stateDir, { recursive: true });
fs.mkdirSync(path.join(proofHome, "Library/Application Support/Google/Chrome"), {
recursive: true,
mode: 0o700,
});
const config = JSON.stringify({
browser: { profiles: { chrome: { driver: "extension", cdpPort: 18999 } } },
});
fs.writeFileSync(configPath, config);
for (const action of ["inspect", "install", "verify"]) {
const result = JSON.parse(
execFileSync(
bun,
[
path.join(packageRoot, "dist/extensions/browser/setup-entry.js"),
"--action",
action,
"--wait-ms",
"1000",
],
{
cwd: proofHome,
env: {
...proofEnvironment(proofHome),
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_PROFILE: "mac-browser-proof",
},
encoding: "utf8",
timeout: 60_000,
},
),
);
assert.equal(result.action, action);
assert.equal(result.target.platform, "darwin");
assert.equal(result.target.kind, "local-host");
assert.equal(result.target.profile, "chrome");
assert.equal(result.target.relayPort, 18999);
assert.equal(result.installation.nativeHostRegistered, action !== "inspect");
assert.notEqual(result.connection.state, "connected");
assert.equal(fs.readFileSync(configPath, "utf8"), config, "Browser setup rewrote local config");
}
}
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-runtime-proof-")));
try {
// Package imports and their worker threads use the same isolated launch facts.
for (const key of Object.keys(process.env)) {
if (key.startsWith("OPENCLAW_")) {
delete process.env[key];
}
}
Object.assign(process.env, proofEnvironment(home));
const cli = path.join(packageRoot, "openclaw.mjs");
const versionOutput = execFileSync(bun, [cli, "--version"], {
cwd: home,
env: proofEnvironment(home),
encoding: "utf8",
timeout: 60_000,
}).trim();
const version = /^OpenClaw (\S+)(?: \([^)]+\))?$/u.exec(versionOutput)?.[1];
assert.equal(
version,
expected.version,
`Bundled CLI version does not match the app: ${versionOutput}`,
);
execFileSync(bun, [cli, "gateway", "--help"], {
cwd: home,
env: proofEnvironment(home),
stdio: "pipe",
timeout: 60_000,
});
const { ensureSqliteLibrarySelected } = await import(
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/memory-core-host-engine-knn.js")).href
);
const selection = ensureSqliteLibrarySelected();
assert.equal(selection.source, "env");
assert.equal(selection.path, sqliteLibrary);
assert.equal(selection.extensionLoadingSupported, true);
// Bun's SQLite selector is one-shot: load the database module and bootstrap
// fixture only after the staged package selects the signed library.
const { DatabaseSync } = await import("node:sqlite");
const { seedMacNodeWorkerProofState, readMacNodeWorkerProofRows } =
await import("./lib/mac-node-worker-proof-state.mjs");
// Ready manifests do not load lazy native capabilities. Exercise their real
// package loaders so omitted optional packages and wrong slices fail staging.
const require = createRequire(path.join(packageRoot, "package.json"));
// Keep required native mode and bundled module identity in a fresh process;
// worker readiness below must still use OpenClaw's normal defaults.
execFileSync(
bun,
[fileURLToPath(new URL("./verify-mac-runtime-fs.mjs", import.meta.url)), packageRoot, home],
{
cwd: home,
env: { ...proofEnvironment(home), FS_SAFE_NATIVE_MODE: "require" },
stdio: "inherit",
},
);
// Browser setup consumes native file operations; prove that prerequisite first.
// Its plugin-owned entry runs independently of the full CLI bootstrap.
proveBrowserSetupRuntime(home);
const database = new DatabaseSync(":memory:", { allowExtension: true });
try {
database.loadExtension(require("sqlite-vec").getLoadablePath());
assert.equal(
typeof database.prepare("SELECT vec_version() AS version").get().version,
"string",
);
} finally {
database.close();
}
const { spawnTerminalPty } = await import(
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/process-runtime.js")).href
);
const terminal = await spawnTerminalPty({
file: "/bin/sh",
args: ["-c", "printf worker-pty-proof"],
cwd: home,
env: proofEnvironment(home),
name: "xterm",
cols: 80,
rows: 24,
});
await new Promise((resolve, reject) => {
let output = "";
const timeout = setTimeout(() => {
terminal.kill("SIGKILL");
reject(new Error("Bundled PTY did not exit"));
}, 10_000);
terminal.onData((data) => {
output += data;
});
terminal.onExit(({ exitCode }) => {
clearTimeout(timeout);
if (exitCode === 0 && output === "worker-pty-proof") {
resolve();
} else {
reject(new Error(`Bundled PTY failed (${exitCode}): ${output}`));
}
});
});
// Browser screenshot normalization reaches this SDK helper, which launches a
// worker by its declared runtime path rather than through a module import.
const { resizeToJpeg } = await import(
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/media-runtime.js")).href
);
const png = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a2ioAAAAASUVORK5CYII=",
"base64",
);
const jpeg = await resizeToJpeg({ buffer: png, maxSide: 1, quality: 80 });
assert.deepEqual(jpeg.subarray(0, 2), Buffer.from([0xff, 0xd8]));
// The retained SQLite SDK launches its store host through another declared
// runtime path. Prove a real create/write/read/close cycle after relocation.
const sqliteBackendPath = path.join(home, "sqlite-worker-proof-backend.mjs");
fs.writeFileSync(
sqliteBackendPath,
`
import { DatabaseSync } from "node:sqlite";
export function createSqliteWorkerBackend(_input, { databasePath }) {
const database = new DatabaseSync(databasePath);
return {
execute(command) {
if (command.type === "roundTrip") {
return database.prepare("SELECT ? AS value").get(command.input).value;
}
throw new Error("Unexpected SQLite worker proof command");
},
close() {
database.close();
},
};
}
`,
);
const { openSqliteWorkerStore } = await import(
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/sqlite-runtime.js")).href
);
const sqliteStore = await openSqliteWorkerStore({
moduleUrl: pathToFileURL(sqliteBackendPath),
databasePath: path.join(home, "sqlite-worker-proof.sqlite"),
input: undefined,
});
try {
assert.equal(
await sqliteStore.execute({ type: "roundTrip", input: "sqlite-worker-proof" }),
"sqlite-worker-proof",
);
} finally {
await sqliteStore.close();
}
// Configured stdio MCP servers and hosted-workspace diffs both reach helpers
// through runtime descriptors. Execute those relocated process boundaries so
// a present-but-incomplete closure fails before the app is signed.
await proveServiceChildRuntime(home);
await proveGitWorkerRuntime(home);
for (const { nativeFirst, desktopSharingEnabled } of [false, true].flatMap((nativeFirstEnabled) =>
[undefined, true, false].map((sharingEnabled) => ({
nativeFirst: nativeFirstEnabled,
desktopSharingEnabled: sharingEnabled,
})),
)) {
const appGatedComputer = !nativeFirst;
const proofHome = path.join(
home,
`${nativeFirst ? "native-first" : "absent"}-${desktopSharingEnabled ?? "default"}`,
);
const stateDir = path.join(proofHome, "state");
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
fs.mkdirSync(proofHome, { recursive: true });
// State lifecycle coordination lives outside removable state. Only remove
// this fresh fixture's exact hashes after the complete worker tree exits.
const coordinatorHash = createHash("sha256").update(databasePath).digest("hex").slice(0, 8);
const coordinatorFiles = ["state-lifecycle", "gateway-lifecycle"].flatMap((family) => {
const file = path.join(
fs.realpathSync("/tmp"),
`openclaw-state-locks-${process.getuid()}`,
`${family}.${coordinatorHash}.lock.sqlite`,
);
return [file, `${file}-journal`, `${file}-wal`, `${file}-shm`];
});
assert(
coordinatorFiles.every((file) => !fs.existsSync(file)),
"Proof coordinator already exists",
);
const nativeRows = nativeFirst ? seedMacNodeWorkerProofState(databasePath) : undefined;
let ready = false;
let failure;
let diagnostic = "";
const exitCode = await runManagedCommand({
bin: bun,
args: [
path.join(packageRoot, "dist/mac-node-worker.js"),
...(nativeFirst ? ["--profile", "mac-worker-proof"] : []),
"node",
"worker",
...(desktopSharingEnabled === undefined
? []
: [desktopSharingEnabled ? "--desktop-sharing" : "--no-desktop-sharing"]),
],
cwd: proofHome,
env: {
...proofEnvironment(proofHome),
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(proofHome, "openclaw.json"),
OPENCLAW_NODE_EXEC_HOST: "app",
OPENCLAW_NODE_EXEC_FALLBACK: "0",
...(appGatedComputer
? {
// Match the Mac app's synchronous readiness lease so the proof verifies
// that the bundled CUA plugin registers its computer-control commands.
OPENCLAW_CUA_DRIVER_ENDPOINT: JSON.stringify({
v: 1,
socketPath: path.join(proofHome, "cua-driver.sock"),
binaryPath: "/usr/bin/true",
}),
}
: {}),
// Same launch shape as MacNodeHostWorker: the worker must stay in the owned
// process group, or requireProcessTreeExit only proves the respawn wrapper died.
OPENCLAW_NO_RESPAWN: "1",
},
stdio: ["pipe", "pipe", "pipe"],
timeoutMs: 300_000,
requireProcessTreeExit: true,
onReady(child) {
const lines = createInterface({ input: child.stdout });
child.stderr.on("data", (data) => {
diagnostic = (diagnostic + data.toString()).slice(0, 4000);
});
lines.on("line", (line) => {
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
if (message.type !== "ready") {
return;
}
if (
message.version !== expected.version ||
!message.manifest?.commands?.includes("system.run") ||
!message.manifest?.commands?.includes("system.which") ||
!message.manifest?.commands?.includes("browser.proxy") ||
!message.manifest?.commands?.includes("browser.proxy.upload.v1") ||
!message.manifest?.commands?.includes("mcp.tools.call.v1") ||
(desktopSharingEnabled !== undefined &&
message.manifest?.commands?.includes("desktop.stream") !== desktopSharingEnabled) ||
(appGatedComputer &&
(!message.manifest?.commands?.includes("screen.snapshot") ||
!message.manifest?.commands?.includes("computer.act")))
) {
failure = new Error(
`Bundled worker returned an incompatible capability manifest: ${JSON.stringify(message.manifest)}`,
);
child.stdin.end('{"type":"stop"}\n');
return;
}
ready = true;
process.stdout.write(
`${JSON.stringify({ architecture: process.arch, revision, sqlite: selection, nativeFirst, desktopSharingEnabled, build: actual, nativeFiles, databasePath, manifest: message.manifest })}\n`,
);
if (appGatedComputer) {
// The readiness lease uses a harmless executable, not a live MCP
// daemon. Kill this capability probe before provider cleanup tries
// to contact it; the second lane still proves graceful shutdown.
terminateManagedChild(child, "SIGKILL");
} else {
child.stdin.end('{"type":"stop"}\n');
}
});
child.on("close", () => lines.close());
child.stdin.on("error", (error) => {
failure = error;
terminateManagedChild(child, "SIGKILL");
});
},
});
for (const file of coordinatorFiles) {
fs.rmSync(file, { force: true });
}
const expectedExitCode = appGatedComputer ? 137 : 0;
if (
failure ||
!ready ||
exitCode !== expectedExitCode ||
/failed during register/u.test(diagnostic)
) {
throw new Error(
`Bundled worker proof failed (${exitCode}): ${failure?.message ?? "missing readiness or registration failure"}; ${diagnostic}`,
{ cause: failure },
);
}
if (nativeRows) {
const initialized = new DatabaseSync(databasePath, { readOnly: true });
try {
assert.deepEqual(readMacNodeWorkerProofRows(initialized), nativeRows);
} finally {
initialized.close();
}
}
}
} finally {
fs.rmSync(home, { recursive: true, force: true });
}