mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d584b49003 |
@@ -0,0 +1,86 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import Database from "better-sqlite3";
|
||||
import { ClaudeManagedLaunch } from "../src/domain/claude-managed-launch.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { createFullTestDb } from "./helpers/test-app.js";
|
||||
|
||||
// A Claude seat can be given its own subscription credential: an operator label and a token file. The managed launch
|
||||
// reads the file when Claude starts, so the value never passes through OpenRig (its records, the launch command, argv or
|
||||
// logs), and Claude doesn't start on another login when the file can't be read.
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const cleanup: Array<() => void> = [];
|
||||
afterEach(() => { for (const f of cleanup.splice(0)) f(); });
|
||||
const SYNTHETIC = "synthetic-fixture-value-not-a-credential";
|
||||
|
||||
function fixture(ref?: { label: string; tokenFile: string }) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "claude-seat-token-"));
|
||||
cleanup.push(() => rmSync(root, { recursive: true, force: true }));
|
||||
const bin = path.join(root, "bin"), home = path.join(root, "home"), cwd = path.join(root, "work");
|
||||
for (const dir of [bin, home, cwd]) mkdirSync(dir);
|
||||
const claude = path.join(bin, "claude");
|
||||
writeFileSync(claude, `#!/bin/sh
|
||||
if [ "$1" = --help ]; then printf '%s\\n' '--permission-mode <mode> (choices: "auto", "default")'; exit 0; fi
|
||||
printf '{"TOKEN":"%s"}' "\${CLAUDE_CODE_OAUTH_TOKEN-absent}"
|
||||
`);
|
||||
chmodSync(claude, 0o700);
|
||||
const db = new Database(":memory:"); cleanup.push(() => { db.close(); });
|
||||
db.exec(`CREATE TABLE rigs(id TEXT, env_json TEXT);
|
||||
CREATE TABLE nodes(id TEXT, rig_id TEXT, runtime TEXT, cwd TEXT, env_json TEXT, claude_token_ref TEXT);
|
||||
CREATE TABLE bindings(id TEXT, node_id TEXT, tmux_session TEXT, tmux_pane TEXT);
|
||||
CREATE TABLE occupant_tenures(node_id TEXT, generation_uuid TEXT, generation_ordinal INTEGER);`);
|
||||
db.prepare("INSERT INTO rigs VALUES ('rig', NULL)").run();
|
||||
db.prepare("INSERT INTO nodes VALUES ('node', 'rig', 'claude-code', ?, NULL, ?)").run(cwd, ref ? JSON.stringify(ref) : null);
|
||||
db.exec("INSERT INTO bindings VALUES ('b', 'node', 'seat@rig', '%1'); INSERT INTO occupant_tenures VALUES ('node', 'gen', 1)");
|
||||
const managed = new ClaudeManagedLaunch(db, { PATH: `${bin}:/usr/bin:/bin`, HOME: home }, {}, async () => undefined);
|
||||
const command = async () => (await managed.prepare({ nodeId: "node", session: "seat@rig", pane: "%1" }, "auto")).command(["--permission-mode", "auto"]);
|
||||
const run = (cmd: string) => exec("/bin/sh", ["-c", cmd], { env: { PATH: "/usr/bin:/bin" }, timeout: 5000 });
|
||||
return { root, command, run };
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === "win32")("a Claude seat's own subscription token file", () => {
|
||||
it("reaches only that seat's Claude process; the value never enters the launch command", async () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "claude-seat-token-file-")); cleanup.push(() => rmSync(root, { recursive: true, force: true }));
|
||||
const tokenFile = path.join(root, "claude-work.token");
|
||||
writeFileSync(tokenFile, `${SYNTHETIC}\n`, { mode: 0o600 });
|
||||
const f = fixture({ label: "work", tokenFile });
|
||||
const command = await f.command();
|
||||
expect(command).not.toContain(SYNTHETIC);
|
||||
expect(command).toContain(tokenFile);
|
||||
expect(JSON.parse((await f.run(command)).stdout)).toEqual({ TOKEN: SYNTHETIC });
|
||||
});
|
||||
|
||||
it("doesn't start Claude when the token file can't be read, and says why", async () => {
|
||||
const f = fixture({ label: "work", tokenFile: path.join(tmpdir(), "no-such-dir-claude-seat-token", "claude-work.token") });
|
||||
const result = await f.run(await f.command()).then(
|
||||
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
||||
(err: { code?: number; stdout?: string; stderr?: string }) => ({ code: err.code ?? -1, stdout: err.stdout ?? "", stderr: err.stderr ?? "" }));
|
||||
expect(result.code).not.toBe(0);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("token file (work) can't be read");
|
||||
});
|
||||
|
||||
it("a seat without one launches exactly as before", async () => {
|
||||
const f = fixture();
|
||||
const command = await f.command();
|
||||
expect(command).not.toContain("CLAUDE_CODE_OAUTH_TOKEN");
|
||||
expect(JSON.parse((await f.run(command)).stdout)).toEqual({ TOKEN: "absent" });
|
||||
});
|
||||
|
||||
it("is kept on the seat as a reference only, and can be cleared", () => {
|
||||
const db = createFullTestDb(); cleanup.push(() => db.close());
|
||||
const repo = new RigRepository(db);
|
||||
const rig = repo.createRig("token-rig");
|
||||
const node = repo.addNode(rig.id, "owner", { runtime: "claude-code" });
|
||||
const read = () => repo.getRig(rig.id)?.nodes.find((n) => n.id === node.id)?.claudeTokenRef ?? null;
|
||||
expect(repo.setNodeClaudeTokenRef(node.id, { label: "work", tokenFile: "/secrets/claude-work.token" })).toBe(true);
|
||||
expect(read()).toEqual({ label: "work", tokenFile: "/secrets/claude-work.token" });
|
||||
expect(repo.setNodeClaudeTokenRef(node.id, null)).toBe(true);
|
||||
expect(read()).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user