Files
AgentHub/server/__tests__/server-s3-system.test.js
T
nntrivi2001 84e2a9f0ac feat(server-s3): one Linux user per account (S3) — experimental
Server mode can now run each account as its own Linux user (ah-<name>)
when `agenthub server install-system` has set S3 up. The dashboard
reaches a member only through sudo -u / write-as / pane-launch, reads a
member's files only through owner- and type-checked opens, and hooks
are authenticated by peer uid. Local mode and server mode without S3
are unchanged.

EXPERIMENTAL: tested with fake sudo and fake accounts only. It has not
yet been verified on a real install with real member accounts, sudo or
a real Codex login. Do not offer it to real users yet.

- S3 core: provisioning, per-member tmux servers, the proxy key
  gateway, shared tools in /opt, dropped capabilities for children
  (P0–P5)
- P4: plan and quota per member home
- P6: ~/.claude.json seed; opt-in plugin baseline installed as the
  member; managed-settings spike script (not run)
- P7: Codex account pool (`agenthub accounts add --tool codex`)
- Roles merged with main's admin / analyst / developer: analysts write
  only their own rows, under S3 as well
- Per-account prefs: one implementation (Chat UI + onboarding)
- Security reviews F1–F49 (five rounds), each fix with a regression
  test: symlink/TOCTOU-safe member reads, no host writes in a member
  home, credentials and the data dir never served, owner and folder
  account must agree, pane tokens bound to the pane's uid, bounded
  sudo/tmux/git calls and member reads (no hang or crash from a member),
  async route rejections answer 500 instead of exiting
- Docs: docs/SERVER-MODE.md (S3 model, accepted ceilings, operator
  runbook), docs/plans/server-mode/S3-PLAN.md, CLAUDE.md S3 rules

Tests: server 2838/2841 (3 need 127.0.0.1:3000, held on this box);
client 1162/1162; tsc clean; headers check passes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 14:57:10 +07:00

282 lines
10 KiB
JavaScript

