Ctrl K
ComunidadGuías…

scripts/verify-ios-local-vm.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Fake-engine server + synthetic Local VM + companion sidecar, in disposable
// homes, for checking the iOS computer view against a Local VM by hand:
//
//   node --experimental-strip-types scripts/verify-ios-local-vm.ts
//
// Prints the sidecar address and a pairing code, then stays up until Ctrl-C.
import { spawn, type ChildProcess } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { deflateSync } from "node:zlib";
import { launchVerificationServer, type VerificationServer } from "./control-omb.ts";
import { fixtureApi } from "./testing/preview-fixture.ts";
import { fakeVncDesktop } from "./testing/fake-vnc-desktop.ts";
import { decodePng } from "./testing/png.ts";
import { createServer as createHttpServer } from "node:http";
import { BASE_IMAGE_DIGEST, CUA_DRIVER_VERSION, IMAGE, IMAGE_LAYER_VERSION, TARGET_LABEL, perBotLocalVmTarget } from "../server/container-computer.ts";

const root = fileURLToPath(new URL("..", import.meta.url));

/** The synthetic desktop: a captured Local VM session (an XFCE desktop with
 * a terminal open), with `frame` characters typed at the prompt so each
 * capture is distinct. The capture holds no account or network details. */
const captured = decodePng(readFileSync(join(root, "scripts/testing/fixtures/local-vm-desktop.png")));
function scene(frame: number): (x: number, y: number) => [number, number, number] {
  // The prompt's block cursor, in terminal cells of 9 by 16 pixels.
  const cursor = { x: 460, y: 153, w: 8, h: 16, step: 9 };
  const typed = frame % 4;
  return (x, y) => {
    if (y >= cursor.y && y < cursor.y + cursor.h && x >= cursor.x && x < cursor.x + cursor.step * (typed + 1)) {
      const cell = Math.floor((x - cursor.x) / cursor.step);
      const inGlyph = (x - cursor.x) % cursor.step < cursor.w;
      if (cell === typed) return inGlyph ? [0xff, 0xff, 0xff] : [0, 0, 0];
      // Typed characters: a lower-case run in the terminal's text colour.
      const ink = inGlyph && y >= cursor.y + 5 && y < cursor.y + 13 && (x - cursor.x) % cursor.step !== 3;
      return ink ? [0xd0, 0xd0, 0xd0] : [0, 0, 0];
    }
    return captured.pixel(x, y);
  };
}

/** A flat RGB PNG of `scene(frame)`, so each capture is distinct. */
function desktopPng(frame: number, width = 1280, height = 800): Buffer {
  const pixel = scene(frame);
  const raw = Buffer.alloc((width * 3 + 1) * height);
  for (let y = 0; y < height; y++) {
    const row = y * (width * 3 + 1);
    raw[row] = 0;
    for (let x = 0; x < width; x++) raw.set(pixel(x, y), row + 1 + x * 3);
  }
  const chunk = (type: string, data: Buffer) => {
    const length = Buffer.alloc(4);
    length.writeUInt32BE(data.length);
    const body = Buffer.concat([Buffer.from(type), data]);
    const crc = Buffer.alloc(4);
    crc.writeUInt32BE(crc32(body));
    return Buffer.concat([length, body, crc]);
  };
  const header = Buffer.alloc(13);
  header.writeUInt32BE(width, 0);
  header.writeUInt32BE(height, 4);
  header[8] = 8; // bit depth
  header[9] = 2; // truecolour
  return Buffer.concat([
    Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
    chunk("IHDR", header),
    chunk("IDAT", deflateSync(raw)),
    chunk("IEND", Buffer.alloc(0)),
  ]);
}

function crc32(data: Buffer): number {
  let crc = ~0;
  for (const byte of data) {
    crc ^= byte;
    for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
  }
  return ~crc >>> 0;
}

async function freePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const server = createServer();
    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => {
      const { port } = server.address() as { port: number };
      server.close(() => resolve(port));
    });
  });
}

async function waitFor(url: string, child: ChildProcess, headers: Record<string, string> = {}): Promise<void> {
  for (let attempt = 0; attempt < 100; attempt++) {
    if (child.exitCode !== null) throw new Error(`${url}: process exited (${child.exitCode})`);
    try {
      if ((await fetch(url, { headers })).ok) return;
    } catch {}
    await new Promise((resolve) => setTimeout(resolve, 200));
  }
  throw new Error(`${url} did not come up`);
}

