/** * @file session-account.js * @description Which Linux account a session ran as (`sessions.account`: "" * = the host's, else an S3 member's name), and whose bill a session no * workspace owns goes on. A workspace owns a session through `lane_id` or a * `dashboard_runs` row (server/lib/scope.js); a session started OUTSIDE every * workspace — a script's `claude -p`, a CLI typed in a plain terminal — has * neither, and without this its spend counted for nobody: not in anyone's * Today, the Users page or a spend quota. * * The account is the hook's PEER (server/lib/hook-auth.js: the uid of the * loopback connection, never the body) when a hook creates the session; a * session no hook made (the Codex/Antigravity/Cursor/omp pollers, history) * gets it from its folder, the S3 rule `accountForPath` already applies to * every pane. A pushed session (another machine's) never gets one. A member * account bills to the user of that name; the host account to the admin an * admin marked as its owner (`users.host_account`), or — with no mark — the * only enabled admin, if there is exactly one. * @author Nguyễn Ngọc Trí Vĩ */ const sql = (q) => require("../db").db.prepare(q); // The user the host account's sessions bill to (null: nobody marked and not exactly one admin). const HOST_OWNER = `COALESCE( (SELECT MIN(id) FROM users WHERE host_account = 1 AND role = 'admin'), (SELECT MIN(id) FROM users WHERE role = 'admin' AND disabled = 0 HAVING COUNT(*) = 1))`; // A session no workspace owns: neither stamped onto an owned lane nor held by // a run of one — exactly the complement of scope.js's ownership. const UNOWNED = `(s.lane_id IS NULL OR s.lane_id NOT IN (SELECT id FROM lanes WHERE owner_id IS NOT NULL)) AND s.id NOT IN (SELECT r.session_id FROM dashboard_runs r JOIN lanes l ON l.id = r.lane_id WHERE r.session_id IS NOT NULL AND l.owner_id IS NOT NULL)`; /** `session_id` → `uid` for every session billed by its account (no workspace owns it). */ const ACCOUNT_OWNED = ` SELECT s.id AS session_id, u.id AS uid FROM sessions s JOIN users u ON u.id = CASE WHEN s.account = '' THEN ${HOST_OWNER} ELSE (SELECT id FROM users WHERE username = s.account) END WHERE s.account IS NOT NULL AND ${UNOWNED}`; /** The account a hook's peer names: "" for the host, a member's name, null otherwise. */ function accountOfPeer(peer) { if (!peer || peer.kind === "host") return ""; if (peer.kind === "member" && peer.name) return peer.name; return null; } /** Record a hook-created session's account — first writer wins, a pushed hook never writes. */ function stampFromPeer(sessionId, peer, pushSource = null) { if (pushSource || !sessionId) return; const account = accountOfPeer(peer); if (account == null) return; sql("UPDATE sessions SET account = ? WHERE id = ? AND account IS NULL").run(account, sessionId); } /** The account a folder runs as: "" outside the users root, a member's name inside it. */ function accountOfPath(cwd) { const linux = require("./linux-accounts"); const conf = linux.s3Enabled() ? linux.serverConf() : null; const root = conf?.usersRoot; const path = require("path"); if (!root || typeof cwd !== "string" || !path.isAbsolute(cwd)) return ""; const rel = path.relative(root, path.resolve(cwd)); if (rel.startsWith("..") || path.isAbsolute(rel)) return ""; // The users root itself is nobody's. return rel.split(path.sep)[0] || null; } /** * Give every local session still without an account the one its folder runs * as (sessions the pollers made, and history from before the column). Cheap * once caught up: only `account IS NULL` rows are read. */ function stampFromFolders() { const rows = sql( "SELECT id, cwd FROM sessions WHERE account IS NULL AND source NOT LIKE 'push:%' LIMIT 5000" ).all(); if (!rows.length) return 0; const set = sql("UPDATE sessions SET account = ? WHERE id = ? AND account IS NULL"); let n = 0; require("../db").db.transaction(() => { for (const r of rows) { const account = accountOfPath(r.cwd); if (account != null) n += set.run(account, r.id).changes; } })(); return n; } /** Mark `userId` (an admin) as the host account's owner, or clear the mark with null. */ function setHostOwner(userId) { require("../db").db.transaction(() => { sql("UPDATE users SET host_account = 0 WHERE host_account = 1").run(); if (userId != null) sql("UPDATE users SET host_account = 1 WHERE id = ?").run(userId); })(); } /** The user id the host account bills to right now, or null. */ function hostOwner() { return sql(`SELECT ${HOST_OWNER} AS id`).get()?.id ?? null; } module.exports = { ACCOUNT_OWNED, accountOfPeer, stampFromPeer, accountOfPath, stampFromFolders, setHostOwner, hostOwner, };