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:
+177
-2
@@ -3,7 +3,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Agent Dashboard for Claude Code API
|
||||
version: 2.8.29
|
||||
version: 2.8.30
|
||||
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
||||
contact:
|
||||
name: Nguyễn Ngọc Trí Vĩ
|
||||
@@ -4657,6 +4657,73 @@ components:
|
||||
description: Events attributed to this run's inner agents, ordered by created_at then id. Capped at 5000 rows.
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardEvent'
|
||||
UserQuota:
|
||||
type: object
|
||||
description: 'A spend quota: `limit_usd` per `period` — `day` from the local midnight of `tz_offset` (getTimezoneOffset() minutes), or `5h`, the current UTC hour and the four before it.'
|
||||
required:
|
||||
- limit_usd
|
||||
- period
|
||||
properties:
|
||||
limit_usd:
|
||||
type: number
|
||||
minimum: 0
|
||||
exclusiveMinimum: true
|
||||
period:
|
||||
type: string
|
||||
enum:
|
||||
- day
|
||||
- 5h
|
||||
tz_offset:
|
||||
type: integer
|
||||
minimum: -840
|
||||
maximum: 840
|
||||
source:
|
||||
type: string
|
||||
enum:
|
||||
- default
|
||||
- user
|
||||
readOnly: true
|
||||
UserQuotaSetting:
|
||||
type: object
|
||||
description: 'A user''s own quota setting: `default` (the default applies), `unlimited`, or `custom` with its own limit.'
|
||||
required:
|
||||
- mode
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
enum:
|
||||
- default
|
||||
- unlimited
|
||||
- custom
|
||||
limit_usd:
|
||||
type: number
|
||||
period:
|
||||
type: string
|
||||
enum:
|
||||
- day
|
||||
- 5h
|
||||
tz_offset:
|
||||
type: integer
|
||||
UserQuotaStatus:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserQuota'
|
||||
- type: object
|
||||
properties:
|
||||
spend:
|
||||
type: number
|
||||
description: USD spent in the current window
|
||||
pct:
|
||||
type: number
|
||||
exceeded:
|
||||
type: boolean
|
||||
description: 'Used up: new panes are refused (EQUOTA) and prompts refused by the hooks'
|
||||
resetsAt:
|
||||
type: string
|
||||
nullable: true
|
||||
description: When it opens again
|
||||
since:
|
||||
type: string
|
||||
description: Start of the current window
|
||||
AccountPin:
|
||||
type: object
|
||||
required:
|
||||
@@ -10975,6 +11042,12 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: 'EQUOTA (server mode): the workspace owner''s spend quota is used up until it resets.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: Unexpected failure (EINTERNAL).
|
||||
content:
|
||||
@@ -14453,6 +14526,30 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/me/quota:
|
||||
get:
|
||||
tags:
|
||||
- Auth
|
||||
operationId: getMyQuota
|
||||
summary: The account's own spend quota now (null without one, and in local mode)
|
||||
responses:
|
||||
'200':
|
||||
description: Quota
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
quota:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserQuotaStatus'
|
||||
nullable: true
|
||||
'401':
|
||||
description: EAUTH
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/me/instructions:
|
||||
get:
|
||||
tags:
|
||||
@@ -14966,6 +15063,16 @@ paths:
|
||||
type: number
|
||||
tokens:
|
||||
type: integer
|
||||
quotaSetting:
|
||||
$ref: '#/components/schemas/UserQuotaSetting'
|
||||
quota:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserQuotaStatus'
|
||||
nullable: true
|
||||
quotaDefault:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserQuota'
|
||||
nullable: true
|
||||
'403':
|
||||
description: EADMIN — a developer
|
||||
content:
|
||||
@@ -15303,6 +15410,8 @@ paths:
|
||||
type: boolean
|
||||
enum:
|
||||
- true
|
||||
quota:
|
||||
$ref: '#/components/schemas/UserQuotaSetting'
|
||||
currentPassword:
|
||||
type: string
|
||||
responses:
|
||||
@@ -15316,7 +15425,7 @@ paths:
|
||||
user:
|
||||
$ref: '#/components/schemas/AuthUser'
|
||||
'400':
|
||||
description: EBADPASSWORD / EBADROLE / EBADFIELD / EBADEMAIL
|
||||
description: EBADPASSWORD / EBADROLE / EBADFIELD / EBADEMAIL / EBADQUOTA
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -15413,6 +15522,72 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/users/quota-default:
|
||||
put:
|
||||
tags:
|
||||
- Auth
|
||||
operationId: setDefaultUserQuota
|
||||
summary: 'Admin: the default spend quota for every user without one of their own (`quota: null` removes it). A user over their quota can start no pane (429 EQUOTA) and their Claude Code/Codex prompts are refused by the hooks'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- quota
|
||||
- currentPassword
|
||||
properties:
|
||||
quota:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserQuota'
|
||||
nullable: true
|
||||
currentPassword:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Saved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
user:
|
||||
$ref: '#/components/schemas/AuthUser'
|
||||
quotaDefault:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserQuota'
|
||||
nullable: true
|
||||
'400':
|
||||
description: EBADQUOTA
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'403':
|
||||
description: EREAUTH (own password wrong) / EADMIN / ELOCALMODE
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: ENOUSER / ENOLANE
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'409':
|
||||
description: ESELF — your own role or disable
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: ETOOMANY — 10 wrong passwords in 15 minutes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/users/{id}/password:
|
||||
post:
|
||||
tags:
|
||||
|
||||
Reference in New Issue
Block a user