scripts/verify-place-inbox-rail.mjs
Rama main · réplica de lectura del repositorio original.
// The rail's "{name}: needs you" in the real packaged app, with a synthetic Admin and synthetic Clouds at their real
// addresses. Run on a disposable GitHub macOS runner only: it edits /etc/hosts and trusts a throwaway certificate.
// node scripts/verify-place-inbox-rail.mjs --app release/mac-arm64/OpenMausBot.app --out <evidence dir>
import assert from "node:assert/strict";
import { execFileSync, spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { createServer } from "node:https";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
const option = (name) => { const at = process.argv.indexOf(name); return at === -1 ? undefined : process.argv[at + 1]; };
if (process.env.GITHUB_ACTIONS !== "true" || process.platform !== "darwin") throw new Error("This check edits /etc/hosts and the system keychain: it runs on a disposable GitHub macOS runner only.");
const APP = resolve(option("--app") ?? "release/mac-arm64/OpenMausBot.app");
const OUT = resolve(option("--out") ?? mkdtempSync(join(tmpdir(), "omb-rail-evidence-")));
mkdirSync(OUT, { recursive: true });
const ROOT = new URL("..", import.meta.url);
const WORK = mkdtempSync(join(tmpdir(), "omb-rail-"));
const POLL_MS = 150_000;
const DEBUG_PORT = 9333;
const ADMIN_HOST = "cloud.openmausbot.com", AWAKE_HOST = "omb-u-aaaaaaaaaaaa.fly.dev", ASLEEP_HOST = "omb-u-bbbbbbbbbbbb.fly.dev";
const BLOCKED = ["us.i.posthog.com", "us-assets.i.posthog.com", "app.posthog.com"];
const TOKEN = `omc_${"T".repeat(43)}`, COOKIE = "omb_session_fixture";
const results = [];
const log = (line) => { console.log(line); appendFileSync(join(OUT, "progress.log"), `${line}\n`); };
function passed(name, detail = "") { results.push({ name, passed: true, detail }); log(`PASS ${name}${detail ? `: ${detail}` : ""}`); }
function trustThrowawayCertificate() {
const key = join(WORK, "key.pem"), cert = join(WORK, "cert.pem"), conf = join(WORK, "cert.conf");
writeFileSync(conf, `[req]\ndistinguished_name=dn\nx509_extensions=v3\nprompt=no\n[dn]\nCN=rail check\n[v3]\nsubjectAltName=${[ADMIN_HOST, AWAKE_HOST, ASLEEP_HOST].map((host) => `DNS:${host}`).join(",")}\nbasicConstraints=critical,CA:TRUE\nkeyUsage=critical,digitalSignature,keyCertSign\nextendedKeyUsage=serverAuth\n`);
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2", "-keyout", key, "-out", cert, "-config", conf]);
execFileSync("sudo", ["security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", cert]);
const lines = [ADMIN_HOST, AWAKE_HOST, ASLEEP_HOST, ...BLOCKED].map((host) => `127.0.0.1 ${host}`).join("\n");
execFileSync("sudo", ["sh", "-c", `printf '%s\\n' '${lines}' >> /etc/hosts && dscacheutil -flushcache`]);
return { key: readFileSync(key), cert: readFileSync(cert), certFile: cert };
}
const clouds = {
[AWAKE_HOST]: { id: "cloud-awake", name: "Studio", count: 0, power: "awake", requests: [], cookie: false },
[ASLEEP_HOST]: { id: "cloud-asleep", name: "Archive", count: 5, power: "asleep", requests: [], cookie: false },
};
const adminLog = [];
const SINCE = Date.now() - 60_000;
const identity = () => ({ cloudContractVersion: 1, expiresAt: Date.now() + 86_400_000, device: { id: "rail-device" }, account: { id: "rail-account", email: "person@example.test" } });
const listed = () => Object.entries(clouds).map(([host, cloud]) => ({
id: cloud.id, name: cloud.name, color: "teal", role: "owner", tier: "pro", runsOn: "cloud", status: "ready", origin: `https://${host}`, teamId: null, primary: false,
power: { state: cloud.power, since: cloud.since ?? SINCE, sleeps: true },
}));
function serve(tls) {
const server = createServer(tls, async (req, res) => {
let raw = "";
for await (const chunk of req) raw += chunk;
const reply = (status, value, headers = {}) => { res.writeHead(status, { "content-type": "application/json", ...headers }); res.end(JSON.stringify(value)); };
const host = (req.headers.host ?? "").split(":")[0];
const path = new URL(req.url, `https://${host}`).pathname;
if (host === ADMIN_HOST) {
adminLog.push(`${req.method} ${path}`);
if (path === "/api/cloud/desktop/authorize") return reply(201, { cloudContractVersion: 1, deviceCode: "A".repeat(43), userCode: "ABCDE-FGHJK", verificationUriComplete: `https://${ADMIN_HOST}/cloud/desktop?code=ABCDE-FGHJK`, expiresIn: 600, interval: 5 });
if (path === "/api/cloud/desktop/token") return reply(200, { ...identity(), accessToken: TOKEN });
if (path === "/api/cloud/desktop/pairing" && req.headers.authorization === `Bearer ${TOKEN}`) {
const wanted = Object.entries(clouds).find(([, cloud]) => cloud.id === JSON.parse(raw || "{}").cloudId);
return wanted ? reply(200, { cloudContractVersion: 1, origin: `https://${wanted[0]}`, code: "ABCD-EFGH-JKLM", expiresAt: Date.now() + 60_000 }) : reply(404, { error: "not_found" });
}
if (path === "/api/cloud/desktop/session" && req.headers.authorization === `Bearer ${TOKEN}`) {
return reply(200, { ...identity(), entitlement: { plan: "free", status: "inactive", expiresAt: null, version: 1 }, clouds: listed() });
}
if (req.url === "/" || path.startsWith("/cloud/")) { res.writeHead(200, { "content-type": "text/html" }); return res.end("<h1>Synthetic Admin</h1>"); }
return reply(404, { error: "not_found" });
}
const cloud = clouds[host];
if (!cloud) return reply(404, { error: "not_found" });
const signedIn = (req.headers.cookie ?? "").includes(`${COOKIE}=`);
if (path === "/api/inbox/count") {
cloud.requests.push({ at: Date.now(), cookie: signedIn, background: req.headers["x-omb-background"] === "1" });
return signedIn ? reply(200, { count: cloud.count }) : reply(401, { error: "sign in" });
}
cloud.requests.push({ at: Date.now(), other: `${req.method} ${path}` });
if (path === "/api/auth/pair") {
cloud.cookie = true;
return reply(200, { token: "fixture", session: { id: "s" } }, { "set-cookie": `${COOKIE}=${randomBytes(16).toString("hex")}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400; Secure` });
}
if (path === "/api/auth/session") return signedIn ? reply(200, { kind: "session", id: "s", label: "app", scopes: ["admin"], expiresAt: Date.now() + 86_400_000 }) : reply(401, { error: "sign in" });
if (path.startsWith("/api/")) return reply(404, { error: "not_found" });
res.writeHead(200, { "content-type": "text/html" });
res.end(`<!doctype html><title>${cloud.name}</title><h1 id="fake-cloud">${cloud.name}</h1>`);
});
return new Promise((done, fail) => { server.once("error", fail); server.listen(443, "0.0.0.0", () => done(server)); });
}
class Page {
constructor(socket) { this.socket = socket; this.next = 0; this.waiting = new Map();
socket.addEventListener("message", (event) => { const data = JSON.parse(event.data); const pending = this.waiting.get(data.id); if (pending) { this.waiting.delete(data.id); pending(data); } }); }
static async open(target) {
const socket = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((done, fail) => { socket.addEventListener("open", done, { once: true }); socket.addEventListener("error", fail, { once: true }); });
return new Page(socket);
}
send(method, params = {}) { const id = ++this.next; return new Promise((done) => { this.waiting.set(id, done); this.socket.send(JSON.stringify({ id, method, params })); }); }
async evaluate(expression) {
const { result, error } = await this.send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true, userGesture: true });
if (error || result?.exceptionDetails) throw new Error(`${expression.slice(0, 120)}: ${JSON.stringify(error ?? result.exceptionDetails).slice(0, 600)}`);
return result.result.value;
}
async shot(name) { const { result } = await this.send("Page.captureScreenshot", { format: "png" }); if (result?.data) writeFileSync(join(OUT, `${name}.png`), Buffer.from(result.data, "base64")); }
close() { this.socket.close(); }
}
const targets = async () => (await (await fetch(`http://127.0.0.1:${DEBUG_PORT}/json/list`)).json());
async function until(what, test, ms = 60_000) {
const end = Date.now() + ms;
for (;;) {
const value = await test().catch(() => null);
if (value) return value;
if (Date.now() > end) throw new Error(`Timed out waiting for ${what}`);
await delay(500);
}
}
const isRail = (target) => new URL(target.url || "about:blank").pathname === "/rail";
const railPage = async () => Page.open(await until("the rail's page", async () => (await targets()).find(isRail)));
const windowTarget = async (test, what) => until(what, async () => (await targets()).find((target) => target.type === "page" && !isRail(target) && test(new URL(target.url || "about:blank"))));
const TILES = `[...document.querySelectorAll("[data-rail-place]")].map((tile) => ({ id: tile.dataset.railPlace, kind: tile.dataset.railKind, label: tile.getAttribute("aria-label"), dot: Boolean(tile.querySelector("[data-rail-dot]")), active: tile.getAttribute("aria-current") === "page" }))`;
const tiles = (rail) => rail.evaluate(TILES);
const tileOf = async (rail, name) => (await tiles(rail)).find((tile) => tile.label?.includes(name));
const open = (rail, name) => rail.evaluate(`[...document.querySelectorAll("[data-rail-place]")].find((tile) => tile.getAttribute("aria-label")?.includes(${JSON.stringify(name)})).click()`);
async function dumpOnFailure(rail) {
writeFileSync(join(OUT, "state.json"), JSON.stringify({ tiles: rail ? await tiles(rail).catch(() => null) : null, adminLog, clouds, targets: await targets().catch(() => null) }, null, 2));
}
let app, server, rail, failure = null;
try {
const { certFile, ...tls } = trustThrowawayCertificate();
server = await serve(tls);
const home = join(WORK, "home"), dataDir = join(home, ".openmausbot");
mkdirSync(dataDir, { recursive: true });
const plan = join(WORK, "room-plan.json");
writeFileSync(plan, "{}");
const cli = join(WORK, "fixture-claude.mjs");
writeFileSync(cli, `#!${process.execPath}
if (process.argv[2] === "auth") { console.log(JSON.stringify({ loggedIn: true, email: "person@example.test" })); process.exit(0); }
process.env.FAKE_CLAUDE_VERSION = "2.1.284";
process.env.FAKE_CLAUDE_ROOM_PLAN = ${JSON.stringify(plan)};
await import(${JSON.stringify(new URL("server/testing/fake-claude-cli.ts", ROOT).href)});
`);
chmodSync(cli, 0o755);
writeFileSync(join(dataDir, "config.json"), JSON.stringify({ instances: {
...Object.fromEntries(["codex", "cursor", "githubCopilot", "openaiCompat", "qwen", "hermes", "pi", "grok"].map((id) => [id, { driver: "not-a-real-driver" }])),
claude: { driver: "claudeAgent", displayName: "Claude", config: { cli } },
} }));
const binary = join(APP, "Contents", "MacOS", "OpenMausBot");
assert.ok(existsSync(binary), `no app at ${binary}`);
const appLog = join(OUT, "app.log");
app = spawn(binary, ["--use-mock-keychain", `--remote-debugging-port=${DEBUG_PORT}`], { env: { ...process.env, HOME: home, OMB_DATA_DIR: dataDir, NODE_EXTRA_CA_CERTS: certFile }, stdio: ["ignore", "pipe", "pipe"] });
for (const stream of [app.stdout, app.stderr]) stream.on("data", (chunk) => appendFileSync(appLog, chunk));
app.once("exit", (code, signal) => { if (!failure) log(`app exited: ${code ?? signal}`); });
const local = await windowTarget((url) => url.hostname === "127.0.0.1" && url.pathname !== "/rail", "this computer's page");
const page = await Page.open(local);
await until("the page's bridge", () => page.evaluate(`Boolean(window.ogb?.cloudAccount) && document.readyState === "complete"`), 120_000);
passed("the packaged app starts with the rail's watch wired in", local.url);
await page.shot("00-app-started");
await page.evaluate(`window.ogb.cloudAccount.begin().then(() => true)`);
await until("the sign-in", () => page.evaluate(`window.ogb.cloudAccount.state().then((state) => state.status === "connected")`), 60_000).catch(async (error) => {
log(`account state: ${JSON.stringify(await page.evaluate("window.ogb.cloudAccount.state()").catch(String)).slice(0, 600)}`);
throw error;
});
assert.ok(adminLog.some((line) => line.endsWith("/api/cloud/desktop/session")), "the app read the Admin's session");
passed("signs in to the synthetic Admin at its real address, over real TLS");
rail = await railPage();
await until("three tiles", async () => (await tiles(rail))?.length >= 3, 60_000);
const first = await tiles(rail);
log(JSON.stringify(first));
assert.ok(first.some((tile) => tile.kind === "computer"), "This computer has a tile");
passed("the rail lists this computer and both Clouds", first.map((tile) => tile.label).join(" | "));
const counts = () => clouds[AWAKE_HOST].requests.filter((one) => one.background !== undefined);
await until("a first look at the Cloud", async () => counts().length > 0, POLL_MS);
assert.ok(counts().every((one) => one.background), "every count is asked as background work");
assert.ok(counts().every((one) => one.cookie), "it asks with a session, which the app made itself by pairing with its own Cloud");
assert.ok(clouds[AWAKE_HOST].cookie, "the app paired with its own awake Cloud without the person opening it");
assert.equal((await tileOf(rail, "Studio")).dot, false);
passed("a Cloud never opened on this computer is asked, as background work, with the session the app paired itself; nothing waits, so its tile stays quiet");
clouds[AWAKE_HOST].count = 2;
const fired = await until("Studio's attention", async () => { const tile = await tileOf(rail, "Studio"); return tile?.dot ? tile : null; }, POLL_MS);
assert.equal(fired.label, "Studio: needs you");
assert.ok(counts().every((one) => one.background && one.cookie));
passed("a place with lines waiting fires its label and dot, without ever being opened", fired.label);
await rail.shot("01-rail-fired");
await open(rail, "Studio");
const cloudWindow = await windowTarget((url) => url.hostname === AWAKE_HOST, "Studio's page");
await rail.close(); rail = await railPage();
await until("Studio on screen", async () => (await tileOf(rail, "Studio"))?.active);
assert.equal((await tileOf(rail, "Studio")).dot, false, "the place on screen has its own row");
passed("the place on screen shows no dot, even with lines waiting", cloudWindow.url);
await open(rail, "This computer");
await windowTarget((url) => url.hostname === "127.0.0.1" && url.pathname !== "/rail", "this computer's page again");
await rail.close(); rail = await railPage();
await until("Studio's dot back", async () => (await tileOf(rail, "Studio"))?.dot, POLL_MS);
passed("leaving it with lines still waiting brings its dot back");
const beforeSleep = () => counts().length;
clouds[AWAKE_HOST].power = "asleep"; clouds[AWAKE_HOST].since = Date.now();
const page2 = await Page.open(await windowTarget((url) => url.hostname === "127.0.0.1" && url.pathname !== "/rail", "this computer's page"));
await page2.evaluate(`window.ogb.cloudAccount.refresh().then(() => true)`);
await delay(3_000);
const sleeping = beforeSleep();
clouds[AWAKE_HOST].count = 0;
await delay(POLL_MS);
assert.equal(beforeSleep(), sleeping, "a sleeping Cloud is not asked");
assert.equal((await tileOf(rail, "Studio")).dot, true, "it keeps the last number it gave");
passed("once the Admin says a Cloud sleeps it is never asked, over a whole poll, and keeps its last number", `${POLL_MS / 1000}s with no request`);
clouds[AWAKE_HOST].power = "awake"; clouds[AWAKE_HOST].since = Date.now();
await page2.evaluate(`window.ogb.cloudAccount.refresh().then(() => true)`);
await until("Studio's attention to clear", async () => !(await tileOf(rail, "Studio")).dot, POLL_MS);
assert.equal((await tileOf(rail, "Studio")).label, "Studio");
passed("awake again with nothing waiting, its label and dot clear");
assert.deepEqual(clouds[ASLEEP_HOST].requests, [], "the Cloud that slept throughout got no request at all");
passed("a Cloud the Admin says is asleep received no request in the whole run", `${Math.round((Date.now() - SINCE) / 1000)}s`);
const api = (method, path, body) => page2.evaluate(`fetch(${JSON.stringify(path)}, { method: ${JSON.stringify(method)}, headers: { "content-type": "application/json" }${body ? `, body: ${JSON.stringify(JSON.stringify(body))}` : ""} }).then(async (res) => ({ status: res.status, body: await res.json().catch(() => null) }))`);
const created = await api("POST", "/api/bots", { name: "Eli", modelSelection: { instanceId: "claude", model: "claude-sonnet-5" } });
assert.equal(created.status, 201, JSON.stringify(created));
const bot = created.body.bot;
assert.ok((await api("PATCH", `/api/bots/${bot.id}`, { approvalMode: "ask", applyToAllThreads: true })).status < 300);
writeFileSync(plan, JSON.stringify({ [bot.id]: { script: [{ key: "Q", match: ["Q-GO"], timeoutMs: 600_000,
steps: [{ server: "ogb", tool: "ask_user", arguments: { question: "Which page?", choices: ["Home", "Pricing"] }, capture: { answer: "([\\s\\S]+)" } }], reply: "Q: {{answer}}" }] } }));
const sent = await api("POST", `/api/bots/${bot.id}/messages`, { text: "Q-GO", threadId: bot.threadId });
assert.equal(sent.status, 202, JSON.stringify(sent));
const asking = await until("the question in this computer's inbox", async () => { const { body } = await api("GET", "/api/inbox/count"); return body?.count >= 1 ? body : null; }, 120_000);
passed("a real bot's question on the real packaged server counts in this computer's inbox", JSON.stringify(asking));
await page2.shot("02-local-inbox-row");
page2.close();
await open(rail, "Studio");
await windowTarget((url) => url.hostname === AWAKE_HOST, "Studio's page again");
await rail.close(); rail = await railPage();
const computer = await until("This computer's attention", async () => { const tile = (await tiles(rail)).find((one) => one.kind === "computer"); return tile?.dot ? tile : null; }, POLL_MS);
assert.match(computer.label, /needs you/);
passed("while the window shows a Cloud, this computer's tile fires through its real local request", computer.label);
await rail.shot("03-rail-this-computer");
} catch (error) {
failure = error;
console.error(error);
await dumpOnFailure(rail);
} finally {
await dumpOnFailure(rail);
writeFileSync(join(OUT, "receipt.json"), `${JSON.stringify({ passed: !failure, results, failure: failure ? String(failure.stack ?? failure) : null,
limitation: "Synthetic Admin and Clouds at their real addresses over real TLS; unsigned ad-hoc build; a bot on the synthetic Claude CLI. No real Cloud, real sleep or real wake." }, null, 2)}\n`);
log(JSON.stringify({ passed: !failure, steps: results.length }));
rail?.close?.();
app?.kill("SIGTERM");
await delay(3_000);
app?.kill("SIGKILL");
server?.closeAllConnections?.(); server?.close?.();
process.exit(failure ? 1 : 0);
}