/** * @file member-policy.js * @description Server mode, one Linux user per account (S3): the admin's * switch for what members' Claude may run on this box — today whether test * runners and container commands are allowed (`allowTests`, off by default). * Kept in `/member-policy.json` (/opt/agenthub/app, host-owned, * world-readable), because `scripts/member-guard.js` runs AS the member and * reads it from there; a missing or unreadable file means the default, so * tests stay blocked unless an admin turned them on. * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("node:fs"); const path = require("node:path"); const FILE = "member-policy.json"; const DEFAULTS = Object.freeze({ allowTests: false }); const appRoot = () => process.env.AGENTHUB_APP_ROOT || "/opt/agenthub/app"; const file = () => path.join(appRoot(), FILE); /** The policy in force (defaults for anything missing or unreadable). */ function read(p = file()) { try { const j = JSON.parse(fs.readFileSync(p, "utf8")); return { allowTests: j?.allowTests === true }; } catch { return { ...DEFAULTS }; } } /** Does it apply here? Only S3 runs members' panes as their own accounts. */ function available() { try { return require("./linux-accounts").s3Enabled(); } catch { return false; } } function state() { return { available: available(), ...read() }; } /** Writes the switch (atomically, 0644: members' hooks read it). */ function setAllowTests(on) { const next = { ...read(), allowTests: on === true }; fs.mkdirSync(appRoot(), { recursive: true }); const tmp = `${file()}.${process.pid}.tmp`; fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", { mode: 0o644 }); fs.chmodSync(tmp, 0o644); fs.renameSync(tmp, file()); return state(); } module.exports = { FILE, DEFAULTS, read, available, state, setAllowTests };