fix(cost): bill a session outside every workspace to its Linux account's user

A script's claude -p in a temp folder (the SG Flow eval runner strips the
pane's env) had no workspace, so its spend reached nobody's Today, the Users
page or a spend quota — ~$57 of a $74 day was missing from the status bar.

sessions.account records the Linux account a session ran as (the hook's
peer uid, else its folder for poller-made and old rows); a session no
workspace owns is owned by that account's user — a member's to them, the
host's to the admin ticked 'Owns this server's host account' (or the only
admin). Workspace ownership still wins.
This commit is contained in:
2026-10-30 08:50:34 +07:00
parent c28395402b
commit 783b7f4101
16 changed files with 331 additions and 16 deletions
+7 -1
View File
@@ -213,6 +213,9 @@ const publicUser = (u) =>
createdAt: u.created_at ?? null,
// An admin gave this password: the account must pick its own first.
mustChangePassword: Boolean(u.must_change_password),
// Owns the host's Linux account: its sessions outside every workspace
// are billed to this admin (server/lib/session-account.js).
hostAccount: Boolean(u.host_account),
};
const MAX_DISPLAY_NAME = 100;
@@ -415,7 +418,10 @@ function setRole(username, role) {
if (!ROLES.includes(role)) throw err("EBADROLE", `role: one of ${ROLES.join(", ")}`);
const u = requireUser(username);
if (role !== "admin") assertKeepsAdmin(u, "demoted");
sql("UPDATE users SET role = ? WHERE id = ?").run(role, u.id);
// Only an admin owns the host account (session-account.js): a demotion drops the mark.
sql(
"UPDATE users SET role = ?, host_account = CASE WHEN ? = 'admin' THEN host_account ELSE 0 END WHERE id = ?"
).run(role, role, u.id);
if (role !== "admin") stopHostPanes(u.id);
}