/** * @file server-s3-reads.test.js * @description S3 plan, P4: a member's tool data lives under their own home, * and the dashboard reads it there — a session in a member's folder finds its * transcript in `/.claude/projects` (by cwd, and by id within that home * when its name is unknown), the transcript route serves it, the session sync sweeps every * member's projects dir, and Codex's rollouts are read from every member's * CODEX_HOME. A member's file is read only when the OPEN file is a regular * file that member owns (review fix F3): never a FIFO, never a link to * another account's file. A fake passwd (mia = this test's uid) and * server.conf make `mia` a provisioned member. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, 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(), "server-s3-reads-"))); const USERS = path.join(TMP, "users"); const HOME = path.join(USERS, "mia"); const PROJ = path.join(HOME, "proj"); const SID = "11111111-2222-4333-8444-555555555555"; process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; process.env.LANES_ROOT = path.join(TMP, "lanes"); process.env.CLAUDE_HOME = path.join(TMP, "host-claude"); process.env.CODEX_HOME = path.join(TMP, "host-codex"); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd"); const { createApp, startServer } = require("../index"); const dbModule = require("../db"); const auth = require("../lib/auth"); const linux = require("../lib/linux-accounts"); const runAs = require("../lib/run-as"); const claudeHome = require("../lib/claude-home"); const TRANSCRIPT = path.join( HOME, ".claude", "projects", claudeHome.encodeCwd(PROJ), `${SID}.jsonl` ); let server; let BASE; before(async () => { fs.mkdirSync(path.dirname(TRANSCRIPT), { recursive: true }); const ts = new Date().toISOString(); fs.writeFileSync( TRANSCRIPT, [ { type: "user", sessionId: SID, cwd: PROJ, timestamp: ts, uuid: "u1", message: { role: "user", content: "say hi" }, }, { type: "assistant", sessionId: SID, cwd: PROJ, timestamp: ts, uuid: "a1", message: { role: "assistant", model: "claude-sonnet-4-5", content: [{ type: "text", text: "pong" }], usage: { input_tokens: 5, output_tokens: 1 }, }, }, ] .map((l) => JSON.stringify(l)) .join("\n") + "\n", { mode: 0o600 } ); 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"); server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; process.env.AGENTHUB_MODE = "server"; await auth.createUser("boss", "admin-pass-1", "admin"); await auth.createUser("mia", "member-pass-1", "developer"); }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("a member's Claude transcripts are found under their own home", () => { it("resolves by cwd and by id", () => { assert.equal(claudeHome.projectsDirFor(PROJ), path.join(HOME, ".claude", "projects")); assert.equal(claudeHome.projectsDirFor(TMP), path.join(TMP, "host-claude", "projects")); assert.equal(claudeHome.getTranscriptPath(SID, PROJ), TRANSCRIPT); assert.equal(claudeHome.findTranscriptPath(SID, PROJ), TRANSCRIPT); assert.ok(claudeHome.allProjectsDirs().includes(path.join(HOME, ".claude", "projects"))); }); it("is imported by the session sync's sweep of every projects dir", async () => { const { syncDefaultProjects } = require("../../scripts/import-history"); const { changed } = await syncDefaultProjects(dbModule, { projectsDir: path.join(HOME, ".claude", "projects"), }); assert.ok(changed.some((c) => c.sessionId === SID)); const row = dbModule.db.prepare("SELECT cwd FROM sessions WHERE id = ?").get(SID); assert.equal(row.cwd, PROJ); }); it("is served by the transcript route", async () => { 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}/api/sessions/${SID}/transcript`, { headers: { cookie } }); const body = await res.json(); assert.equal(res.status, 200); assert.ok(body.total >= 2, JSON.stringify(body).slice(0, 200)); assert.ok(JSON.stringify(body.messages).includes("pong")); }); }); describe("a member's Codex rollouts are read from their own CODEX_HOME", () => { it("lists them beside this account's", () => { const day = path.join(HOME, ".codex", "sessions", "2026", "10", "08"); fs.mkdirSync(day, { recursive: true }); const f = path.join( day, "rollout-2026-10-08T00-00-00-aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee.jsonl" ); fs.writeFileSync(f, "{}\n"); const { allRolloutFiles } = require("../lib/codex-ingest"); const members = runAs.memberHomes().map((h) => path.join(h, ".codex")); assert.deepEqual(members, [path.join(HOME, ".codex")]); assert.ok(allRolloutFiles(members[0]).some((r) => r.path === f)); }); }); describe("a member's file is read only if it is theirs", () => { it("refuses a FIFO without blocking, and a file another account owns", () => { const fifo = path.join(HOME, "pipe.jsonl"); require("node:child_process").execFileSync("mkfifo", [fifo]); assert.throws(() => runAs.readFileSafe(fifo, "utf8"), { code: "EFOREIGNFILE" }); assert.equal(runAs.readFileSafe(TRANSCRIPT, "utf8").includes("pong"), true); // A link out of the home is read through, and must still be mia's file. const outside = path.join(TMP, "host-secret.txt"); fs.writeFileSync(outside, "host only"); const link = path.join(HOME, "link.jsonl"); fs.symlinkSync(outside, link); fs.writeFileSync(process.env.AGENTHUB_PASSWD, "ah-mia:x:5001:5001::/:/bin/sh\n"); runAs.__resetCache(); try { assert.throws(() => runAs.readFileSafe(link, "utf8"), { code: "EFOREIGNFILE" }); assert.throws(() => runAs.readFileSafe(TRANSCRIPT, "utf8"), { code: "EFOREIGNFILE" }); assert.equal(runAs.readFileSafe(outside, "utf8"), "host only", "a host path reads as before"); } finally { const uid = process.getuid(); fs.writeFileSync(process.env.AGENTHUB_PASSWD, `ah-mia:x:${uid}:${uid}::/:/bin/sh\n`); runAs.__resetCache(); } }); });