feat: account pool per tool with groups and a Cursor pool; update runs in the background
- The update dialog can be closed while an update applies: it keeps running, the page still reloads itself when the new server is up, and a failure re-opens the dialog with its error. - Account pool card: one tab per AI tool (Claude Code, Codex, Cursor CLI, Oh My Pi, and every other installed tool, which says it has no pool), accounts as cards with 5-hour and weekly quota shown as what is LEFT plus their reset times, and the saved profiles of Settings → AI tools → Profiles as cards with "Share to pool". - Cursor account pool (server/lib/cursor-pool.js): Cursor API keys handed to a cursor-agent pane only through tmux -e CURSOR_API_KEY; `agenthub accounts add <name> --tool cursor`. - Pool groups (server/lib/pool-groups.js, /api/accounts/groups): a group's members run only on its accounts of each tool, a user in no group on the accounts no group lists, a pin still wins. account_pins loses its tool CHECK (rebuilt, pins kept); three new tables. - Oh My Pi login sharing can be switched from its pool tab too.
This commit is contained in:
@@ -3,10 +3,13 @@
|
||||
* - GET /api/auth/me (Auth)
|
||||
* - POST /api/auth/login (Auth)
|
||||
* - POST /api/auth/logout (Auth)
|
||||
* - GET /api/accounts (Auth) — the Claude and Codex account pools, no keys or tokens
|
||||
* - GET /api/accounts (Auth) — the Claude, Codex and Cursor account pools and
|
||||
* the pool groups, 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
|
||||
* - PUT /api/accounts/{name}/users (Auth) — an account's Edit: exactly who runs on it
|
||||
* - POST /api/accounts/groups, PATCH/DELETE /api/accounts/groups/{id} (Auth) —
|
||||
* pool groups: a team's members run only on the group's accounts
|
||||
* - 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
|
||||
@@ -43,6 +46,27 @@ const schemas = {
|
||||
pinnedAt: { type: "string" },
|
||||
},
|
||||
},
|
||||
PoolGroup: {
|
||||
type: "object",
|
||||
required: ["id", "name", "users", "accounts"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
name: { type: "string" },
|
||||
createdAt: { type: "string" },
|
||||
users: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: { userId: { type: "integer" }, username: { type: "string" } },
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
type: "object",
|
||||
description: "Per tool (claude, codex, cursor), the pool account names it may use",
|
||||
additionalProperties: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
UserPrefs: {
|
||||
type: "object",
|
||||
required: ["prefs", "onboardedAt"],
|
||||
@@ -636,7 +660,7 @@ const paths = {
|
||||
tags: ["Auth"],
|
||||
operationId: "listPoolAccounts",
|
||||
summary:
|
||||
"The Claude and Codex account pools as the balancer sees them (never a key or token)",
|
||||
"The Claude, Codex and Cursor account pools as the balancer sees them, and the pool groups (never a key or token)",
|
||||
parameters: [
|
||||
{
|
||||
name: "force",
|
||||
@@ -681,6 +705,27 @@ const paths = {
|
||||
properties: {
|
||||
claude: { type: "array", items: { $ref: "#/components/schemas/AccountPin" } },
|
||||
codex: { type: "array", items: { $ref: "#/components/schemas/AccountPin" } },
|
||||
cursor: { type: "array", items: { $ref: "#/components/schemas/AccountPin" } },
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
type: "array",
|
||||
description:
|
||||
"Pool groups (server/lib/pool-groups.js; empty in local mode): a member runs only on its group's accounts, a user in no group on the accounts no group lists",
|
||||
items: { $ref: "#/components/schemas/PoolGroup" },
|
||||
},
|
||||
cursor: {
|
||||
type: "array",
|
||||
description:
|
||||
"The Cursor account pool (server/lib/cursor-pool.js): API keys only, no quota or spend",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
kind: { type: "string", enum: ["apikey"] },
|
||||
addedAt: { type: "string" },
|
||||
livePanes: { type: "integer" },
|
||||
},
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
@@ -715,7 +760,7 @@ const paths = {
|
||||
tags: ["Auth"],
|
||||
operationId: "addPoolAccount",
|
||||
summary:
|
||||
"Admin: add a Claude (saved login, pasted credentials or setup-token) or Codex (API key / auth.json text) account to the pool, with their own password again. The secret is never answered back",
|
||||
"Admin: add a Claude (saved login, pasted credentials or setup-token), Codex (saved profile, API key / auth.json text) or Cursor (API key) account to the pool, with their own password again. The secret is never answered back",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
@@ -724,7 +769,7 @@ const paths = {
|
||||
type: "object",
|
||||
required: ["tool", "currentPassword"],
|
||||
properties: {
|
||||
tool: { type: "string", enum: ["claude", "codex"] },
|
||||
tool: { type: "string", enum: ["claude", "codex", "cursor"] },
|
||||
name: { type: "string", description: "Defaults to the profile's name" },
|
||||
profile: {
|
||||
type: "string",
|
||||
@@ -736,7 +781,8 @@ const paths = {
|
||||
},
|
||||
token: {
|
||||
type: "string",
|
||||
description: "Claude: a setup-token; Codex: an API key or auth.json text",
|
||||
description:
|
||||
"Claude: a setup-token; Codex: an API key or auth.json text; Cursor: a Cursor API key",
|
||||
},
|
||||
currentPassword: { type: "string" },
|
||||
},
|
||||
@@ -770,7 +816,7 @@ const paths = {
|
||||
type: "object",
|
||||
required: ["tool", "userId", "currentPassword"],
|
||||
properties: {
|
||||
tool: { type: "string", enum: ["claude", "codex"] },
|
||||
tool: { type: "string", enum: ["claude", "codex", "cursor"] },
|
||||
userId: { type: "integer" },
|
||||
account: { type: "string", nullable: true },
|
||||
currentPassword: { type: "string" },
|
||||
@@ -816,7 +862,7 @@ const paths = {
|
||||
type: "object",
|
||||
required: ["tool", "userIds", "currentPassword"],
|
||||
properties: {
|
||||
tool: { type: "string", enum: ["claude", "codex"] },
|
||||
tool: { type: "string", enum: ["claude", "codex", "cursor"] },
|
||||
userIds: { type: "array", items: { type: "integer" } },
|
||||
currentPassword: { type: "string" },
|
||||
},
|
||||
@@ -836,6 +882,105 @@ const paths = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/accounts/groups": {
|
||||
post: {
|
||||
tags: ["Auth"],
|
||||
operationId: "createPoolGroup",
|
||||
summary: "Admin: a new, empty pool group, with their own password again",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["name", "currentPassword"],
|
||||
properties: { name: { type: "string" }, currentPassword: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "`{group: {id, name}}`" },
|
||||
400: err("EBADNAME"),
|
||||
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
||||
409: err("EEXISTS — a group has that name"),
|
||||
429: err("ETOOMANY — too many wrong passwords"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/accounts/groups/{id}": {
|
||||
patch: {
|
||||
tags: ["Auth"],
|
||||
operationId: "updatePoolGroup",
|
||||
summary:
|
||||
"Admin: rename a pool group, set exactly its members (one group per user — a member of another group moves here) and/or exactly its accounts of each tool named, with their own password again",
|
||||
parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["currentPassword"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
userIds: { type: "array", items: { type: "integer" } },
|
||||
accounts: {
|
||||
type: "object",
|
||||
description: "`{tool: [account names]}` for claude, codex, cursor",
|
||||
additionalProperties: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
currentPassword: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "`{group}`",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: { group: { $ref: "#/components/schemas/PoolGroup" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
400: err("EBADNAME, EBADUSER, EBADTOOL"),
|
||||
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
||||
404: err("ENOGROUP, ENOUSER, ENOACCOUNT"),
|
||||
409: err("EEXISTS — a group has that name"),
|
||||
429: err("ETOOMANY — too many wrong passwords"),
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
tags: ["Auth"],
|
||||
operationId: "deletePoolGroup",
|
||||
summary:
|
||||
"Admin: delete a pool group — its members go back to the shared pool, no account is removed — with their own password again",
|
||||
parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["currentPassword"],
|
||||
properties: { currentPassword: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "`{ok: true}`" },
|
||||
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
||||
404: err("ENOGROUP"),
|
||||
429: err("ETOOMANY — too many wrong passwords"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/accounts/{name}": {
|
||||
delete: {
|
||||
tags: ["Auth"],
|
||||
@@ -850,7 +995,7 @@ const paths = {
|
||||
type: "object",
|
||||
required: ["tool", "currentPassword"],
|
||||
properties: {
|
||||
tool: { type: "string", enum: ["claude", "codex"] },
|
||||
tool: { type: "string", enum: ["claude", "codex", "cursor"] },
|
||||
currentPassword: { type: "string" },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user