/** * @file drop-caps.js * @description Server mode, one Linux user per account (S3): the system unit * gives the dashboard CAP_DAC_READ_SEARCH as an AMBIENT capability (it reads * members' 0600 transcripts), and Linux passes an ambient capability on to * every program a process execs — the host's tmux server and every agent in * a host pane, git and its hooks, npm installs. None of them may read * everything. So, when this process holds ambient capabilities, every child * starts through `setpriv --inh-caps=-all --ambient-caps=-all --`, which * clears them before the real program runs. Patched like windows-hide.js: * `ChildProcess.prototype.spawn` for every async form, the sync exports by * hand (they never reach that prototype). sudo needs no help (setuid clears * them). node-pty forks natively, outside child_process: its callers take the * argv from `ptyArgv` (pty-attach.js — a host `tmux attach` client could * otherwise run `detach-client -E ` with the capability). A no-op * anywhere else. * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("node:fs"); const DONE = Symbol.for("agenthub.dropCaps"); const SETPRIV = "/usr/bin/setpriv"; const FLAGS = ["--inh-caps=-all", "--ambient-caps=-all", "--"]; let active = false; // install() patched child_process: wrap native spawns too function hasAmbientCaps(status = null) { try { const text = status ?? fs.readFileSync("/proc/self/status", "utf8"); const m = /^CapAmb:\s*([0-9a-f]+)$/m.exec(text); return !!m && /[1-9a-f]/.test(m[1]); } catch { return false; } } /** Node's own shell rule (child_process normalizeSpawnArguments), for the sync forms. */ function viaShell(file, args, shell) { const sh = typeof shell === "string" ? shell : "/bin/sh"; return [sh, ["-c", [file, ...args].join(" ")]]; } function install(cp = require("node:child_process"), status = null) { if (process.platform !== "linux" || cp[DONE] || !hasAmbientCaps(status)) return false; if (!fs.existsSync(SETPRIV)) { console.warn("[drop-caps] setpriv is missing: children inherit CAP_DAC_READ_SEARCH"); return false; } cp[DONE] = true; active = true; const spawn = cp.ChildProcess.prototype.spawn; cp.ChildProcess.prototype.spawn = function (options) { // options.args[0] is argv0; setpriv execs the file under its own path. options.args = ["setpriv", ...FLAGS, options.file, ...options.args.slice(1)]; options.file = SETPRIV; return spawn.call(this, options); }; // spawnSync / execFileSync (file, args?, opts?) and execSync (cmd, opts?). const wrap = (fn) => function (file, args, opts) { if (!Array.isArray(args)) [args, opts] = [[], args]; let [f, a] = [file, args || []]; if (opts?.shell) [f, a] = viaShell(f, a, opts.shell); return fn.call(this, SETPRIV, [...FLAGS, f, ...a], opts ? { ...opts, shell: false } : opts); }; cp.spawnSync = wrap(cp.spawnSync); cp.execFileSync = wrap(cp.execFileSync); const execFileSync = cp.execFileSync; cp.execSync = function (cmd, opts) { return execFileSync.call(this, cmd, [], { ...(opts || {}), shell: opts?.shell || true }); }; return true; } /** `[file, args]` for a spawn that bypasses child_process (node-pty): via setpriv once installed. */ function ptyArgv(file, args = []) { return active ? [SETPRIV, [...FLAGS, file, ...args]] : [file, args]; } module.exports = { install, hasAmbientCaps, ptyArgv };