mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
"Rigs that survive." Crash the daemon or the whole machine — power-off, not a graceful stop — and on reboot the seats come back to their original sessions with pre-crash markers recalled, zero fresh-prime. Proven at the operating-system level via a real hard power-off capstone. Survival backbone: - Crash-restore resume-token ledger (FR-3 → FR-7 + FR-6.1 periodic freshness re-stamp): capture-on-adoption, snapshot-refresh, restore- target-pin, 1h advisory freshness threshold with --fresh re-verify, and no-silent-fresh-prime on the restore path. - Fixture-home isolation guard: a fresh-prime that would clobber a live rig's home dir is refused loudly. - Liveness PID-verify: live seat distinguished from dead-PID ghost. - Daemon event-loop health + TerminalSessionBroker resilience across daemon restarts. - Startup-proof: challenge-verified orientation read. Deny-by-default rig-send reversal: only needs_input (a real interactive picker on the target pane) hard-refuses; unknown / stale / missing / busy advise-and-send. --dangerously-interact --reason override unchanged. Removes the friction bridge that routine coordination hit under the 0.4.1 posture. New CLI: rig auth secret-boundary hardening (fd-first: open the fd + operate on the fd, never re-resolve the path — closes the check-then- use gap under interruption / concurrent-legitimate-rig-process scenarios; TOCTOU class closed as a consequence; leak-hunt regression against the real credential file). rig send unified targeting: multi-recipient list + --pod + --rig; backward-compatible single-send. Codex activity-signal hardening: idle Codex takes normal sends; genuine prompt still blocks; hook-trust autoclear on the restore path for fully-unattended Codex restore. Recovery: seat-handover full-cycle (delivers captured context to a fresh successor via the shipped restore-packet write + deliverInitialSessionPrompt; loud-unwind both lanes on failure); seat- forking closeout; idle-gate watchdog wakes an idle seat holding a claimable gate. Wave-2 UI: rig-status + launch-control on the workspace; switch-client view-retarget; dashboard theming (Vellum Dark opt-in via Tailwind darkMode selector; light byte-identical). Hardening + bug pile-ins: session-admin mutation auth-guard; rig queue show bounded body preview; manual configurable compaction trigger + same-seat guard. Doctrine: mission-slice-sop skill ships. Migrations: additive only. 045_resume_verification, 046_seat_identity_verdicts, 047_events_node_type_index. Existing v0.4.2 databases upgrade by running rig daemon start. Behavior change (rig send default posture): unknown / stale / missing / busy no longer default-block. If call sites relied on 0.4.1 default- block, add an explicit guard. See CHANGELOG.md and docs/releases/v0.4.3.md.
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { Command } from "commander";
|
|
import { startupProofCommand, resolveStartupProofEndpoint, submitStartupProof } from "../src/commands/startup-proof.js";
|
|
|
|
function captureLogs(fn: () => Promise<void>): Promise<{ logs: string[]; errors: string[]; exitCode: number | undefined }> {
|
|
return new Promise(async (resolve) => {
|
|
const logs: string[] = [];
|
|
const errors: string[] = [];
|
|
const origLog = console.log;
|
|
const origErr = console.error;
|
|
const origExit = process.exitCode;
|
|
process.exitCode = undefined;
|
|
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
console.error = (...args: unknown[]) => errors.push(args.join(" "));
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
console.log = origLog;
|
|
console.error = origErr;
|
|
}
|
|
const exitCode = process.exitCode;
|
|
process.exitCode = origExit;
|
|
resolve({ logs, errors, exitCode });
|
|
});
|
|
}
|
|
|
|
describe("rig startup-proof", () => {
|
|
beforeEach(() => vi.restoreAllMocks());
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it("resolves endpoint and token from the activity endpoint file", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "openrig-startup-proof-"));
|
|
try {
|
|
mkdirSync(root, { recursive: true });
|
|
writeFileSync(join(root, "activity-endpoint.json"), JSON.stringify({ baseUrl: "http://127.0.0.1:17433", token: "tok-file" }));
|
|
expect(resolveStartupProofEndpoint({ env: { OPENRIG_HOME: root } })).toEqual({
|
|
baseUrl: "http://127.0.0.1:17433",
|
|
token: "tok-file",
|
|
});
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("posts startup_proof through the authenticated activity hook", async () => {
|
|
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({
|
|
ok: true,
|
|
oriented: "verified",
|
|
nodeId: "node-1",
|
|
challengeId: "challenge-1",
|
|
}), { status: 200, headers: { "content-type": "application/json" } })) as unknown as typeof fetch;
|
|
|
|
const result = await submitStartupProof(
|
|
{ challengeId: "challenge-1", answer: "answer-1" },
|
|
{
|
|
fetchImpl,
|
|
env: {
|
|
OPENRIG_URL: "http://127.0.0.1:7433",
|
|
OPENRIG_ACTIVITY_HOOK_TOKEN: "tok-env",
|
|
OPENRIG_SESSION_NAME: "dev-qa@test-rig",
|
|
OPENRIG_NODE_ID: "node-1",
|
|
OPENRIG_RUNTIME: "codex",
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({ ok: true, oriented: "verified", nodeId: "node-1" });
|
|
expect(fetchImpl).toHaveBeenCalledWith("http://127.0.0.1:7433/api/activity/hooks", expect.objectContaining({
|
|
method: "POST",
|
|
headers: expect.objectContaining({ authorization: "Bearer tok-env" }),
|
|
}));
|
|
const body = JSON.parse((fetchImpl.mock.calls[0]?.[1] as RequestInit).body as string);
|
|
expect(body).toMatchObject({
|
|
eventFamily: "startup_proof",
|
|
sessionName: "dev-qa@test-rig",
|
|
nodeId: "node-1",
|
|
runtime: "codex",
|
|
challengeId: "challenge-1",
|
|
answer: "answer-1",
|
|
});
|
|
});
|
|
|
|
it("CLI submit prints verified result without exposing the bearer token", async () => {
|
|
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({
|
|
ok: true,
|
|
oriented: "verified",
|
|
nodeId: "node-1",
|
|
challengeId: "challenge-1",
|
|
}), { status: 200, headers: { "content-type": "application/json" } })) as unknown as typeof fetch;
|
|
const program = new Command();
|
|
program.exitOverride();
|
|
program.addCommand(startupProofCommand({
|
|
fetchImpl,
|
|
env: {
|
|
OPENRIG_URL: "http://127.0.0.1:7433",
|
|
OPENRIG_ACTIVITY_HOOK_TOKEN: "tok-secret",
|
|
OPENRIG_NODE_ID: "node-1",
|
|
OPENRIG_RUNTIME: "claude-code",
|
|
},
|
|
}));
|
|
|
|
const { logs, exitCode } = await captureLogs(async () => {
|
|
await program.parseAsync(["node", "rig", "startup-proof", "submit", "--challenge-id", "challenge-1", "--answer", "answer-1"]);
|
|
});
|
|
|
|
const output = logs.join("\n");
|
|
expect(exitCode).toBeUndefined();
|
|
expect(output).toContain("startup_proof: verified");
|
|
expect(output).toContain("node_id: node-1");
|
|
expect(output).not.toContain("tok-secret");
|
|
});
|
|
});
|