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
+15 -1
View File
@@ -2,7 +2,8 @@
* @file UserEditDialog.tsx
* @description The Users page's ONE account editor (admin only): profile
* (display name, email), access (role, active/disabled — neither for your own
* account), a new password (typed or generated, copyable — someone else's
* account — and, for an admin, whether they own the host's Linux account, so
* its sessions outside every workspace are billed to them), a new password (typed or generated, copyable — someone else's
* may be anything and makes them choose their own at next sign-in; your own
* must pass every rule of `lib/passwordRules.ts`), the workspaces the
* account owns (remove, add from every workspace), its spend quota (the
@@ -87,6 +88,7 @@ export function UserEditDialog({
const [email, setEmail] = useState(user.email ?? "");
const [role, setRole] = useState<Role>(user.role);
const [active, setActive] = useState(!user.disabled);
const [hostAccount, setHostAccount] = useState(Boolean(user.hostAccount));
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [owned, setOwned] = useState<OwnedWorkspace[] | null>(null);
@@ -139,6 +141,7 @@ export function UserEditDialog({
if (norm(email) !== (user.email ?? null)) patch.email = norm(email);
if (!self && role !== user.role) patch.role = role;
if (!self && active === user.disabled) patch.disabled = !active;
if (role === "admin" && hostAccount !== Boolean(user.hostAccount)) patch.hostAccount = hostAccount;
if (password) patch.password = password;
if (added.length) patch.addWorkspaces = added;
if (removed.size) patch.removeWorkspaces = [...removed];
@@ -364,6 +367,17 @@ export function UserEditDialog({
)}
</div>
</div>
{role === "admin" && (
<div className="mt-3">
<Checkbox
checked={hostAccount}
onChange={setHostAccount}
label={t("edit.hostAccount")}
labelClassName="text-sm text-fg-secondary"
/>
<p className="ml-6 mt-0.5 text-[11px] text-fg-muted">{t("edit.hostAccountHint")}</p>
</div>
)}
</Section>
<Section title={t("edit.password")}>
+2
View File
@@ -112,6 +112,8 @@
"status": "Status",
"active": "Active",
"statusHint": "A disabled user cannot sign in; their workspaces and history stay.",
"hostAccount": "Owns this server's host account",
"hostAccountHint": "Sessions run as the server's own Linux account outside every workspace (a script's claude -p, a CLI in a plain terminal) count toward this admin's spend. At most one admin; with none marked, the only admin.",
"selfRole": "You cannot change your own role.",
"selfDisable": "You cannot disable yourself.",
"password": "Password",
+2
View File
@@ -112,6 +112,8 @@
"status": "Trạng thái",
"active": "Đang hoạt động",
"statusHint": "Người dùng bị vô hiệu hoá không thể đăng nhập; workspace và lịch sử vẫn được giữ.",
"hostAccount": "Sở hữu tài khoản host của máy chủ",
"hostAccountHint": "Session chạy bằng tài khoản Linux của máy chủ nhưng ngoài mọi workspace (claude -p của script, CLI trong terminal thường) được tính vào chi phí của admin này. Tối đa một admin; nếu chưa ai được chọn thì là admin duy nhất.",
"selfRole": "Bạn không thể đổi vai trò của chính mình.",
"selfDisable": "Bạn không thể tự vô hiệu hoá mình.",
"password": "Mật khẩu",
+4
View File
@@ -800,6 +800,8 @@ export type UserPatch = {
addWorkspaces?: number[];
removeWorkspaces?: number[];
signOut?: true;
/** Owns the host's Linux account: its sessions outside every workspace bill to them (admins only). */
hostAccount?: boolean;
/** Their own spend quota (`default` = the default applies). */
quota?: UserQuotaSetting;
};
@@ -983,6 +985,8 @@ export type AuthUser = {
createdAt?: string;
/** An admin gave this password: the user must choose their own before anything else. */
mustChangePassword?: boolean;
/** Owns the host's Linux account (server/lib/session-account.js) — absent from an older server. */
hostAccount?: boolean;
};
export type AuthState = { mode: "local" } | { mode: "server"; user: AuthUser | null };
/** GET /api/me/prefs — the signed-in account's own UI preferences (localStorage keys). */