scripts/verify-group-vm.ts
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { launchVerificationServer, runControlOmb } from "./control-omb.ts";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const podman = process.env.OMB_VERIFY_PODMAN;
const machine = process.env.OMB_VERIFY_MACHINE;
if (!podman || !isAbsolute(podman) || !machine) {
throw new Error("Set OMB_VERIFY_PODMAN to an absolute executable path and OMB_VERIFY_MACHINE explicitly");
}
type Connection = { Name: string; URI: string; Identity: string };
const connections = JSON.parse(execFileSync(podman, ["system", "connection", "list", "--format", "json"], {
encoding: "utf8", timeout: 15_000,
})) as Connection[];
const connection = connections.find(c => c.Name === machine);
if (!connection) throw new Error("The requested Podman connection does not exist");
const controller = new AbortController();
const cancel = () => controller.abort();
process.once("SIGINT", cancel);
process.once("SIGTERM", cancel);
const fixture = await launchVerificationServer(process.env, controller.signal, {
binDir: dirname(podman), host: connection.URI, sshKey: connection.Identity, staticDir: join(ROOT, "dist"),
});
async function api(path: string, body?: unknown, method = body === undefined ? "GET" : "POST", cleanup = false): Promise<any> {
if (!cleanup) controller.signal.throwIfAborted();
const timeout = AbortSignal.timeout(120_000);
const response = await fetch(fixture.info.url + path, {
method, headers: { "content-type": "application/json" },
// Finish an in-flight mutation before cleanup; aborting HTTP does not
// cancel the server-side container creation.
signal: cleanup || method !== "GET" ? timeout : AbortSignal.any([controller.signal, timeout]),
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
if (!response.ok) throw new Error(`${path}: ${await response.text()}`);
return response.json();
}
type Evidence = { id: string; computerArgs: string[]; target: string; status: string };
const ids: string[] = [];
const evidence: Evidence[] = [];
let groupId: string | undefined;
let receipt: Record<string, unknown> | undefined;
const errors: unknown[] = [];
try {
console.log(JSON.stringify({ stage: "isolated-fixture", ...fixture.info }));
await api("/api/config", { localVm: { mode: "per-bot", maxInstances: 2 } }, "PUT");
for (const name of ["Fixture GUI A", "Fixture GUI B"]) {
const { bot } = await api("/api/bots", { name });
ids.push(bot.id);
await api(`/api/bots/${bot.id}`, { computer: "vm" }, "PATCH");
await api(`/api/bots/${bot.id}/local-computer/run`, {});
console.log(JSON.stringify({ stage: "fixture-desktop", id: bot.id }));
}
const { group } = await api("/api/groups", { name: "Isolated Goal GUI routing", memberIds: ids,
setup: { bulletin: "Fixture only", defaultResponder: { kind: "member", botId: ids[0] } } });
groupId = group.id;
async function turn(id: string) {
await api(`/api/groups/${group.id}`, { defaultResponder: { kind: "member", botId: id } }, "PATCH");
// A stale dump must never count as proof of this dispatch.
rmSync(fixture.fixtureDumpPath, { force: true });
await api(`/api/groups/${group.id}/messages`, { mode: "goal", text: "Reply once for isolated GUI routing verification." });
const wait = await runControlOmb(["wait", "--channel", group.id, "--timeout", "60", "--url", fixture.info.url]) as { status: string };
controller.signal.throwIfAborted();
assert.equal(wait.status, "settled", "Goal turn must settle");
return { wait, dump: JSON.parse(readFileSync(fixture.fixtureDumpPath, "utf8")) };
}
for (const id of ids) {
const { wait, dump } = await turn(id);
const computer = dump.mcpConfig?.mcpServers?.computer;
assert(computer, "Goal speaker must receive computer MCP");
const target = (await api(`/api/bots/${id}/local-computer`)).container_name;
assert.equal(typeof target, "string");
assert(computer.args.includes(target), "MCP must target exactly the speaking bot GUI container");
assert(String(dump.systemPrompt).includes("computer"), "Goal must include computer instructions");
// Settled capabilities must already be revoked, including the final speaker.
const gate = await fetch(computer.env.OMB_CONTROL_URL, {
headers: { authorization: `Bearer ${computer.env.OMB_CONTROL_TOKEN}` }, signal: AbortSignal.timeout(5_000),
});
assert.equal(gate.status, 401, "A settled speaker must lose computer authority");
evidence.push({ id, computerArgs: computer.args, target, status: wait.status });
}
assert.notEqual(evidence[0].target, evidence[1].target, "Different speakers must use different desktops");
await api(`/api/bots/${ids[0]}`, { computer: "off" }, "PATCH");
const { dump: offDump } = await turn(ids[0]);
assert(!offDump.mcpConfig?.mcpServers?.computer, "Computer off must not receive desktop tools");
receipt = { passed: true, fixture: fixture.info.url, evidence, computerOff: true, settledCapabilitiesRevoked: true };
} catch (error) {
errors.push(error);
} finally {
if (groupId) await api(`/api/groups/${groupId}/interrupt`, {}, "POST", true).catch(e => errors.push(e));
for (const id of ids) {
// Stop before removing: an interrupted acceptance run may still own a lease.
await api(`/api/bots/${id}/interrupt`, {}, "POST", true).catch(e => errors.push(e));
await api(`/api/bots/${id}/local-computer/remove`, {}, "POST", true).catch(e => errors.push(e));
}
await fixture.close().catch(e => errors.push(e));
process.removeListener("SIGINT", cancel);
process.removeListener("SIGTERM", cancel);
}
if (errors.length) throw new AggregateError(errors, "Fixture verification or cleanup failed; inspect the exact fixture IDs above");
const evidenceDir = join(ROOT, ".omb-scratch/verification-logs");
mkdirSync(evidenceDir, { recursive: true });
writeFileSync(join(evidenceDir, "group-vm-routing.json"), JSON.stringify(receipt, null, 2));
console.log(JSON.stringify(receipt));