feat(pool): pin a user to one Claude/Codex pool account

An admin pins a server-mode user to one pool account per tool
(account_pins; PUT /api/accounts/pins with the password step-up and audit,
the Users page's Pin a user, agenthub accounts pin|unpin). The workspace
owner's pin wins over the balancer whenever the pane may use the account;
a pinned login still waits while another pool login holds the home.
This commit is contained in:
2026-10-19 10:05:19 +07:00
parent aa8ac6cad9
commit 9c98638ec0
20 changed files with 776 additions and 39 deletions
+52
View File
@@ -5,6 +5,7 @@
* - POST /api/auth/logout (Auth)
* - GET /api/accounts (Auth) — the Claude and Codex account pools, no keys or tokens
* - POST /api/accounts, DELETE /api/accounts/{name} (Auth) — an admin adds/removes one
* - PUT /api/accounts/pins (Auth) — an admin pins a user to one pool account
* - GET /api/users (Auth) — every account's usage (admin/analyst)
* - GET /api/users/audit (Auth) — staff reads of developers' sessions
* - GET/PUT /api/me/prefs, POST /api/me/onboarded (Auth) — the account's own UI preferences
@@ -30,6 +31,16 @@ const tags = [
];
const schemas = {
AccountPin: {
type: "object",
required: ["userId", "username", "account"],
properties: {
userId: { type: "integer" },
username: { type: "string" },
account: { type: "string", description: "The pool account's name" },
pinnedAt: { type: "string" },
},
},
UserPrefs: {
type: "object",
required: ["prefs", "onboardedAt"],
@@ -633,6 +644,15 @@ const paths = {
},
},
},
pins: {
type: "object",
description:
"Per tool, the users an admin pinned to one account (empty in local mode)",
properties: {
claude: { type: "array", items: { $ref: "#/components/schemas/AccountPin" } },
codex: { type: "array", items: { $ref: "#/components/schemas/AccountPin" } },
},
},
codex: {
type: "array",
description:
@@ -706,6 +726,38 @@ const paths = {
},
},
},
"/api/accounts/pins": {
put: {
tags: ["Auth"],
operationId: "pinPoolAccount",
summary:
"Admin: pin one user's new sessions of a tool to one pool account (`account: null` unpins), with their own password again",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["tool", "userId", "currentPassword"],
properties: {
tool: { type: "string", enum: ["claude", "codex"] },
userId: { type: "integer" },
account: { type: "string", nullable: true },
currentPassword: { type: "string" },
},
},
},
},
},
responses: {
200: { description: "`{pin: {userId, username, account}}` — `account` null when unpinned" },
400: err("EBADTOOL"),
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
404: err("ENOUSER, ENOACCOUNT"),
429: err("ETOOMANY — too many wrong passwords"),
},
},
},
"/api/accounts/profiles": {
get: {
tags: ["Auth"],