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
+22 -3
View File
@@ -54,8 +54,9 @@ const { listAudit, recordAccountChange } = require("../lib/audit");
const router = Router();
// Which account each session belongs to — through its workspace, or a
// dashboard run on one (server/lib/scope.js's sessionWhere, for every user at once).
// Which account each session belongs to — through its workspace, a dashboard
// run on one, or (no workspace owns it) the Linux account it ran as
// (server/lib/scope.js's sessionWhere, for every user at once).
const OWNED_SESSIONS = `
SELECT s.id AS session_id, l.owner_id AS uid
FROM sessions s JOIN lanes l ON l.id = s.lane_id
@@ -63,7 +64,9 @@ const OWNED_SESSIONS = `
UNION
SELECT s.id, l.owner_id
FROM dashboard_runs r JOIN lanes l ON l.id = r.lane_id JOIN sessions s ON s.id = r.session_id
WHERE l.owner_id IS NOT NULL`;
WHERE l.owner_id IS NOT NULL
UNION
${require("../lib/session-account").ACCOUNT_OWNED}`;
/**
* Map(user id → {cost, tokens}) per window, each window a UTC hour
@@ -580,6 +583,18 @@ router.patch(
if (self && body.disabled) throw ESELF("you cannot disable yourself");
}
const disabledChange = body.disabled !== undefined && body.disabled !== Boolean(u.disabled);
if (body.hostAccount !== undefined) {
if (typeof body.hostAccount !== "boolean") {
throw Object.assign(new Error("hostAccount: true or false"), { code: "EBADFIELD" });
}
if (body.hostAccount && (body.role ?? u.role) !== "admin") {
throw Object.assign(new Error("only an admin can own the host account"), {
code: "EBADROLE",
});
}
}
const hostChange =
body.hostAccount !== undefined && body.hostAccount !== Boolean(u.host_account);
if (body.password !== undefined) {
// Your own password: every rule. Someone else's: a starting one they replace.
if (self) {
@@ -619,6 +634,10 @@ router.patch(
auth.setRole(u.username, body.role);
done.push(`role ${body.role}`);
}
if (hostChange) {
require("../lib/session-account").setHostOwner(body.hostAccount ? u.id : null);
done.push(body.hostAccount ? "owns the host account" : "no longer owns the host account");
}
for (const id of add) auth.assignWorkspace(id, u.username);
for (const id of remove) auth.unassignWorkspace(id, u.username);
if (add.length) done.push(`workspace ${add.join(", ")}`);