Files
AgentHub/server/__tests__/server-s3-panes.test.js
T
nntrivi2001 943ba03b76 fix: v2.7.3 — Update now restarts through our own service unit, member panes stay live on a dev server, a start refuses a missing folder
- self-restart: a server in our unit's cgroup (/agenthub.service, system
  or user) restarts via `systemctl restart --no-block` (sudo -n for the
  system unit), our launchd job by exiting into KeepAlive. The detached
  restart-helper replacement held the port while Restart=always started
  the unit every 3 s (v2.7.2 on the S3 box: 240+ restarts, MainPID 0).
- run-as.mayHaveTmux: only ENOENT means "no tmux". A dev server in server
  mode without CAP_DAC_READ_SEARCH got EACCES on a member's 0700
  .agenthub/tmux, dropped the member from the list and ended their runs
  within seconds — the real cause of `401 not a live pane's token`.
- tmux: a member's list is re-asked every 5 s, not 1 s (one sudo + three
  auth.log lines each).
- POST /api/lanes/:id/start: 409 ENOCWD for a folder that no longer
  exists (the pane started in the account's home otherwise).
- Settings: Remote sources only for staff (a developer got a 403).
- scripts/test-server.js drops every inherited AGENTHUB_* (pane env).
Tests: self-restart.test.js, server-s3-panes (mayHaveTmux), lanes-api
(ENOCWD); touched suites 771 + client pages 191, tsc -b.
2026-10-12 14:04:08 +07:00

268 lines
11 KiB
JavaScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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-<name> 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.__resetConfCache();
assert.equal(runAs.sudoArgv(ACCT, "git", ["status"])[1][3], "/usr/bin/git");
assert.equal(runAs.sudoArgv(ACCT, "tmux", [])[1][3], "/opt/x/tmux");
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);
}
});
});