/** * @file claude-pool-login.test.js * @description The Claude pool's `login` kind (claude-pool.js): a real login * is written into a MEMBER's `~/.claude/.credentials.json` as the member * (write-as, played by fixtures/fake-sudo.js) and never offered to a host * pane or over a member's own login; the server refreshes it ahead of expiry * and pushes the new pair into every home it placed it in; a pair a member's * Claude refreshed first is copied back only when Anthropic says it is the * same account; removing the account takes it out of those homes; and the * Users-page route moves a saved switch-user profile in — the one this * machine's own Claude Code is logged into with its LIVE login, the host's * home then being one more placement the server refreshes. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, beforeEach, after } = 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(), "claude-pool-login-"))); const USERS = path.join(TMP, "users"); const HOME = path.join(USERS, "mia"); const CREDS = path.join(HOME, ".claude", ".credentials.json"); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd"); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data"); process.env.CLAUDE_HOME = path.join(TMP, ".claude"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; process.env.DASHBOARD_POOL_REFRESH_MS = "0"; process.env.AGENTHUB_CC_KEYCHAIN = "0"; const { createApp, startServer } = require("../index"); const auth = require("../lib/auth"); const linux = require("../lib/linux-accounts"); const runAs = require("../lib/run-as"); const pool = require("../lib/claude-pool"); const usage = require("../lib/cc-usage"); const { fakeSudo } = require("./fixtures/fake-sudo"); const sudo = fakeSudo(); const H = 3600e3; const oauth = (tag, expiresIn = 8 * H) => ({ accessToken: `sk-ant-oat01-access-${tag}`, refreshToken: `sk-ant-ort01-refresh-${tag}`, expiresAt: Date.now() + expiresIn, scopes: ["user:inference"], subscriptionType: "max", }); const ACCOUNT = { accountUuid: "uuid-team", emailAddress: "team@example.com" }; const diskCreds = () => JSON.parse(fs.readFileSync(CREDS, "utf8")).claudeAiOauth; let acct; let server; let BASE; const exchanges = []; let owners = {}; before(async () => { fs.mkdirSync(HOME, { recursive: true }); const uid = process.getuid(); fs.writeFileSync(process.env.AGENTHUB_PASSWD, `ah-mia:x:${uid}:${uid}::/:/bin/sh\n`); fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=1\nUSERS_ROOT=${USERS}\nPORT=4820\n` ); linux.__resetConfCache(); runAs.__resetCache(); linux.__setRunImpl(() => "ok"); runAs.__setExecImpl(sudo.exec); process.env.AGENTHUB_MODE = "server"; server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; await auth.createUser("boss", "admin-pass-1", "admin"); await auth.createUser("mia", "member-pass-1", "developer"); acct = runAs.accountForPath(HOME); assert.ok(acct, "mia is a provisioned member"); // No network: Anthropic's answers are played here. usage.fetchUsage = async () => ({}); usage.exchangeRefresh = async (creds) => { exchanges.push(creds.refreshToken); return oauth(`r${exchanges.length}`); }; usage.fetchOwner = async (token) => owners[token] || null; }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); beforeEach(() => { fs.rmSync(path.join(HOME, ".claude"), { recursive: true, force: true }); fs.rmSync(path.join(HOME, ".claude.json"), { force: true }); fs.rmSync(path.join(TMP, ".claude", "agenthub-accounts"), { recursive: true, force: true }); exchanges.length = 0; owners = {}; }); describe("a pool login", () => { it("goes into a member's home as the member, never to a host pane", () => { pool.addLoginAccount("team", { oauth: oauth("a"), account: ACCOUNT, userID: "u-1" }); assert.equal(pool.forSpawn(null), null, "a host pane keeps the host's own login"); const spawn = pool.forSpawn(acct); assert.equal(spawn.name, "team"); assert.deepEqual(spawn.env, {}, "no token in the pane's env"); spawn.place(); assert.equal(diskCreds().refreshToken, oauth("a").refreshToken); assert.equal(fs.statSync(CREDS).mode & 0o777, 0o600); const state = JSON.parse(fs.readFileSync(path.join(HOME, ".claude.json"), "utf8")); assert.equal(state.hasCompletedOnboarding, true); assert.equal(state.oauthAccount.emailAddress, "team@example.com"); assert.ok(sudo.calls.some((c) => c.includes(CREDS) && c.includes("private"))); assert.ok( !sudo.calls.flat().some((x) => String(x).includes("sk-ant")), "no secret in sudo argv" ); }); it("is never written over a member's own login", () => { pool.addLoginAccount("team", { oauth: oauth("a"), account: ACCOUNT }); fs.mkdirSync(path.dirname(CREDS), { recursive: true }); fs.writeFileSync(CREDS, JSON.stringify({ claudeAiOauth: oauth("mine") })); assert.equal(pool.forSpawn(acct), null); assert.equal(diskCreds().refreshToken, oauth("mine").refreshToken); }); it("is refreshed by the server ahead of expiry and pushed into the home", async () => { pool.addLoginAccount("team", { oauth: oauth("a", 10 * 60e3), account: ACCOUNT }); pool.forSpawn(acct).place(); await pool.refreshLogins(); assert.deepEqual(exchanges, [oauth("a").refreshToken], "the server spent the refresh token"); assert.equal(pool.__read("team").oauth.refreshToken, oauth("r1").refreshToken); assert.equal(diskCreds().refreshToken, oauth("r1").refreshToken, "the home got the new pair"); await pool.refreshLogins(); assert.equal(exchanges.length, 1, "a fresh login is not refreshed again"); }); it("copies a home's own refresh back only when it is the same account", async () => { pool.addLoginAccount("team", { oauth: oauth("a"), account: ACCOUNT }); pool.forSpawn(acct).place(); // The member's Claude refreshed first. const theirs = oauth("cli", 9 * H); fs.writeFileSync(CREDS, JSON.stringify({ claudeAiOauth: theirs })); owners[theirs.accessToken] = { uuid: "uuid-team" }; await pool.refreshLogins(); assert.equal(pool.__read("team").oauth.refreshToken, theirs.refreshToken); // A login of another account is the member's own: dropped, never written over. const other = oauth("other", 10 * H); fs.writeFileSync(CREDS, JSON.stringify({ claudeAiOauth: other })); owners[other.accessToken] = { uuid: "uuid-someone-else" }; await pool.refreshLogins(); assert.equal(pool.__read("team").oauth.refreshToken, theirs.refreshToken); assert.deepEqual(pool.__read("team").placed, {}); assert.equal(diskCreds().refreshToken, other.refreshToken); }); it("is taken out of the homes it is still ours in when removed", () => { pool.addLoginAccount("team", { oauth: oauth("a"), account: ACCOUNT }); pool.forSpawn(acct).place(); pool.removeAccount("team"); assert.equal(fs.existsSync(CREDS), false); assert.equal(pool.__read("team"), null); }); it("refuses a second copy of one Claude account", () => { pool.addLoginAccount("team", { oauth: oauth("a"), account: ACCOUNT }); assert.throws(() => pool.addLoginAccount("team-2", { oauth: oauth("b"), account: ACCOUNT }), { code: "EEXISTS", }); assert.throws(() => pool.addLoginAccount("x", { oauth: { accessToken: "a" } }), { code: "EBADTOKEN", }); }); }); describe("moving a saved profile in from the Users page", () => { const profileDir = path.join(TMP, ".claude", "agenthub-profiles"); const writeProfile = (name, tag, uuid) => { fs.mkdirSync(profileDir, { recursive: true }); fs.writeFileSync( path.join(profileDir, `${name}.json`), JSON.stringify({ name, label: name, credentials: oauth(tag), account: { accountUuid: uuid, emailAddress: `${name}@example.com` }, }) ); }; async function call(method, p, body) { const login = await fetch(BASE + "/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "boss", password: "admin-pass-1" }), }); const cookie = login.headers.get("set-cookie").split(";")[0]; const res = await fetch(BASE + p, { method, headers: { "Content-Type": "application/json", cookie }, body: body ? JSON.stringify(body) : undefined, }); return { status: res.status, body: await res.json().catch(() => null) }; } it("moves it into the pool — the account this machine runs as with its LIVE login", async () => { writeProfile("work", "w", "uuid-work"); writeProfile("me", "m-saved", "uuid-me"); // This machine's own Claude Code is logged in as "me", and has refreshed // since the profile was saved: only the live pair still works. const hostCreds = path.join(TMP, ".claude", ".credentials.json"); fs.writeFileSync( hostCreds, JSON.stringify({ claudeAiOauth: oauth("m-live", 10 * 60e3), mcpOAuth: { keep: 1 } }) ); fs.writeFileSync( `${path.join(TMP, ".claude")}.json`, JSON.stringify({ oauthAccount: { accountUuid: "uuid-me" } }) ); const list = await call("GET", "/api/accounts/profiles"); assert.deepEqual( list.body.profiles.map((p) => [p.name, p.active]), [ ["me", true], ["work", false], ] ); assert.ok(!JSON.stringify(list.body).includes("sk-ant")); const moved = await call("POST", "/api/accounts", { tool: "claude", profile: "work", currentPassword: "admin-pass-1", }); assert.equal(moved.status, 201); assert.equal(moved.body.account.name, "work"); assert.ok(!JSON.stringify(moved.body).includes("sk-ant")); assert.equal(pool.__read("work").kind, "login"); assert.equal(fs.existsSync(path.join(profileDir, "work.json")), false, "one holder only"); const mine = await call("POST", "/api/accounts", { tool: "claude", profile: "me", currentPassword: "admin-pass-1", }); assert.equal(mine.status, 201); const rec = pool.__read("me"); assert.equal( rec.oauth.refreshToken, oauth("m-live").refreshToken, "the live pair, not the saved one" ); assert.deepEqual(Object.keys(rec.placed), ["@host"]); assert.equal(fs.existsSync(path.join(profileDir, "me.json")), false); // The server refreshes it and pushes the new pair into the host's own file, // keeping its other keys; removing the account leaves the host logged in. await pool.refreshLogins(); const host = JSON.parse(fs.readFileSync(hostCreds, "utf8")); assert.equal(host.claudeAiOauth.refreshToken, pool.__read("me").oauth.refreshToken); assert.notEqual(host.claudeAiOauth.refreshToken, oauth("m-live").refreshToken); assert.deepEqual(host.mcpOAuth, { keep: 1 }); assert.equal(fs.statSync(hostCreds).mode & 0o777, 0o600); pool.removeAccount("me"); assert.ok(fs.existsSync(hostCreds), "the host keeps its own login"); pool.removeAccount("work"); }); it("takes a pasted .credentials.json once Anthropic names its account", async () => { const pasted = oauth("p"); owners[pasted.accessToken] = { uuid: "uuid-pasted", email: "p@example.com" }; const r = await call("POST", "/api/accounts", { tool: "claude", name: "pasted", credentials: JSON.stringify({ claudeAiOauth: pasted }), currentPassword: "admin-pass-1", }); assert.equal(r.status, 201); assert.equal(pool.__read("pasted").account.accountUuid, "uuid-pasted"); }); });