const scratch = mkdtempSync(join(tmpdir(), "omb-ios-local-vm-"));
const bin = join(scratch, "bin");
const frames = join(scratch, "frames");
const fixtureTarget = join(scratch, "target.json");
mkdirSync(bin);
mkdirSync(frames);
for (let frame = 0; frame < 4; frame++) {
  writeFileSync(join(frames, `${frame}.b64`), desktopPng(frame).toString("base64"));
}
let fixture: VerificationServer | undefined;
let sidecar: ChildProcess | undefined;
// The VM's live desktop, behind VNC authentication like the real one.
const desktop = await fakeVncDesktop({ paint: scene(0) });
// A signal during server startup aborts it; the launcher then stops its child
// and removes its own data directory before the launch promise settles.
const startup = new AbortController();
let launching: Promise<VerificationServer> | undefined;
let stopping: Promise<void> | undefined;
const stop = () => stopping ??= (async () => {
  sidecar?.kill("SIGTERM");
  startup.abort();
  await launching?.catch(() => {});
  await desktop.close().catch(() => {});
  await fixture?.close().catch(() => {});
  rmSync(scratch, { recursive: true, force: true });
})();
process.once("SIGINT", () => void stop().then(() => process.exit(0)));
process.once("SIGTERM", () => void stop().then(() => process.exit(0)));

