/** * @file server-s3-panes.test.js * @description S3 plan, P3a + review fix F1, with tmux and sudo faked: a * member's session lives on the member's own tmux server, and EVERY call on * it runs AS the member (sudo) — never as a host client on a socket the * member controls. A session's env and argv travel in a launch file written * as the member, a paste on stdin: nothing secret or private in sudo's argv. * A member's server is believed only about the member's own sessions. The * host's own sessions are untouched. A member pane's environment is the * member's (home, shared tools on PATH, no host desktop). The run-as path rule. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, beforeEach, after } = require("node:test"); const assert = require("node:assert/strict"); const os = require("node:os"); const path = require("node:path"); const fs = require("node:fs"); const TMP = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "server-s3-panes-"))); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); const tmux = require("../lib/tmux"); const runAs = require("../lib/run-as"); const linux = require("../lib/linux-accounts"); const auth = require("../lib/auth"); const { fakeSudo } = require("./fixtures/fake-sudo"); const HOME = path.join(TMP, "users", "mia"); const ACCT = { name: "mia", login: "ah-mia", home: HOME, uid: 5001 }; const SOCK = path.join(HOME, ".agenthub", "tmux", "sock"); const HANDOFF = path.join(HOME, ".agenthub", "handoff"); const MEMBER = "agenthub-lane-7"; const HOST = "agenthub-lane-8"; fs.mkdirSync(HANDOFF, { recursive: true }); let calls; // what the host's own tmux client ran let asMember; // [login, ...tmux args, stdin] run as the member (sudo) let sudo; beforeEach(() => { calls = []; asMember = []; sudo = fakeSudo(); runAs.__setExecImpl(sudo.exec); tmux.__setExecImpl((args) => { calls.push(args); if (args.includes("list-sessions")) return `${HOST}\nother\n`; return ""; }); tmux.__setAccountImpl( (name) => (name === MEMBER ? ACCT : null), (acct, args, input) => { asMember.push([acct.login, ...args, input]); // A server the member runs could claim another workspace's session. if (args.includes("list-sessions")) return `${MEMBER}\nagenthub-lane-9\n`; return ""; } ); }); after(() => { tmux.__reset(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("a member's panes on the member's own tmux server", () => { it("creates the session AS the member, env and argv only in the launch file", () => { tmux.newSession({ name: MEMBER, cwd: path.join(HOME, "p"), argv: ["claude", "--", "a private prompt"], env: { AGENTHUB_PANE_TOKEN: "ahpane_secret", CLAUDE_CODE_OAUTH_TOKEN: "sk-ant-oat-secret" }, }); assert.deepEqual(calls, [], "no host client ever touches a member's server"); const create = asMember.find((a) => a.includes("new-session")); const launch = create.slice(-3, -1); assert.deepEqual(create.slice(0, 9), [ "ah-mia", "new-session", "-d", "-s", MEMBER, "-c", path.join(HOME, "p"), "--", "/opt/agenthub/libexec/pane-launch", ]); assert.equal(path.dirname(launch[1]), HANDOFF); const body = fs.readFileSync(launch[1], "utf8").split("\0"); assert.deepEqual(body.slice(0, 6), [ "2", "AGENTHUB_PANE_TOKEN=ahpane_secret", "CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat-secret", "claude", "--", "a private prompt", ]); assert.equal(fs.statSync(launch[1]).mode & 0o777, 0o600); const logged = JSON.stringify([asMember, sudo.calls]); assert.ok(!/secret|private prompt/.test(logged), "sudo logs its argv"); assert.ok(asMember.some((a) => a.includes("mouse"))); }); it("runs every per-session call as the member; a paste goes in on stdin", () => { tmux.hasSession(MEMBER); tmux.sendText(MEMBER, "/reload-plugins"); tmux.killSession(MEMBER); tmux.paneCommand(MEMBER); tmux.sendCommand(MEMBER, ["codex"], { K: "v-secret" }, path.join(HOME, "p")); tmux.pasteText(MEMBER, "a private\nprompt"); assert.deepEqual(calls, []); const load = asMember.find((a) => a.includes("load-buffer")); assert.equal(load.at(-2), "-"); assert.equal(load.at(-1), "a private\nprompt"); const respawn = asMember.find((a) => a.includes("respawn-pane")); assert.ok(respawn.includes("/opt/agenthub/libexec/pane-launch")); assert.ok(!JSON.stringify(asMember.map((a) => a.slice(0, -1))).includes("secret")); }); it("attaches a terminal as the member, a host session with our own binary", () => { assert.deepEqual(tmux.memberAttach(MEMBER), [ "sudo", ["-n", "-u", "ah-mia", "tmux", "-S", SOCK, "attach-session", "-t", `=${MEMBER}`], ]); assert.equal(tmux.memberAttach(HOST), null); }); it("leaves the host's own sessions on the default server", () => { tmux.newSession({ name: HOST, cwd: "/tmp", argv: ["bash"], env: {} }); tmux.killSession(HOST); assert.deepEqual(asMember, []); assert.ok(calls.every((a) => a[0] !== "-S")); }); it("lists sessions across every server, believing a member's only about their own", async () => { const orig = runAs.membersWithTmux; runAs.membersWithTmux = () => [ACCT]; try { // A member's list is async: until it lands their panes are UNKNOWN. const cold = tmux.liveSessions(); assert.deepEqual([...cold].sort(), [HOST, "other"].sort()); assert.equal(cold.has(MEMBER), true, "unknown, kept"); assert.equal(cold.has("agenthub-lane-9"), false); assert.deepEqual(tmux.listSessions("agenthub-lane-"), [HOST]); await tmux.__settle(); assert.deepEqual([...tmux.liveSessions()].sort(), [MEMBER, HOST, "other"].sort()); assert.deepEqual(tmux.listSessions("agenthub-lane-").sort(), [HOST, MEMBER].sort()); // A session this process ends or makes is in the next answer at once. tmux.killSession(MEMBER); assert.deepEqual(tmux.listSessions("agenthub-lane-"), [HOST]); tmux.newSession({ name: MEMBER, cwd: HOME, argv: ["bash"], env: {} }); assert.deepEqual(tmux.listSessions("agenthub-lane-").sort(), [HOST, MEMBER].sort()); } finally { runAs.membersWithTmux = orig; } }); it("never falls back to the host's server for a member with no account", () => { tmux.__setAccountImpl(() => { throw Object.assign(new Error("gone"), { code: "EMEMBERGONE" }); }); assert.throws(() => tmux.newSession({ name: MEMBER, cwd: HOME, argv: ["bash"], env: {} }), { code: "EMEMBERGONE", }); assert.deepEqual(calls, []); }); }); describe("a member pane's environment", () => { it("is the member's home and identity, the shared tools, no host desktop", () => { const env = runAs.memberPaneEnv( ACCT, { AGENTHUB_RUN_ID: MEMBER, PATH: "/home/host/.local/bin:/usr/bin", DISPLAY: ":0", XDG_RUNTIME_DIR: "/run/user/1001", CODEX_HOME: "/home/host/.codex", ANTHROPIC_AUTH_TOKEN: "ahpane_x", }, { codex: true } ); assert.equal(env.HOME, HOME); assert.equal(env.USER, "ah-mia"); assert.equal(env.PATH, "/opt/agenthub/tools/bin:/usr/local/bin:/usr/bin:/bin"); assert.equal(env.CODEX_HOME, path.join(HOME, ".codex")); assert.equal(env.DISABLE_AUTOUPDATER, "1"); assert.match(env.CLAUDE_DASHBOARD_PORT, /^\d+$/); assert.equal(env.DISPLAY, undefined); assert.equal(env.XDG_RUNTIME_DIR, undefined); assert.equal(env.AGENTHUB_RUN_ID, MEMBER); assert.equal(env.ANTHROPIC_AUTH_TOKEN, "ahpane_x"); }); }); describe("run-as: a path under the users root belongs to that user", () => { it("maps only enabled, provisioned users; null outside S3 or the root; refuses a gone one", async () => { process.env.AGENTHUB_MODE = "server"; fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=1\nUSERS_ROOT=${TMP}/users\nPORT=4820\n` ); linux.__resetConfCache(); linux.__setRunImpl(() => "ok"); try { await auth.createUser("root", "member-pass-1", "developer"); // a name, not uid 0 // `ah-root` is not in /etc/passwd on a test box: not provisioned — a // folder of theirs is an error, never "the host runs it". const gone = { code: "EMEMBERGONE" }; assert.throws(() => runAs.accountForPath(`${TMP}/users/root/p`), gone); assert.throws(() => runAs.accountForPath(`${TMP}/users/../users/x`), gone); assert.equal(runAs.accountForPath("/etc"), null); assert.equal(runAs.accountForPath(`${TMP}/users`), null); delete process.env.AGENTHUB_MODE; assert.equal(runAs.accountForPath(`${TMP}/users/root/p`), null); assert.equal(runAs.accountForSession(MEMBER), null); } finally { delete process.env.AGENTHUB_MODE; } }); it("names a member by uid only for ah- accounts", () => { assert.equal(runAs.memberNameForUid(0), null); // root assert.equal(runAs.memberNameForUid(process.getuid()), null); // the host }); it("builds sudo argv with env assignments before the command", () => { assert.deepEqual(runAs.sudoArgv(ACCT, "git", ["status"], { GIT_TERMINAL_PROMPT: "0" }), [ "sudo", ["-n", "-u", "ah-mia", "GIT_TERMINAL_PROMPT=0", "git", "status"], ]); assert.throws(() => runAs.sudoArgv(ACCT, "git", [], { "-E": "x" })); assert.throws(() => runAs.sudoArgv(ACCT, "git", [], { "A=B": "x" })); }); it("names the binary by the absolute path server.conf records (what sudoers lists)", () => { fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=1\nUSERS_ROOT=${TMP}/users\nGIT_BIN=/usr/bin/git\nTMUX_BIN=/opt/x/tmux\n` ); linux.__setTmuxMember(path.join(TMP, "no-tmux-member")); assert.equal(runAs.sudoArgv(ACCT, "git", ["status"])[1][3], "/usr/bin/git"); assert.equal(runAs.sudoArgv(ACCT, "tmux", [])[1][3], "/opt/x/tmux"); // Once install-system put tmux-member there, a member's tmux goes through // it (their running server's own binary); the installed one is still known. const wrapper = path.join(TMP, "tmux-member"); fs.writeFileSync(wrapper, "#!/bin/sh\n", { mode: 0o755 }); linux.__setTmuxMember(wrapper); try { assert.equal(runAs.sudoArgv(ACCT, "tmux", [])[1][3], wrapper); assert.equal(linux.serverConf().tmuxInstalled, "/opt/x/tmux"); } finally { linux.__setTmuxMember(path.join(TMP, "no-tmux-member")); } assert.equal( runAs.sudoArgv(ACCT, "/opt/agenthub/libexec/write-as", [])[1][3], "/opt/agenthub/libexec/write-as" ); }); it("counts a member's tmux it cannot stat as maybe there, a missing one as none", () => { const home = path.join(TMP, "users", "zoe"); const acct = { name: "zoe", login: "ah-zoe", home, uid: 5002 }; assert.equal(runAs.mayHaveTmux(acct), false, "no .agenthub at all"); const dir = path.join(home, ".agenthub", "tmux"); fs.mkdirSync(dir, { recursive: true }); assert.equal(runAs.mayHaveTmux(acct), false, "no socket yet"); // The member's own 0700 dir, seen by a dashboard without CAP_DAC_READ_SEARCH. fs.chmodSync(dir, 0o000); try { if (process.getuid() !== 0) assert.equal(runAs.mayHaveTmux(acct), true, "EACCES proves nothing"); } finally { fs.chmodSync(dir, 0o700); } }); });