Files
AgentHub/server/__tests__/codex-pool.test.js
T
nntrivi2001 84e2a9f0ac feat(server-s3): one Linux user per account (S3) — experimental
Server mode can now run each account as its own Linux user (ah-<name>)
when `agenthub server install-system` has set S3 up. The dashboard
reaches a member only through sudo -u / write-as / pane-launch, reads a
member's files only through owner- and type-checked opens, and hooks
are authenticated by peer uid. Local mode and server mode without S3
are unchanged.

EXPERIMENTAL: tested with fake sudo and fake accounts only. It has not
yet been verified on a real install with real member accounts, sudo or
a real Codex login. Do not offer it to real users yet.

- S3 core: provisioning, per-member tmux servers, the proxy key
  gateway, shared tools in /opt, dropped capabilities for children
  (P0–P5)
- P4: plan and quota per member home
- P6: ~/.claude.json seed; opt-in plugin baseline installed as the
  member; managed-settings spike script (not run)
- P7: Codex account pool (`agenthub accounts add --tool codex`)
- Roles merged with main's admin / analyst / developer: analysts write
  only their own rows, under S3 as well
- Per-account prefs: one implementation (Chat UI + onboarding)
- Security reviews F1–F49 (five rounds), each fix with a regression
  test: symlink/TOCTOU-safe member reads, no host writes in a member
  home, credentials and the data dir never served, owner and folder
  account must agree, pane tokens bound to the pane's uid, bounded
  sudo/tmux/git calls and member reads (no hang or crash from a member),
  async route rejections answer 500 instead of exiting
- Docs: docs/SERVER-MODE.md (S3 model, accepted ceilings, operator
  runbook), docs/plans/server-mode/S3-PLAN.md, CLAUDE.md S3 rules

Tests: server 2838/2841 (3 need 127.0.0.1:3000, held on this box);
client 1162/1162; tsc clean; headers check passes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 14:57:10 +07:00

320 lines
15 KiB
JavaScript