try {
  // Read-only inspection plus the two screenshot execs. Anything else fails,
  // so no command can reach a real container runtime.
  writeFileSync(join(bin, "docker"), `#!${process.execPath}
const fs = require('node:fs');
let args = process.argv.slice(2);
if (args[0] === '-H') args = args.slice(2);
const labels = ${JSON.stringify({ "com.openmausbot.local-vm": "1", "com.openmausbot.cua-driver": CUA_DRIVER_VERSION, "com.openmausbot.cua-base": BASE_IMAGE_DIGEST, "com.openmausbot.image-layer": IMAGE_LAYER_VERSION, "com.openmausbot.workspace": "1" })};
const imageId = 'sha256:' + 'a'.repeat(64);
const counter = ${JSON.stringify(join(scratch, "captures"))};
const target = fs.existsSync(${JSON.stringify(fixtureTarget)}) ? JSON.parse(fs.readFileSync(${JSON.stringify(fixtureTarget)}, 'utf8')) : null;
let result;
if (args[0] === 'exec' && args.includes('base64')) {
  const n = fs.existsSync(counter) ? Number(fs.readFileSync(counter, 'utf8')) : 0;
  fs.writeFileSync(counter, String(n + 1));
  result = fs.readFileSync(${JSON.stringify(frames)} + '/' + (n % 4) + '.b64', 'utf8');
}
else if (args[0] === 'exec') result = args.includes('--version') ? 'cua-driver ${CUA_DRIVER_VERSION}'
  : args.includes('health_report') ? {schema_version:'1',overall:'ok',checks:[]} : {};
else if (args[0] === 'info') result = 'fixture';
else if (args[0] === 'image' && args[1] === 'inspect') result = [{Id:imageId,Config:{Labels:labels}}];
else if (args[0] === 'inspect' && (args[1] === 'openmausbot-computer' || args[1] === target?.containerName)) result = [{
  Config:{Image:${JSON.stringify(IMAGE)},Labels:args[1] === target?.containerName ? {...labels,${JSON.stringify(TARGET_LABEL)}:target.label} : labels,Env:['VNC_PW=fixture-password']},
  State:{Running:true},Image:imageId,
  Mounts:[{Type:'bind',Source:args[1] === target?.containerName
    ? require('node:path').join(process.env.OMB_DATA_DIR,'vm-homes',target.label.slice(0,16))
    : require('node:path').join(process.env.OMB_DATA_DIR,'vm-home'),Destination:'/home/cua/workspace',RW:true}],
  HostConfig:{PortBindings:{'6901/tcp':[{HostIp:'127.0.0.1',HostPort:'${desktop.port}'}]},
    Privileged:false,Memory:4294967296,MemorySwap:4294967296,NanoCpus:2000000000,PidsLimit:4096,
    CapDrop:['ALL'],CapAdd:['CAP_SETUID','CAP_SETGID'],IpcMode:'private',ShmSize:536870912,
    CgroupnsMode:'private',SecurityOpt:[],RestartPolicy:{Name:'no',MaximumRetryCount:0}},
  NetworkSettings:{Ports:{'6901/tcp':[{HostIp:'127.0.0.1',HostPort:'${desktop.port}'}]}}
}];
else if (args[0] === 'ps') result = '';
else process.exit(1);
process.stdout.write(typeof result === 'string' ? result : JSON.stringify(result));
`, { mode: 0o700 });

  launching = launchVerificationServer(process.env, startup.signal, {
    binDir: bin, host: "ssh://127.0.0.1:1", sshKey: join(scratch, "unused-key"), staticDir: join(root, "dist"),
  });
  fixture = await launching;
  const api = fixtureApi(fixture.info.url);
  const { bot } = await api("POST", "/api/bots", {
    name: "Vee", description: "Works on the Local VM.", modelSelection: { instanceId: "claude", model: "claude-sonnet-4-5" },
  });
  const target = perBotLocalVmTarget(bot.id);
  writeFileSync(fixtureTarget, JSON.stringify({ containerName: target.containerName, label: target.label }));
  await api("PATCH", "/api/config", { localVm: { mode: "per-bot" } });
  await api("PATCH", `/api/bots/${bot.id}`, { computer: "vm" });
  const still = await api("POST", `/api/bots/${bot.id}/local-computer/screenshot?threadId=${bot.threadId}`);
  if (!String(still.image).startsWith("data:image/png;base64,")) throw new Error("fixture screenshot route did not return a PNG");

  const harnessPort = new URL(fixture.info.url).port;
  const companionPort = await freePort();
  const controlPort = await freePort();
  const controlToken = randomBytes(32).toString("base64url");
  const controlHeaders = { "x-openmausbot-control-token": controlToken };
  const companionHome = join(scratch, "companion-home");
  mkdirSync(companionHome);
  sidecar = spawn(process.execPath, ["--experimental-strip-types", join(root, "companion", "src", "index.ts")], {
    env: {
      PATH: process.env.PATH,
      HOME: companionHome,
      USERPROFILE: companionHome,
      OMB_PORT: harnessPort,
      OMB_COMPANION_PORT: String(companionPort),
      OMB_CONTROL_PORT: String(controlPort),
      OMB_CONTROL_TOKEN: controlToken,
      OMB_COMPANION_DIR: join(companionHome, "companion"),
    },
    stdio: ["ignore", "inherit", "inherit"],
  });
  await waitFor(`http://127.0.0.1:${controlPort}/state`, sidecar, controlHeaders);
  const { code } = await (await fetch(`http://127.0.0.1:${controlPort}/pairing`, { method: "POST", headers: controlHeaders })).json() as { code: string };

  console.log(JSON.stringify({
    harness: fixture.info.url,
    companion: `127.0.0.1:${companionPort}`,
    control: `http://127.0.0.1:${controlPort}`,
    pairingCode: code,
    bot: { id: bot.id, threadId: bot.threadId },
    log: fixture.info.logPath,
  }, null, 2));
  console.log("Pair the Simulator with the address and code above. Allow computer view with:");
  console.log(`  curl -X POST -H "x-openmausbot-control-token: ${controlToken}" http://127.0.0.1:${controlPort}/devices/<device-id>/cloud-desktop`);
  // What the phone has done to the desktop, and who holds the computer.
  const eventsPort = await freePort();
  createHttpServer(async (_req, res) => {
    const control = await api("GET", `/api/bots/${bot.id}/computer/control`).catch(() => null);
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({
      connections: desktop.connections(),
      authenticated: desktop.authResponses.length,
      pointerEvents: desktop.pointer.length,
      lastPointer: desktop.pointer.at(-1) ?? null,
      clicks: desktop.pointer.filter((event, index, all) => event.buttons & 1 && !((all[index - 1]?.buttons ?? 0) & 1)).length,
      typed: desktop.typed(),
      keys: desktop.keys.length,
      controlHeld: control?.held ?? null,
    }, null, 2));
  }).listen(eventsPort, "127.0.0.1");
  console.log(`Desktop events and control state: http://127.0.0.1:${eventsPort}/`);
  console.log("Ctrl-C stops everything and removes the temporary data.");
  await new Promise(() => {});
} catch (error) {
  console.error(error);
  await stop();
  process.exit(1);
}