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:
2026-10-28 14:31:11 +07:00
parent 9e3d6f4ca1
commit fa27b299e7
29 changed files with 2044 additions and 23 deletions
+7
View File
@@ -78,6 +78,13 @@
"quotaWindow": {
"wk": "Week",
"mo": "Month"
},
"userQuota": "Quota {{pct}}%",
"userQuotaTitle": "Your spend quota: {{spend}} of {{limit}} {{period}}",
"userQuotaBlocked": "Your spend quota is used up ({{spend}} of {{limit}} {{period}}): no new session and no prompt until {{at}}",
"userQuotaPeriod": {
"day": "today",
"5h": "in the last 5 hours"
}
},
"version": "Version",
+35 -2
View File
@@ -87,7 +87,8 @@
"ELOCALMODE": "Accounts are managed in server mode only.",
"ELASTADMIN": "The last admin cannot be deleted, demoted or disabled.",
"EWEAKPASSWORD": "The new password does not meet every rule (or is the current one).",
"EPWCHANGE": "Choose your own password first."
"EPWCHANGE": "Choose your own password first.",
"EBADQUOTA": "Enter a positive dollar limit and a period of a day or 5 hours."
},
"readOnly": "Analysts can view accounts but not change them.",
"auditTitle": "Audit log",
@@ -412,5 +413,37 @@
"spendSub": "7 d {{week}} · 30 d {{month}}",
"sessionsN": "{{n}} sessions",
"workspacesN": "{{n}} workspaces",
"tmuxCol": "tmux"
"tmuxCol": "tmux",
"quota": {
"title": "Spend quota",
"limit": "Limit in dollars",
"period": "Period",
"periods": {
"day": "per day (from local midnight)",
"5h": "per 5 hours (sliding)"
},
"per": {
"day": "{{limit}} / day",
"5h": "{{limit}} / 5 h"
},
"used": "{{spend}} of {{limit}}",
"defaultTag": "(default)",
"none": "No quota",
"blockedUntil": "Used up — no new session and no prompt until {{at}}",
"cardTitle": "Default spend quota",
"cardDescription": "Every user without a quota of their own gets this one. Once it is used up they can start no session and their prompts are refused until it resets; a pane no hook can stop (Antigravity, Cursor, Codex without trusted hooks, omp on its own login) is ended.",
"noDefault": "No default — no limit",
"editDefault": "Edit",
"defaultDialog": "Default spend quota",
"defaultOn": "Limit what each user may spend",
"defaultSaved": "Default spend quota saved",
"enforceHint": "Over the quota: no new session, Claude Code and Codex prompts refused, other AI tools' panes ended — until it resets.",
"modes": {
"defaultOf": "Default ({{quota}})",
"defaultNone": "Default (no limit)",
"unlimited": "No limit",
"custom": "Their own limit"
},
"now": "Now: {{spend}} of {{limit}}"
}
}
+7
View File
@@ -78,6 +78,13 @@
"quotaWindow": {
"wk": "Tuần",
"mo": "Tháng"
},
"userQuota": "Quota {{pct}}%",
"userQuotaTitle": "Quota chi phí của bạn: {{spend}} trên {{limit}} {{period}}",
"userQuotaBlocked": "Bạn đã dùng hết quota chi phí ({{spend}} trên {{limit}} {{period}}): không mở phiên mới, không gửi prompt đến {{at}}",
"userQuotaPeriod": {
"day": "hôm nay",
"5h": "trong 5 giờ qua"
}
},
"version": "Phiên bản",
+35 -2
View File
@@ -87,7 +87,8 @@
"ELOCALMODE": "Chỉ quản lý tài khoản được ở chế độ server.",
"ELASTADMIN": "Không thể xoá, hạ quyền hay khoá admin cuối cùng.",
"EWEAKPASSWORD": "Mật khẩu mới chưa đạt đủ các yêu cầu (hoặc trùng mật khẩu hiện tại).",
"EPWCHANGE": "Hãy đặt mật khẩu riêng của bạn trước."
"EPWCHANGE": "Hãy đặt mật khẩu riêng của bạn trước.",
"EBADQUOTA": "Nhập hạn mức USD lớn hơn 0 và chu kỳ ngày hoặc 5 giờ."
},
"readOnly": "Analyst chỉ xem được tài khoản, không thay đổi được.",
"auditTitle": "Nhật ký audit",
@@ -412,5 +413,37 @@
"spendSub": "7 ngày {{week}} · 30 ngày {{month}}",
"sessionsN": "{{n}} session",
"workspacesN": "{{n}} workspace",
"tmuxCol": "tmux"
"tmuxCol": "tmux",
"quota": {
"title": "Quota chi phí",
"limit": "Hạn mức (USD)",
"period": "Chu kỳ",
"periods": {
"day": "mỗi ngày (từ nửa đêm giờ địa phương)",
"5h": "mỗi 5 giờ (trượt)"
},
"per": {
"day": "{{limit}} / ngày",
"5h": "{{limit}} / 5 giờ"
},
"used": "{{spend}} trên {{limit}}",
"defaultTag": "(mặc định)",
"none": "Không giới hạn",
"blockedUntil": "Đã hết — không mở phiên mới, không gửi prompt đến {{at}}",
"cardTitle": "Quota chi phí mặc định",
"cardDescription": "Áp cho mọi user không có quota riêng. Khi dùng hết, user không mở được phiên mới và prompt bị từ chối đến khi quota mở lại; pane không hook nào chặn được (Antigravity, Cursor, Codex chưa tin hook, omp dùng login riêng) sẽ bị dừng.",
"noDefault": "Chưa có mặc định — không giới hạn",
"editDefault": "Sửa",
"defaultDialog": "Quota chi phí mặc định",
"defaultOn": "Giới hạn chi phí của mỗi user",
"defaultSaved": "Đã lưu quota chi phí mặc định",
"enforceHint": "Vượt quota: không mở phiên mới, prompt Claude Code và Codex bị từ chối, pane của các AI tool khác bị dừng — đến khi quota mở lại.",
"modes": {
"defaultOf": "Mặc định ({{quota}})",
"defaultNone": "Mặc định (không giới hạn)",
"unlimited": "Không giới hạn",
"custom": "Hạn mức riêng"
},
"now": "Hiện tại: {{spend}} trên {{limit}}"
}
}