/** * @file server-s3-hooks.test.js * @description S3 plan, P3d: a hook is authenticated by the uid behind its * loopback connection (`/proc/net/tcp*`, faked here). A member may report only * work in their own home — not another member's folder, session or pane, and * no transcript outside it; an account that is neither the host nor a member * is refused; the host is unchanged. A member's session stays alive while its * pane lives (the dashboard cannot read another uid's /proc cwd). * @author Nguyễn Ngọc Trí Vĩ */ 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(), "server-s3-hooks-"))); const USERS = path.join(TMP, "users"); const MIA = path.join(USERS, "mia"); const MAX = path.join(USERS, "max"); 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.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd"); const { createApp, startServer } = require("../index"); const { db } = require("../db"); const auth = require("../lib/auth"); const linux = require("../lib/linux-accounts"); const runAs = require("../lib/run-as"); const hookAuth = require("../lib/hook-auth"); const tmux = require("../lib/tmux"); const hooksRouter = require("../routes/hooks"); let server; let BASE; let peer = 5001; // mia const post = (data, extra = {}) => fetch(`${BASE}/api/hooks/event`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ hook_type: "UserPromptSubmit", data, ...extra }), }); before(async () => { for (const d of [MIA, MAX]) fs.mkdirSync(path.join(d, "proj"), { recursive: true }); fs.writeFileSync( process.env.AGENTHUB_PASSWD, "ah-mia:x:5001:5001::/:/bin/sh\nah-max:x:5002:5002::/:/bin/sh\nother:x:5003:5003::/:/bin/sh\n" ); fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=${process.getuid()}\nUSERS_ROOT=${USERS}\nPORT=4820\n` ); linux.__resetConfCache(); runAs.__resetCache(); linux.__setRunImpl(() => "ok"); hookAuth.__setPeerUid(() => peer); server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; process.env.AGENTHUB_MODE = "server"; await auth.createUser("mia", "member-pass-1", "developer"); const max = await auth.createUser("max", "member-pass-2", "developer"); const lane = db .prepare("INSERT INTO lanes (cwd, tool, owner_id) VALUES (?, 'claude', ?)") .run(path.join(MAX, "proj"), max.id).lastInsertRowid; db.prepare( "INSERT INTO sessions (id, name, status, cwd) VALUES ('s-max', 'max', 'active', ?)" ).run(path.join(MAX, "proj")); db.prepare( "INSERT INTO dashboard_runs (id, lane_id, cwd, mode, status) VALUES ('agenthub-lane-777', ?, ?, 'interactive', 'running')" ).run(lane, path.join(MAX, "proj")); }); beforeEach(() => { peer = 5001; }); after(() => { hookAuth.__setPeerUid(null); tmux.__reset(); delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("peerUid reads the client end of the connection from /proc/net/tcp", () => { it("matches the client's port, our port and ESTABLISHED", () => { hookAuth.__setTables(() => [ " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid\n" + " 0: 0100007F:12D4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1001\n" + " 1: 0100007F:C7F2 0100007F:12D4 01 00000000:00000000 00:00000000 00000000 5001\n" + " 2: 0100007F:12D4 0100007F:C7F2 01 00000000:00000000 00:00000000 00000000 1001\n", "", ]); assert.equal(hookAuth.peerUid({ remotePort: 0xc7f2, localPort: 0x12d4 }), 5001); assert.equal(hookAuth.peerUid({ remotePort: 0xc7f3, localPort: 0x12d4 }), null); }); }); describe("a member's hooks report only their own work", () => { it("ingests a hook from a member's own folder", async () => { const res = await post({ session_id: "s-mia-1", cwd: path.join(MIA, "proj"), prompt: "hi" }); assert.equal(res.status, 200); assert.ok(db.prepare("SELECT 1 FROM sessions WHERE id = 's-mia-1'").get()); }); it("refuses a hook claiming another member's folder", async () => { const res = await post({ session_id: "s-mia-2", cwd: path.join(MAX, "proj"), prompt: "x" }); assert.equal(res.status, 403); assert.equal((await res.json()).error.code, "EFOREIGN"); assert.equal(db.prepare("SELECT 1 FROM sessions WHERE id = 's-mia-2'").get(), undefined); }); it("refuses a hook naming another member's session", async () => { const res = await post({ session_id: "s-max", cwd: path.join(MIA, "proj"), prompt: "x" }); assert.equal(res.status, 403); }); it("drops a transcript path outside the member's home", async () => { const res = await post({ session_id: "s-mia-3", cwd: path.join(MIA, "proj"), transcript_path: path.join(MAX, ".claude", "projects", "x", "s.jsonl"), prompt: "x", }); assert.equal(res.status, 200); const row = db.prepare("SELECT transcript_path FROM sessions WHERE id = 's-mia-3'").get(); assert.ok(!row?.transcript_path, "never another account's file"); }); it("never binds a member's session to another member's pane", async () => { const res = await post( { session_id: "s-mia-4", cwd: path.join(MIA, "proj"), prompt: "x" }, { run_id: "agenthub-lane-777" } ); assert.equal(res.status, 200); const run = db.prepare("SELECT session_id FROM dashboard_runs WHERE id = 'agenthub-lane-777'"); assert.equal(run.get().session_id, null); }); it("refuses an account that is neither the host nor a member", async () => { peer = 5003; const res = await post({ session_id: "s-x", cwd: path.join(MIA, "proj"), prompt: "x" }); assert.equal(res.status, 403); assert.equal((await res.json()).error.code, "EPEER"); }); it("leaves the host's own hooks as they were", async () => { peer = process.getuid(); const res = await post({ session_id: "s-host", cwd: TMP, prompt: "x" }); assert.equal(res.status, 200); }); }); describe("a member's session is alive while its pane lives", () => { it("keeps it with a live pane, settles it once the pane is gone", () => { const cwd = path.join(MIA, "proj"); db.prepare( "INSERT INTO sessions (id, name, status, cwd, tool) VALUES ('s-live', 'l', 'active', ?, 'claude')" ).run(cwd); db.prepare( "INSERT INTO dashboard_runs (id, cwd, mode, status, session_id) VALUES ('agenthub-lane-778', ?, 'interactive', 'running', 's-live')" ).run(cwd); let panes = "agenthub-lane-778\n"; tmux.__setExecImpl((args) => (args.includes("list-sessions") ? panes : "")); const status = () => db.prepare("SELECT status FROM sessions WHERE id = 's-live'").get().status; const probe = { available: true, cwds: new Set() }; hooksRouter.livenessReap({ ignoreIdleGate: true, probe }); assert.equal(status(), "active"); panes = ""; tmux.__setExecImpl((args) => (args.includes("list-sessions") ? panes : "")); hooksRouter.livenessReap({ ignoreIdleGate: true, probe }); assert.equal(status(), "completed"); }); });