/** * @file server-s3-handoff.test.js * @description S3 plan, P3c: what a member's pane must read lives in the * member's handoff dir, never the dashboard's data dir (which the member's * account cannot reach): the workspace prompt and context files, the AI Proxy * settings for Claude (no key — the gateway), Codex's provider as `-c` * overrides (no file in the member's CODEX_HOME), and files dropped into the * prompt box. Every one is written AS the member (`write-as`, played by * fixtures/fake-sudo.js), never by the dashboard itself. A fake passwd 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-handoff-"))); const USERS = path.join(TMP, "users"); const HOME = path.join(USERS, "mia"); const HANDOFF = path.join(HOME, ".agenthub", "handoff"); 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.CODEX_HOME = path.join(TMP, ".codex"); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; process.env.LANES_ROOT = path.join(TMP, "lanes"); 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 proxies = require("../lib/ai-proxies"); const { proxyForSpawn, resolveProxy } = require("../lib/proxy-apply"); const { launchExtras } = require("../lib/lane-sources"); const { fakeSudo } = require("./fixtures/fake-sudo"); const sudo = fakeSudo(); let server; let BASE; let laneId; let acct; let profile; before(async () => { fs.mkdirSync(path.join(HOME, "proj"), { recursive: true }); fs.mkdirSync(HANDOFF, { recursive: true }); fs.writeFileSync(process.env.AGENTHUB_PASSWD, "ah-mia:x:5001:5001::/:/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}`; const mia = await auth.createUser("mia", "member-pass-1", "developer"); laneId = Number( db .prepare( "INSERT INTO lanes (cwd, tool, owner_id, instructions) VALUES (?, 'claude', ?, 'Be brief.')" ) .run(path.join(HOME, "proj"), mia.id).lastInsertRowid ); const ctxFile = path.join(TMP, "data", "workspace-context", String(laneId), "spec.md"); fs.mkdirSync(path.dirname(ctxFile), { recursive: true }); fs.writeFileSync(ctxFile, "# spec\n"); db.prepare( "INSERT INTO lane_sources (lane_id, kind, path, name, added_at) VALUES (?, 'file', ?, 'spec.md', 'now')" ).run(laneId, ctxFile); acct = runAs.accountForLane(laneId); profile = proxies.createProxy({ label: "Team GW", baseUrl: "http://127.0.0.1:9", apiKey: "sk-real-key", model: "m1", }); }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); const mode = (f) => fs.statSync(f).mode & 0o777; describe("a member pane's files live in the member's handoff dir", () => { it("resolves the workspace to its member", () => { assert.equal(acct?.login, "ah-mia"); }); it("refuses a NUL inside a pane's argv or env: it would split the launch file", () => { assert.throws( () => runAs.launchArgv(acct, {}, ["claude", "hi\0--dangerously-skip-permissions"]), { code: "EBADARG", } ); assert.throws(() => runAs.launchArgv(acct, { A: "x\0y" }, ["claude"]), { code: "EBADARG" }); }); it("copies the context files and writes the prompt there, readable", () => { const x = launchExtras(laneId, "claude"); const ctx = path.join(HANDOFF, `context-${laneId}`); assert.deepEqual(x.addDirs, [ctx]); assert.equal(fs.readFileSync(path.join(ctx, "spec.md"), "utf8"), "# spec\n"); assert.equal(mode(path.join(ctx, "spec.md")), 0o644); assert.equal(x.systemPromptFile, path.join(HANDOFF, `prompt-${laneId}`, "CLAUDE.md")); assert.equal(mode(x.systemPromptFile), 0o644); const text = fs.readFileSync(x.systemPromptFile, "utf8"); assert.match(text, /Be brief\./); assert.ok(text.includes(path.join(ctx, "spec.md")), "the prompt names the copy"); assert.ok(!text.includes(path.join(TMP, "data")), "never the data dir"); const writes = sudo.calls.filter((c) => c.includes("/opt/agenthub/libexec/write-as")); assert.ok(writes.length >= 3, "copies and prompt written as the member"); assert.ok(writes.every((c) => c.includes("ah-mia"))); assert.ok(!writes.flat().some((a) => a.includes("Be brief")), "content never in argv"); }); it("gives Claude a keyless settings file of the pane's own in the handoff dir", () => { const spawn = proxyForSpawn( "claude", resolveProxy("claude", profile.name), "ahpane_t", acct, "agenthub-lane-1" ); const file = spawn.argv[0].replace(/^--settings=/, ""); assert.equal(file, path.join(HANDOFF, `proxy-${profile.name}--agenthub-lane-1.json`)); assert.equal(mode(file), 0o600); const text = fs.readFileSync(file, "utf8"); assert.ok(!text.includes("sk-real-key")); assert.match(JSON.parse(text).env.ANTHROPIC_BASE_URL, /\/api\/proxy-gw\//); assert.equal(JSON.parse(text).env.ANTHROPIC_AUTH_TOKEN, "ahpane_t"); assert.deepEqual(spawn.env, { ANTHROPIC_AUTH_TOKEN: "ahpane_t" }); }); it("gives Codex the provider as -c overrides, never a file in CODEX_HOME", () => { const spawn = proxyForSpawn("codex", resolveProxy("codex", profile.name), "ahpane_t", acct); assert.ok(!spawn.argv.includes("-p")); const joined = spawn.argv.join(" "); assert.match(joined, new RegExp(`model_providers\\.${profile.name}=\\{`)); assert.match(joined, /base_url="http:\/\/127\.0\.0\.1:\d+\/api\/proxy-gw\/[^"]+\/v1"/); assert.match(joined, /env_key="AGENTHUB_PROXY_KEY"/); assert.ok(!joined.includes("sk-real-key")); assert.ok(!fs.existsSync(path.join(process.env.CODEX_HOME, `${profile.name}.config.toml`))); assert.deepEqual(spawn.env, { AGENTHUB_PROXY_KEY: "ahpane_t" }); }); it("saves a member's dropped file in their handoff dir", async () => { const login = await fetch(`${BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "mia", password: "member-pass-1" }), }); const cookie = login.headers.get("set-cookie").split(";")[0]; const res = await fetch(`${BASE}/api/run/drop-image`, { method: "POST", headers: { "Content-Type": "image/png", cookie }, body: Buffer.from("89504e47", "hex"), }); assert.equal(res.status, 200); const { path: saved } = await res.json(); assert.equal(path.dirname(saved), path.join(HANDOFF, "drops")); assert.equal(mode(saved), 0o644); }); });