feat(server-mode): spend quota per user — no new session, prompts refused once used up
An admin sets a default spend quota on the Users page and overrides it per
user (Edit user → Spend quota: default / no limit / their own), in dollars
per local day or per sliding 5 hours. Spend is the users page's own
hourlyTokenRows + calculateCost over the sessions a user owns.
Over the quota, until it opens again:
- spawnRun refuses every start/resume/home prompt with EQUOTA (429);
- a minute sweep writes each pane's AGENTHUB_QUOTA_FILE (host: data dir,
member: handoff dir via write-as) and hook-handler.js exits 2 on
UserPromptSubmit from that file alone (Claude Code, Codex with trusted
hooks) — fail-open, never asks the dashboard, never for Antigravity;
- a member's omp on the shared login loses its gateway key (clientsFor);
- panes no hook can stop (Antigravity, Cursor, Codex without trusted
hooks, omp on its own login) are ended.
GET /api/me/quota feeds a "Quota N%" chip in the status bar.
New: server/lib/user-quota.js, user_quotas table, PUT /api/users/quota-default,
PATCH /api/users/:id {quota}, docs in SERVER-MODE.md § Spend quotas.
This commit is contained in:
@@ -710,6 +710,55 @@ omp login # e.g. anthropic
|
||||
> runs; it cannot carry a login away. A member who edits the `agenthub`
|
||||
> provider out of their `models.yml` gets it back at their next omp pane.
|
||||
|
||||
## Spend quotas
|
||||
|
||||
An admin limits what each user may spend, in dollars, on the Users page
|
||||
(`server/lib/user-quota.js`):
|
||||
|
||||
- **Default spend quota** card: one limit every user without their own gets
|
||||
(`PUT /api/users/quota-default` `{quota: {limit_usd, period, tz_offset} | null}`).
|
||||
- **Edit user → Spend quota**: *Default*, *No limit*, or *Their own limit*
|
||||
(`PATCH /api/users/:id` `{quota: {mode: "default" | "unlimited" | "custom", …}}`).
|
||||
- A period is `day` (from local midnight of the zone the admin's browser was
|
||||
in when they saved it) or `5h` (sliding: the current UTC hour and the four
|
||||
before it — spend booked in hour H counts until H + 5 h).
|
||||
- Both writes need the admin's own password again and are audited, like
|
||||
every account change; each one re-applies at once (`sweep`).
|
||||
|
||||
Spend is the users page's own figure: the tokens of the sessions the user
|
||||
OWNS through a workspace, priced by `calculateCost`. The Users table shows
|
||||
each user's spend against their quota; the user sees their own as **Quota N%**
|
||||
in the status bar (`GET /api/me/quota`).
|
||||
|
||||
Once a user's spend reaches the limit, until the window opens again:
|
||||
|
||||
- **No new session.** Every start, resume or prompt from the workspace home
|
||||
goes through `spawnRun`, which answers `429 EQUOTA` with the reason.
|
||||
- **Claude Code and Codex prompts are refused** by the hooks. Every pane
|
||||
carries `AGENTHUB_QUOTA_FILE`. The dashboard writes that file every minute:
|
||||
the host's under `<data dir>/user-quota/u<id>.json`, a member's (S3) in
|
||||
their handoff dir through `write-as`. While it says `blocked`,
|
||||
`scripts/hook-handler.js` exits 2 on `UserPromptSubmit` with the reason on
|
||||
stderr, and the CLI drops the prompt and shows why. The hook reads only that
|
||||
local file and never asks the dashboard. It fails open: a missing or
|
||||
unreadable file, or a passed `until`, lets the prompt through. Codex takes
|
||||
this only with AgentHub's Codex hooks installed and trusted.
|
||||
- **A member's omp on the shared login** (above) loses its gateway key: the
|
||||
run's key is left out of `clients.json` until the quota opens. Its pane stays.
|
||||
- **Every other pane of theirs is ended.** This covers Antigravity, Cursor, a
|
||||
Codex without trusted hooks (a member's Codex always), an omp on its own
|
||||
login, and any other catalog tool. Plain shells are left alone.
|
||||
|
||||
Limits of the check:
|
||||
|
||||
- Tokens land after a turn, so a user can finish one turn past the limit
|
||||
(omp/Codex journals are polled, so a little more).
|
||||
- A user's own terminal outside AgentHub carries no flag file.
|
||||
- Without S3 every pane runs as the host account, which can edit the flag.
|
||||
|
||||
Turn the minute sweep off with `DASHBOARD_QUOTA_SWEEP_MS=0`. The `EQUOTA`
|
||||
refusal at start does not depend on it.
|
||||
|
||||
## One Linux user per account
|
||||
|
||||
Every pane of a user, an agent's shell included, runs as that user's own Linux
|
||||
|
||||
Reference in New Issue
Block a user