/**
* @file server-s3-system.test.js
* @description S3 plan, P2 (one Linux user per account), with every sudo and
* systemctl call faked: the system-unit service manager (host-owned env file,
* sudo for start/stop/restart, pure-fs staleness), service-manager picking it,
* Quit stopping it through sudo, the Linux-account layer (server.conf, name
* rule, the exact provision argv), user create provisioning FIRST, disable
* ending the logins first (and locking only an account that exists), and a
* member's bootstrap doing nothing.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
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.mkdtempSync(path.join(os.tmpdir(), "server-s3-system-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
process.env.AGENTHUB_SYSTEM_UNIT_DIR = path.join(TMP, "units");
process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf");
// `lin` has a Linux account; `pre` (made before S3) and `Old.Name` do not.
process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd");
fs.writeFileSync(process.env.AGENTHUB_PASSWD, "ah-lin:x:5101:5101::/:/bin/sh\n");
const sys = require("../../scripts/systemd-system-service");
const linux = require("../lib/linux-accounts");
const auth = require("../lib/auth");
const { db } = require("../db");
const UNIT = path.join(process.env.AGENTHUB_SYSTEM_UNIT_DIR, "agenthub.service");
const writeUnit = () => {
fs.mkdirSync(path.dirname(UNIT), { recursive: true });
fs.writeFileSync(UNIT, `[Unit]\n${sys.MARKER}\n`);
};
const writeConf = (uid = 4242) => {
linux.__resetConfCache();
fs.writeFileSync(
process.env.AGENTHUB_SERVER_CONF,
`HOST_USER=host\nHOST_UID=${uid}\nUSERS_ROOT=/srv/agenthub/users\nPORT=4820\n`
);
};
after(() => {
sys.__reset();
delete process.env.AGENTHUB_MODE;
fs.rmSync(TMP, { recursive: true, force: true });
});
describe("the system-unit service manager", () => {
let calls;
beforeEach(() => {
calls = [];
fs.rmSync(path.dirname(UNIT), { recursive: true, force: true });
fs.rmSync(sys.envPath(), { force: true });
sys.__setExecImpl(
(args) => {
calls.push(["systemctl", ...args]);
return { status: 0, stdout: args[0] === "is-active" ? "active\n" : "enabled\n" };
},
(args) => {
calls.push(["sudo", "-n", "systemctl", ...args]);
return { status: 0 };
}
);
});
it("is unavailable, and changes nothing, without its unit", () => {
assert.equal(sys.unitIsOurs("agenthub"), false);
assert.equal(sys.systemctlAvailable(), false);
assert.equal(sys.writeUserService({ argv: ["/n", "/r/server/index.js"] }), "unavailable");
assert.equal(sys.restartUserService("agenthub"), "unavailable");
assert.deepEqual(calls, []);
});
it("repoints by rewriting the host-owned env file, never the unit", () => {
writeUnit();
const unitBefore = fs.readFileSync(UNIT, "utf8");
const unit = {
argv: ["/usr/bin/node", "/plugins/v2/server/index.js"],
env: { NODE_ENV: "production" },
};
assert.equal(sys.writeUserService(unit), "written");
assert.equal(sys.writeUserService(unit), "unchanged");
const env = fs.readFileSync(sys.envPath(), "utf8");
assert.match(env, /^AGENTHUB_NODE=\/usr\/bin\/node$/m);
assert.match(env, /^AGENTHUB_ROOT=\/plugins\/v2$/m);
assert.match(env, /^NODE_ENV=production$/m);
assert.equal(fs.statSync(sys.envPath()).mode & 0o777, 0o600);
assert.equal(fs.readFileSync(UNIT, "utf8"), unitBefore);
assert.equal(sys.unitRoot("agenthub"), "/plugins/v2");
assert.equal(sys.unitIsStale("agenthub", "/plugins/v2"), false);
assert.equal(sys.unitIsStale("agenthub", "/plugins/v3"), true);
assert.ok(!calls.some((c) => c[0] === "sudo"), "no root needed to repoint");
});
it("restarts, starts and enables only through sudo, restart without blocking", () => {
writeUnit();
assert.equal(sys.restartUserService("agenthub"), "restarted");
assert.equal(sys.setUnitEnabled("agenthub", false), "ok");
sys.installUserService({ name: "agenthub", argv: ["/n", "/r/server/index.js"] });
const sudo = calls.filter((c) => c[0] === "sudo").map((c) => c.slice(3).join(" "));
assert.deepEqual(sudo, [
"restart --no-block agenthub.service",
"disable agenthub.service",
"start --no-block agenthub.service",
]);
assert.deepEqual(sys.stopArgv("agenthub"), [
"sudo",
["-n", "systemctl", "stop", "--no-block", "agenthub.service"],
]);
});
it("is what service-manager picks while the unit exists", () => {
const load = () => {
delete require.cache[require.resolve("../../scripts/service-manager")];
return require("../../scripts/service-manager");
};
if (process.platform !== "linux") return;
fs.rmSync(UNIT, { force: true });
assert.notEqual(load().kind, "systemd-system");
writeUnit();
assert.equal(load().kind, "systemd-system");
});
it("lets Quit stop it through sudo, never by SIGTERM", () => {
const { stopServer } = require("../lib/quit");
const spawned = [];
let killed = false;
stopServer({
svc: {
...sys,
systemctlAvailable: () => true,
unitIsOurs: () => true,
unitIsActive: () => true,
},
spawn: (cmd, args) => {
spawned.push([cmd, ...args]);
return { unref() {} };
},
kill: () => (killed = true),
});
assert.deepEqual(spawned, [
["sudo", "-n", "systemctl", "stop", "--no-block", "agenthub.service"],
]);
assert.equal(killed, false);
});
});
describe("Linux accounts", () => {
let argvs;
let failNext;
before(() => {
linux.__setRunImpl((argv) => {
argvs.push(argv);
if (failNext) {
failNext = false;
throw Object.assign(new Error("exit 4"), { stderr: "agenthub-provision: ah-x exists\n" });
}
return `ok ${argv[1]} ${argv[2]}\n`;
});
});
beforeEach(() => {
argvs = [];
failNext = false;
writeConf();
process.env.AGENTHUB_MODE = "server";
});
it("is off outside server mode or without server.conf", () => {
delete process.env.AGENTHUB_MODE;
assert.equal(linux.s3Enabled(), false);
assert.equal(linux.provision("create", "anyone"), null);
process.env.AGENTHUB_MODE = "server";
fs.rmSync(process.env.AGENTHUB_SERVER_CONF);
linux.__resetConfCache();
assert.equal(linux.s3Enabled(), false);
assert.equal(linux.accountFor("mia"), null);
assert.deepEqual(argvs, []);
});
it("reads server.conf and maps a user to ah-<name> under USERS_ROOT", () => {
assert.deepEqual(linux.serverConf(), {
hostUser: "host",
hostUid: 4242,
usersRoot: "/srv/agenthub/users",
port: 4820,
bins: {},
});
assert.deepEqual(linux.accountFor("mia"), {
login: "ah-mia",
home: "/srv/agenthub/users/mia",
});
});
it("refuses a name Linux cannot take, before any sudo", () => {
for (const bad of ["Mia", "1mia", "mi.a", "-x", "a".repeat(30), "root;id", "../x"]) {
assert.throws(() => linux.provision("create", bad), { code: "EBADUSER" }, bad);
}
assert.throws(() => linux.provision("delete", "mia"), /unknown provision action/);
assert.deepEqual(argvs, []);
});
it("calls exactly the provision helper with an action and a name", () => {
linux.provision("create", "mia");
linux.provision("disable", "mia");
assert.deepEqual(argvs, [
["/opt/agenthub/sbin/agenthub-provision", "create", "mia"],
["/opt/agenthub/sbin/agenthub-provision", "disable", "mia"],
]);
});
it("creates the Linux account BEFORE the user, and no user when that fails", async () => {
const made = await auth.createUser("lin", "member-pass-1", "developer");
assert.equal(made.username, "lin");
assert.deepEqual(argvs[0], ["/opt/agenthub/sbin/agenthub-provision", "create", "lin"]);
failNext = true;
await assert.rejects(auth.createUser("lux", "member-pass-1", "developer"), {
code: "EPROVISION",
message: /ah-x exists/,
});
assert.equal(db.prepare("SELECT 1 FROM users WHERE username = 'lux'").get(), undefined);
await assert.rejects(auth.createUser("Lux", "member-pass-1", "developer"), {
code: "EBADUSER",
});
});
it("ends the logins first on disable, then locks the Linux account", () => {
argvs = [];
failNext = true;
// A failed lock is still reported — but the dashboard access is gone.
assert.throws(() => auth.setDisabled("lin", true), { code: "EPROVISION" });
assert.equal(db.prepare("SELECT disabled FROM users WHERE username = 'lin'").get().disabled, 1);
auth.setDisabled("lin", false);
assert.equal(db.prepare("SELECT disabled FROM users WHERE username = 'lin'").get().disabled, 0);
auth.setDisabled("lin", true);
assert.equal(db.prepare("SELECT disabled FROM users WHERE username = 'lin'").get().disabled, 1);
assert.deepEqual(
argvs.map((a) => a[1]),
["disable", "enable", "disable"]
);
});
it("disables and enables a user who has no Linux account, with no sudo", () => {
argvs = [];
for (const n of ["pre", "Old.Name"]) {
db.prepare("INSERT INTO users (username, password_hash) VALUES (?, 'x')").run(n);
const id = db.prepare("SELECT id FROM users WHERE username = ?").get(n).id;
db.prepare(
"INSERT INTO auth_sessions (token_hash, user_id, expires_at) VALUES (?, ?, '2999-01-01')"
).run(`h-${n}`, id);
auth.setDisabled(n, true);
const row = db.prepare("SELECT disabled FROM users WHERE id = ?").get(id);
assert.equal(row.disabled, 1, n);
assert.equal(
db.prepare("SELECT 1 FROM auth_sessions WHERE user_id = ?").get(id),
undefined,
`${n}'s logins end`
);
auth.setDisabled(n, false);
assert.equal(db.prepare("SELECT disabled FROM users WHERE id = ?").get(id).disabled, 0, n);
}
assert.deepEqual(argvs, []);
});
});
describe("a member's plugin bootstrap", () => {
it("does nothing under another uid than the host's", () => {
const { notTheHost, bootstrap } = require("../../scripts/plugin-bootstrap");
writeConf(process.getuid() + 1);
assert.equal(notTheHost(), true);
assert.equal(bootstrap(path.join(TMP, "member-rt")), "not-host");
assert.equal(fs.existsSync(path.join(TMP, "member-rt")), false);
writeConf(process.getuid());
assert.equal(notTheHost(), false);
fs.rmSync(process.env.AGENTHUB_SERVER_CONF);
assert.equal(notTheHost(), false);
});
});