mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* 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
622 lines
23 KiB
JavaScript
Executable file
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 });
|
|
}
|