/**
* @file codex-pool.test.js
* @description The Codex account pool (server/lib/codex-pool.js), fake
* accounts only: key/auth.json validation and file modes, the balancer
* (full quota last — read from the account's own rollouts —, then fewest live
* Codex panes, then spend), an API-key account reaching a pane only through
* tmux `-e` with a `-c` provider naming the variable, a ChatGPT account's
* `auth.json` written AS the member into their CODEX_HOME (never a host pane's,
* never over the member's own login, sticky while a pane of theirs runs on
* one), the newest `auth.json` copied back into the pool, and an empty pool
* leaving every spawn as it was.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after, beforeEach } = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");
const TMP = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "codex-pool-test-")));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.CLAUDE_HOME = path.join(TMP, "claude");
process.env.CODEX_HOME = path.join(TMP, "codex");
const { db } = require("../db");
const pool = require("../lib/codex-pool");
const tmux = require("../lib/tmux");
const runAs = require("../lib/run-as");
const pty = require("../lib/pty-run");
const { fakeSudo } = require("./fixtures/fake-sudo");
const KEY = (c) => `sk-proj-${c.repeat(40)}`;
const jwt = (claims) =>
`e30.${Buffer.from(JSON.stringify(claims)).toString("base64url")}.sig-${Math.random()}`;
const AUTH = (accountId, lastRefresh = "2026-10-01T00:00:00Z", rt = `rt-${accountId}`) => ({
auth_mode: "chatgpt",
OPENAI_API_KEY: null,
tokens: {
id_token: jwt({ email: `${accountId}@example.com` }),
access_token: jwt({ exp: 2e9 }),
refresh_token: rt,
account_id: accountId,
},
last_refresh: lastRefresh,
});
const MIA = { name: "mia", login: "ah-mia", home: path.join(TMP, "users", "mia"), uid: 5001 };
const MIA_AUTH = path.join(MIA.home, ".codex", "auth.json");
const realAccountForLane = runAs.accountForLane;
const realAccountForPath = runAs.accountForPath;
const realMemberHomes = runAs.memberHomes;
let sudo;
let tmuxArgv; // the last new-session argv
function run(
id,
account,
{ status = "completed", session = null, tool = "codex", lane = null } = {}
) {
db.prepare(
`INSERT INTO dashboard_runs (id, session_id, mode, cwd, status, started_at, account, tool, lane_id)
VALUES (?, ?, 'interactive', '/tmp', ?, ?, ?, ?, ?)`
).run(id, session, status, new Date().toISOString(), account, tool, lane);
}
function rollout(sessionId, usedPercent, home = process.env.CODEX_HOME) {
const dir = path.join(home, "sessions", "2026", "10", "09");
fs.mkdirSync(dir, { recursive: true });
const line = {
type: "event_msg",
payload: {
type: "token_count",
rate_limits: {
primary: { used_percent: usedPercent, window_minutes: 300, resets_at: 4e9 },
secondary: { used_percent: 5, window_minutes: 10080, resets_at: 4e9 },
},
},
};
const file = path.join(dir, `rollout-2026-10-09T00-00-00-${sessionId}.jsonl`);
fs.writeFileSync(
file,
`${JSON.stringify({ type: "session_meta", payload: { id: sessionId } })}\n${JSON.stringify(line)}\n`
);
return file;
}
/** Record that AgentHub placed pool account `name` in mia's home (what place() does). */
function placedWithMia(name) {
const file = path.join(pool.poolDir(), `${name}.json`);
fs.writeFileSync(file, JSON.stringify({ ...pool.__read(name), placedIn: ["ah-mia"] }), {
mode: 0o600,
});
}
const envOf = (argv, name) =>
argv
.slice(0, argv.indexOf("--"))
.find((x) => String(x).startsWith(`${name}=`))
?.slice(name.length + 1);
before(() => {
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") throw Object.assign(new Error("gone"), { status: 1 });
if (args[0] === "new-session") tmuxArgv = args;
return "";
});
});
after(() => {
tmux.__setExecImpl(null);
runAs.__setExecImpl(null);
runAs.accountForLane = realAccountForLane;
runAs.accountForPath = realAccountForPath;
runAs.memberHomes = realMemberHomes;
fs.rmSync(TMP, { recursive: true, force: true });
});
beforeEach(() => {
fs.rmSync(pool.poolDir(), { recursive: true, force: true });
fs.rmSync(path.join(TMP, "users"), { recursive: true, force: true });
fs.rmSync(path.join(process.env.CODEX_HOME, "sessions"), { recursive: true, force: true });
db.prepare("DELETE FROM dashboard_runs").run();
pool.resetQuota();
tmuxArgv = null;
sudo = fakeSudo();
runAs.__setExecImpl(sudo.exec);
runAs.accountForLane = (laneId) => (laneId >= 50 ? MIA : null);
// ...and her folder is hers (a pane's cwd must be its workspace's account's).
runAs.accountForPath = (p) => (String(p).startsWith(MIA.home) ? MIA : realAccountForPath(p));
runAs.memberHomes = () => [MIA.home];
fs.mkdirSync(path.join(MIA.home, ".agenthub", "handoff"), { recursive: true });
});
describe("codex pool: accounts", () => {
it("is empty until an admin adds one, and stores it 0600 in a 0700 dir", () => {
assert.equal(pool.pickAccount(null), null);
assert.throws(() => pool.addAccount("a", { apiKey: "nope" }), { code: "EBADTOKEN" });
assert.throws(() => pool.addAccount("../x", { apiKey: KEY("a") }), { code: "EBADNAME" });
assert.throws(() => pool.addAccount("c", { auth: { tokens: {} } }), { code: "EBADTOKEN" });
assert.equal(pool.addAccount("a", { apiKey: KEY("a") }).kind, "apikey");
assert.equal(pool.addAccount("c", { auth: AUTH("acct-c") }).kind, "chatgpt");
// An auth.json holding only an API key is an API-key account.
assert.equal(pool.addAccount("k", { auth: { OPENAI_API_KEY: KEY("k") } }).kind, "apikey");
assert.throws(() => pool.addAccount("a", { apiKey: KEY("b") }), { code: "EEXISTS" });
assert.equal(fs.statSync(path.join(pool.poolDir(), "a.json")).mode & 0o777, 0o600);
assert.equal(fs.statSync(pool.poolDir()).mode & 0o777, 0o700);
const shown = JSON.stringify(pool.poolStatus());
assert.ok(
!shown.includes(KEY("a")) && !shown.includes("rt-acct-c"),
"status never shows a secret"
);
pool.removeAccount("a");
assert.throws(() => pool.removeAccount("a"), { code: "ENOACCOUNT" });
});
it("ranks by Codex live panes (a Claude run of the same name does not count), then quota", () => {
for (const n of ["a", "b"]) pool.addAccount(n, { apiKey: KEY(n) });
run("c1", "a", { status: "running", tool: "claude" });
assert.equal(pool.pickAccount(null).name, "a");
run("x1", "a", { status: "running" });
assert.equal(pool.pickAccount(null).name, "b");
// b's own rollout says its 5-hour window is 95% used: b goes last.
run("x2", "b", { session: "11111111-2222-3333-4444-555555555555" });
rollout("11111111-2222-3333-4444-555555555555", 95);
pool.resetQuota();
assert.equal(pool.pickAccount(null).name, "a");
const b = pool.poolStatus().find((x) => x.name === "b");
assert.equal(b.full, true);
assert.equal(b.quota.windows[0].usedPercent, 95);
});
it("reads a run's quota only from its own account's CODEX_HOME, never dated in the future", () => {
pool.addAccount("b", { apiKey: KEY("b") });
const MIA_CODEX = path.join(MIA.home, ".codex");
// A host run's session id, forged in mia's home: not the host's reading.
run("h1", "b", { session: "11111111-2222-3333-4444-000000000001" });
rollout("11111111-2222-3333-4444-000000000001", 95, MIA_CODEX);
pool.resetQuota();
assert.equal(pool.poolStatus().find((x) => x.name === "b").quota, null);
// Mia's own run, but her rollout is dated 2030 to stay the newest forever.
run("m1", "b", { session: "11111111-2222-3333-4444-000000000002", lane: 50 });
const future = rollout("11111111-2222-3333-4444-000000000002", 0, MIA_CODEX);
fs.utimesSync(future, new Date("2030-01-01"), new Date("2030-01-01"));
pool.resetQuota();
assert.equal(pool.poolStatus().find((x) => x.name === "b").quota, null);
// Her own current reading of her own run still counts.
fs.utimesSync(future, new Date(), new Date());
pool.resetQuota();
assert.equal(pool.poolStatus().find((x) => x.name === "b").quotaPercent, 5);
});
it("never gives a host pane a ChatGPT account (it would replace the host's login)", () => {
pool.addAccount("c", { auth: AUTH("acct-c") });
assert.equal(pool.pickAccount(null), null);
assert.equal(pool.pickAccount(MIA).name, "c");
});
});
describe("codex pool: spawn", () => {
it("an empty pool leaves a Codex spawn exactly as before", () => {
pty.spawnRun({ laneId: 7, cwd: "/tmp", tool: "codex" });
const plain = tmuxArgv;
assert.ok(!plain.some((x) => /AGENTHUB_POOL_KEY|agenthub_pool/.test(String(x))));
const row = db.prepare("SELECT account FROM dashboard_runs WHERE id = 'agenthub-lane-7'").get();
assert.equal(row.account, null);
});
it("an API-key account reaches a host pane through tmux -e only", () => {
pool.addAccount("a", { apiKey: KEY("a") });
pty.spawnRun({ laneId: 7, cwd: "/tmp", tool: "codex", initialPrompt: "hi" });
assert.equal(envOf(tmuxArgv, "AGENTHUB_POOL_KEY"), KEY("a"));
const after = tmuxArgv.slice(tmuxArgv.indexOf("--"));
assert.ok(!after.some((x) => String(x).includes(KEY("a"))), "never in the pane's argv");
assert.ok(after.includes('model_provider="agenthub_pool"'));
assert.ok(after.some((x) => String(x).includes('env_key="AGENTHUB_POOL_KEY"')));
assert.ok(after.indexOf('model_provider="agenthub_pool"') < after.indexOf("hi"));
const row = db.prepare("SELECT account FROM dashboard_runs WHERE id = 'agenthub-lane-7'").get();
assert.equal(row.account, "a");
// Claude panes and Codex panes on an AI Proxy keep their own way in.
tmuxArgv = null;
pty.spawnRun({ laneId: 8, cwd: "/tmp", tool: "claude" });
assert.ok(!tmuxArgv.some((x) => String(x).includes("AGENTHUB_POOL_KEY")));
});
it("writes a ChatGPT account's auth.json AS the member, 0600, nothing secret over sudo", () => {
pool.addAccount("c", { auth: AUTH("acct-c") });
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
const written = JSON.parse(fs.readFileSync(MIA_AUTH, "utf8"));
assert.equal(written.tokens.refresh_token, "rt-acct-c");
assert.equal(fs.statSync(MIA_AUTH).mode & 0o777, 0o600);
assert.ok(
sudo.calls.some((a) => a.includes(MIA_AUTH)),
"written through write-as"
);
assert.ok(!JSON.stringify(sudo.calls).includes("rt-acct-c"));
assert.ok(!tmuxArgv.some((x) => String(x).includes("rt-acct-c")));
const row = db
.prepare("SELECT account FROM dashboard_runs WHERE id = 'agenthub-lane-50'")
.get();
assert.equal(row.account, "c");
});
it("never replaces a member's own login, and stays on the account their live pane uses", () => {
pool.addAccount("c", { auth: AUTH("acct-c") });
pool.addAccount("d", { auth: AUTH("acct-d") });
fs.mkdirSync(path.dirname(MIA_AUTH), { recursive: true });
fs.writeFileSync(MIA_AUTH, JSON.stringify(AUTH("mias-own")));
assert.equal(pool.pickAccount(MIA), null);
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
assert.equal(JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.account_id, "mias-own");
// A pool login in place, its pane live on d: the next pane is d too, though
// d already has more live panes than c.
fs.writeFileSync(MIA_AUTH, JSON.stringify(AUTH("acct-d")));
placedWithMia("d");
run("agenthub-lane-51", "d", { status: "running", lane: 51 });
assert.equal(pool.pickAccount(MIA).name, "d");
db.prepare("DELETE FROM dashboard_runs").run();
assert.equal(pool.pickAccount(MIA).name, "c");
});
it("a member's own seat in a pool account's ChatGPT Team stays theirs", () => {
// Codex's account_id is the WORKSPACE: every seat of a Team shares it.
pool.addAccount("c", { auth: AUTH("team-x") });
placedWithMia("c"); // even in a home AgentHub once placed c in
const own = AUTH("team-x", "2026-10-02T00:00:00Z", "rt-mias-seat");
own.tokens.id_token = jwt({ email: "mia@example.com" });
fs.mkdirSync(path.dirname(MIA_AUTH), { recursive: true });
fs.writeFileSync(MIA_AUTH, JSON.stringify(own));
assert.equal(pool.pickAccount(MIA), null);
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
assert.equal(JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.refresh_token, "rt-mias-seat");
});
it("copies the newest auth.json back into the pool", () => {
pool.addAccount("c", { auth: AUTH("acct-c", "2026-10-01T00:00:00Z") });
// A refresh comes back only from a home AgentHub placed the account in.
fs.mkdirSync(path.dirname(MIA_AUTH), { recursive: true });
fs.writeFileSync(MIA_AUTH, JSON.stringify(AUTH("acct-c", "2026-10-05T00:00:00Z", "rt-new")));
pool.syncBack();
assert.equal(pool.__read("c").auth.tokens.refresh_token, "rt-acct-c", "never placed there");
fs.rmSync(MIA_AUTH);
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
assert.deepEqual(pool.__read("c").placedIn, ["ah-mia"]);
fs.writeFileSync(MIA_AUTH, JSON.stringify(AUTH("acct-c", "2026-09-01T00:00:00Z", "rt-old")));
pool.syncBack();
assert.equal(pool.__read("c").auth.tokens.refresh_token, "rt-acct-c", "older: kept ours");
fs.writeFileSync(MIA_AUTH, JSON.stringify(AUTH("acct-c", "2026-10-05T00:00:00Z", "rt-new")));
pool.syncBack();
assert.equal(pool.__read("c").auth.tokens.refresh_token, "rt-new");
assert.equal(fs.statSync(path.join(pool.poolDir(), "c.json")).mode & 0o777, 0o600);
// A new pane starts from the newest copy.
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
assert.equal(JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.refresh_token, "rt-new");
});
it("never takes back a forged auth.json: a future date, or another account's id_token", () => {
pool.addAccount("c", { auth: AUTH("acct-c", "2026-10-01T00:00:00Z") });
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
const forged = AUTH("acct-c", "9999-12-31T00:00:00Z", "rt-forged");
fs.writeFileSync(MIA_AUTH, JSON.stringify(forged));
pool.syncBack();
assert.equal(pool.__read("c").auth.tokens.refresh_token, "rt-acct-c", "future last_refresh");
// mia's own ChatGPT tokens relabelled with the pool account's id.
const own = AUTH("mias-own", "2026-10-05T00:00:00Z", "rt-mia");
own.tokens.account_id = "acct-c";
fs.writeFileSync(MIA_AUTH, JSON.stringify(own));
pool.syncBack();
assert.equal(pool.__read("c").auth.tokens.refresh_token, "rt-acct-c", "another identity");
// A garbage id_token proves nothing either.
own.tokens.id_token = "x";
fs.writeFileSync(MIA_AUTH, JSON.stringify(own));
pool.syncBack();
assert.equal(pool.__read("c").auth.tokens.refresh_token, "rt-acct-c", "no identity");
});
});