feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP

- Users page: one Edit user form (display name, email, role, active, a new or
  generated password, workspaces given or taken back, sign out everywhere),
  validated as a whole before anything is written (PATCH /api/users/:id,
  GET /api/users/:id); users.display_name/email added migration-safe.
- Settings → Account for every signed-in user: profile, own password (the
  current one required, this login kept), sign out other devices
  (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie
  only, never a pane token).
- Display names wherever a person is named (sidebar, workspace owner groups,
  Users page and audit log); owner_display_name / reader_display additive.
- Users page: admins add and remove Claude/Codex pool accounts.
- Folder browser: an admin with their own Linux account opens in their home;
  the "Server folder" tick opens the host's (never remembered).
- AI Proxy: "Config 9router" beside Add profile when 9router is installed,
  opened on the dashboard's own host (GET /api/ai-proxies/9router).
- Terminal: a tmux selection reaches the clipboard over plain HTTP too
  (writeClipboard's textarea fallback, focus handed back).
- Chat: a file the session touched outside the workspace previews
  (GET /api/sessions/:id/file).
- install-system: `!requiretty` only when visudo accepts it (sudo-rs on
  Ubuntu 25.10+ rejects the setting).
- openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 17:10:29 +07:00
parent 9c573f1e3c
commit 0c2f49876d
74 changed files with 5347 additions and 392 deletions
+31 -3
View File
@@ -85,12 +85,14 @@ A dashboard shared by several people runs in **server mode** ([SERVER-MODE.md](S
| `POST /api/auth/login` `{username, password}` | `{user}` + the cookie; `401 EBADLOGIN`, `429 ETOOMANY`, `404 ENOTSERVER` in local mode |
| `POST /api/auth/logout` | `{ok: true}`, cookie cleared |
In server mode a **developer** sees only rows of the workspaces they own (`lanes.owner_id`): every list, count, chart and cost answer is limited to them, and a request naming another user's session, agent, workspace or run (in the path, query or body) answers `404 ENOTFOUND`. Admin and analyst see everything; `GET /api/users` lists every account with its workspaces, sessions and 7/30-day `{cost, tokens}`, and `GET /api/users/audit[?limit=]` the staff reads of developers' sessions and admins' account changes (both `403 EADMIN` for a developer). See [SERVER-MODE.md](SERVER-MODE.md#what-each-user-sees).
In server mode a **developer** sees only rows of the workspaces they own (`lanes.owner_id`): every list, count, chart and cost answer is limited to them, and a request naming another user's session, agent, workspace or run (in the path, query or body) answers `404 ENOTFOUND`. Admin and analyst see everything; `GET /api/users` lists every account (`displayName`, `email`, `createdAt`, `logins`, `lastLoginAt` included) with its workspaces, sessions and 7/30-day `{cost, tokens}`, and `GET /api/users/audit[?limit=]` the staff reads of developers' sessions and admins' account changes (`reader`/`owner` are usernames, `reader_display`/`owner_display` their display names or null) (both `403 EADMIN` for a developer). See [SERVER-MODE.md](SERVER-MODE.md#what-each-user-sees).
An admin changes accounts with `POST /api/users` (`{username, password, role}` → `201 {user}`), `PATCH /api/users/:id` (`{role?, disabled?}`), `POST /api/users/:id/password` (`{password}`; that user's logins end) and `POST /api/users/:id/workspaces` (`{laneId}`; the workspace's owner becomes that user). Every one carries `currentPassword`, the admin's own password: `403 EREAUTH` when it is wrong or missing, `429 ETOOMANY` after 10 wrong tries in 15 minutes. `409 ESELF` for an admin's own role or disable, `409 EEXISTS` for a taken name, `400 EBADUSER`/`EBADPASSWORD`/`EBADROLE`, `404 ENOUSER`/`ENOLANE`; a developer or analyst gets `403 EADMIN`, and local mode `403 ELOCALMODE`. No answer carries a password hash; each change is written to the audit log.
An admin changes accounts with `POST /api/users` (`{username, password, role}` → `201 {user}`), `GET /api/users/:id` (`{user, workspaces: [{id, title, cwd}]}`, the account with `logins` and `lastLoginAt`, for the Edit form; admin or analyst), `PATCH /api/users/:id` (any of `{displayName?, email?, role?, disabled?, password?, addWorkspaces?: [laneId], removeWorkspaces?: [laneId], signOut?: true}` — every field validated before any is written; `removeWorkspaces` leaves the workspace with no owner and answers `409 ENOTOWNED` for one the user does not own; an admin's own `password`/`signOut` keeps the login it came from), `POST /api/users/:id/password` (`{password}`; that user's logins end) and `POST /api/users/:id/workspaces` (`{laneId}`; the workspace's owner becomes that user). Every one carries `currentPassword`, the admin's own password: `403 EREAUTH` when it is wrong or missing, `429 ETOOMANY` after 10 wrong tries in 15 minutes. `409 ESELF` for an admin's own role or disable, `409 EEXISTS` for a taken name, `400 EBADUSER`/`EBADPASSWORD`/`EBADROLE`/`EBADEMAIL`/`EBADFIELD`, `404 ENOUSER`/`ENOLANE`; a developer or analyst gets `403 EADMIN`, and local mode `403 ELOCALMODE`. No answer carries a password hash; each change is written to the audit log.
`GET /api/accounts[?force=1]` lists the Claude account pool ([SERVER-MODE.md](SERVER-MODE.md#claude-account-pool)): `{accounts: [{name, addedAt, expiresAt, expired, livePanes, spend5h, spend7d, quota: {fiveHour, sevenDay, checkedAt, error} | null, quotaPercent, full}], codex: [{name, kind: "apikey"|"chatgpt", addedAt, livePanes, spend5h, spend7d, quota: {windows: [{label, usedPercent, resetsAt}], asOf} | null, quotaPercent, full}], blockPercent}` — `codex` is the [Codex account pool](SERVER-MODE.md#codex-account-pool). It never includes a key or a token. `force=1` re-reads every quota first. Run rows (`/api/run/history`) carry `account`, the pool account their pane ran on (`null` = the machine's own login).
An admin (server mode) adds an account with `POST /api/accounts` `{tool: "claude"|"codex", name, currentPassword, …}` → `201 {account}`. For Claude the body carries one of: `profile` (a saved switch-user profile, MOVED in: the profile is deleted; the one the host's CLI is logged into moves in with its live login and the host's home becomes one more placement the server refreshes — `409 EACTIVE` only for a macOS Keychain login; `name` defaults to it), `credentials` (the text of a `.credentials.json`; Anthropic is asked whose account it is, `502 EUPSTREAM` when it cannot be reached), or `token` (a `claude setup-token` token). For Codex, `token` is an OpenAI API key or the text of an `auth.json`. `GET /api/accounts/profiles` lists the saved profiles `{profiles: [{name, label, email, subscriptionType, active}]}` (no token). Claude rows in `GET /api/accounts` carry `kind` (`token`|`login`), `email` and `refreshError`; a login's `expiresAt` is null and removes one with `DELETE /api/accounts/:name` `{tool, currentPassword}` → `{ok: true}`. No answer carries the token. `403 EREAUTH` wrong password (`429 ETOOMANY` after 10), `403 EADMIN` for a non-admin, `403 ELOCALMODE` in local mode, `400 EBADTOOL`/`EBADNAME`/`EBADTOKEN`, `409 EEXISTS`, `404 ENOACCOUNT`; each change is audited by account name.
**The account's own preferences and first-run setup** (`server/routes/me.js`; local mode is the one account `local`, server mode one row per account in `user_prefs`):
| Route | Answer |
@@ -100,6 +102,10 @@ An admin changes accounts with `POST /api/users` (`{username, password, role}`
| `GET /api/me/instructions` | `{instructions, max, tools}` — `tools` lists the AI tool ids whose new panes receive them (`["claude","codex"]`; Cursor CLI and Antigravity have no per-session way to take them). The account's own agent instructions (Settings → AI tools → My agent instructions, `""` when none); `401 EAUTH` with no login |
| `PUT /api/me/instructions` `{instructions: "<markdown>" \| null}` | replaced, the same shape back; blank or `null` clears; `400 EBADFIELD` not a string, `413 ETOOBIG` over 16 000 characters. Put BEFORE the workspace's own instructions in every new Claude/Codex pane of a workspace the account owns (local mode: every workspace) — see Workspace context below |
| `POST /api/me/onboarded` | the first-run setup is done or skipped (kept from the first call) |
| `GET /api/me/account` | Settings → Account. Server mode: `{mode: "server", user: {id, username, role, disabled, displayName, email, createdAt}, workspaces, sessions, logins, lastLoginAt}`; local mode `{mode: "local"}` |
| `PATCH /api/me/account` `{displayName?, email?}` | the account's own profile (`""`/`null` clears), the same shape back; `400 EBADFIELD` (display name over 100 characters or with control characters) / `EBADEMAIL` |
| `POST /api/me/password` `{currentPassword, newPassword}` | `{ok: true}`; every other login of the account ends, this one stays. `403 EREAUTH` wrong current password (`429 ETOOMANY` after 10), `400 EBADPASSWORD` |
| `POST /api/me/sign-out-others` | `{ended}` — every login of the account but this one ends |
The keys are the browser's own localStorage names: `theme`, `agenthub.theme.dark`, `agenthub.theme.light`, `i18nextLng`, `agenthub.terminal.font`, `agenthub.pet`, `agenthub.pet.size`, `agent-dashboard-sagi-enabled`, `agent-dashboard-sagi-muted`, `agent-monitor-notifications`, `agenthub.defaultTool`, `agenthub.permissionMode` (never `bypassPermissions` — bypass stays the per-session switch), `agenthub.tour`, and `agenthub.chatUi` (`"1"` = Claude/Codex sessions open as a chat; read and written on the server only, by `client/src/lib/chatUi.ts`). The request never names an account, and writes are same-origin. An account's save writes only their own row, and under S3 a `theme` of `dark` or `light` also becomes the `theme` in their own `~/.claude/settings.json`, written as them.
@@ -240,7 +246,12 @@ those at the top of a non-admin's own folder), every entry carries its `mtime`
(ms) and the answer its `home` (the browser's Home place). In server mode with
`AGENTHUB_USERS_ROOT` set (under S3, else server.conf's `USERS_ROOT`), a
developer's browser opens on their own folder (`parent: null` there) and any
path whose real path leaves it is `403 EFOLDER`. The browser's **New folder**
path whose real path leaves it is `403 EFOLDER`. An admin is held to no
folder, but one with a Linux account of their own (S3) opens on THEIR home
(`/srv/agenthub/users/<name>`) and the answer carries `hostHome` (the host
account's home); `host=1` opens that instead — the browser's **Server folder**
tick, unticked at every open, because a workspace there runs as the host
account every admin shares. The browser's **New folder**
is `POST /api/lanes/browse/mkdir` (`{parent, name}` → `201 {path}`): one plain
folder name inside an existing folder, `400 EBADNAME`/`ENOTFOUND`,
`409 EEXISTS`; a non-admin only inside their own folder, and in server mode
@@ -1619,6 +1630,20 @@ curl -o shot.png "http://localhost:4820/api/sessions/sess_abc123/image?path=/hom
---
#### Get a Text File the Session Read or Wrote
```http
GET /api/sessions/:id/file?path=<absolute path>
```
`{ "text": "...", "truncated": false }` for a text file the session's own `Read`, `Write`, `Edit`, `MultiEdit` or `NotebookEdit` named (a `PostToolUse` event of this session whose `tool_input.file_path` or `notebook_path` equals `path` exactly) — how a Chat pane's Outputs/Files preview shows a file outside the workspace folder (a report written to `~/Documents`). At most the first 512 KB (`truncated: true` beyond). 400 `EBADPATH` for a relative path; 403 `ESECRET` for a credential by name (`isSecretConfigPath` — `.credentials.json`, `auth.json`, AgentHub's own stores and data dir — or a `settings.local.json`), judged on the path and on what it resolves to; 415 `EBINARY` for a file with a NUL byte in its first 8 KB; 404 for an unknown session, a path the session never touched, or a non-regular or missing file. 403 `EUNSUPPORTED` and the S3 member rules are the same as for `/image` above.
```bash
curl "http://localhost:4820/api/sessions/sess_abc123/file?path=/home/me/Documents/REPORT.md"
```
---
#### Get Session Stats
```http
@@ -2665,6 +2690,7 @@ Each tool: `{ id, label, command, homepageUrl, installed, path, enabled, source:
```
GET /api/ai-proxies Every saved profile, the built-in `native` one first
GET /api/ai-proxies/9router `{ installed }` — is a `9router` binary on the server's PATH (nothing is run)
POST /api/ai-proxies Create — Body: { label, baseUrl, apiKey?, model, models?, behavesAs?, scope? }
POST /api/ai-proxies/test Probe an endpoint — Body: { baseUrl, model, apiKey?, name? }
PATCH /api/ai-proxies/:name Update the same fields, all optional
@@ -2685,6 +2711,8 @@ A **proxy profile** is an endpoint that stands in front of Claude Code or Codex
**No response here ever contains key material.** A saved profile reports `hasKey: true|false` and there is deliberately no `GET /:name` — a route returning one profile in full would be the single way an API key could leave the server. Profiles live one JSON file each under `<CLAUDE_HOME>/agenthub-proxies/` (dir `0700`, files `0600`); the first read after the rename moves an existing `agenthub-routers/` there and leaves a symlink at the old name, so an older build on the same CLAUDE_HOME still finds them. An omitted `apiKey` on `PATCH` keeps the stored one; the edit form cannot display a key, so it has none to send back.
**`GET /api/ai-proxies/9router`** answers `{ installed: boolean }` from a PATH lookup only (the server's and the login shell's PATH); the AI Proxy page shows a **Config 9router** link beside **Add profile**, opening 9router's own UI in a new tab on the dashboard's own host, port 20128 (`http://<the host the page was opened on>:20128` — e.g. `http://10.1.13.7:20128` from a LAN browser, so the viewer needs no 9router of their own; 9router must listen beyond loopback for that), only when it is `true`.
**`POST /api/ai-proxies/test`** answers the question saving cannot: does this endpoint, with this key, actually serve this model — for each CLI? It sends, in parallel and with a 15 s timeout each, one Anthropic-style message capped at a single output token to `<baseUrl>/v1/messages` (what Claude Code calls) and one OpenAI Responses call (`max_output_tokens: 16`) to `<baseUrl>/v1/responses` (what Codex calls). A proxy that answers is a `200` here whatever it replies: `{ claude: { ok, status, message }, codex: { ok, status, message } }`, each carrying the proxy's own words (truncated to 300 characters), `status` `null` when nothing answered at all. A proxy that serves only one wire shape shows one green line and one red. Only bad input is a 4xx.
The stored key is only ever sent to the endpoint it was stored against. A blank `apiKey` falls back to the key of the profile named by `name`, but **only** while the submitted `baseUrl` still equals the saved one (both compared without `/v1`) — so testing a retyped endpoint needs a retyped key (`400 EBADKEY`). Without that rule this route would be a way to make the server POST a saved API key to any URL a browser names.
+1 -1
View File
@@ -400,7 +400,7 @@ The dashboard web UI merges workspaces and runs into a single **Workspace** page
- **Resume interrupted sessions** — when a session's pane dies under it (the machine shut down, tmux was killed, the CLI crashed), its run is recorded `interrupted` instead of `abandoned`, and every page of the dashboard opens the **Resume interrupted sessions?** dialog (`client/src/components/lanes/InterruptedSessionsPrompt.tsx`, from `GET /api/run/interrupted`). Each session is listed with its workspace, tool, name or first prompt and when it stopped, all ticked: **Resume** starts each ticked one in its workspace's first free tab (`POST /api/lanes/:id/start {slot: "auto", resumeSessionId}`, with the run's model, effort and permission mode — never `bypassPermissions`) and opens the first; **Don't resume** (also ×, Escape, the backdrop) resumes none. Either answer dismisses the whole list (`POST /api/run/interrupted/dismiss`), so it is asked once; a resume that failed stays listed. What counts as interrupted: a pane the dashboard did not close (Stop, ×, Quit are `killed`), not a Terminal tab, with a known session — and for Claude, not one the user ended themselves (`SessionEnd` reason `prompt_input_exit`/`logout`). Codex and other tools report no such reason, so their every vanished pane counts; a Codex pane without its opt-in hooks has no known session and is never listed. A session resumed by any other path drops off the list. A run younger than 20 s is never judged dead (a tmux list is a cached answer, and another dashboard on the same database may not have listed a brand-new pane yet). In server mode each user is offered only their own workspaces' sessions.
- **The session view** — `/workspace?lane=<id>&run=<runId>` (a live session picked in the sidebar or Recents, or one just started) shows the console's tabs and the Changes panel, with that run's tab active. The sidebar's workspace row always opens the workspace's home, live tabs or not (a live tab opens from its session row); the **Home** button at the left end of the tab bar opens the home of a workspace whose tabs stay running. Every workspace keeps its own tabs, panes and sessions: another workspace's tab is never shown. A browser notification for a workspace with a live pane opens it here directly.
- **Untracked worktrees** are no longer listed in the sidebar; Settings → Git → **Worktrees** (`showUntracked`, hidden sources `claude` = `<repo>/.claude/worktrees/*` and `agenthub` = `LANES_ROOT`) now decides which of them the home's Add folder suggests.
- **No workspaces yet** — the page shows no banner: a workspace is added with the **+** beside Workspaces in the sidebar (`workspace-tree-add`; folded to a rail, the tree icon unfolds it first) or from a terminal with `agenthub workspace add --cwd $(pwd)`. In the Add-workspace modal, a click into the folder field opens the same picker as **Browse**: the OS's own dialog for a browser on the dashboard's machine (`POST /api/lanes/pick-path`), the in-app folder browser for any other browser — one behind a proxy included (`EREMOTE`); typing a path still works from the keyboard.
- **No workspaces yet** — the page shows no banner: a workspace is added with the **+** beside Workspaces in the sidebar (`workspace-tree-add`; folded to a rail, the tree icon unfolds it first) or from a terminal with `agenthub workspace add --cwd $(pwd)`. In the Add-workspace modal, the folder field is typed into (with suggestions); only **Browse** opens a picker — the OS's own dialog for a browser on the dashboard's machine (`POST /api/lanes/pick-path`), the in-app folder browser for any other browser, one behind a proxy included (`EREMOTE`). A click into the field only focuses it.
- **Reordering** — drag a row and drop it anywhere in the list (a line shows where it lands, by the pointer's position — on another row, on a session list, above the first or below the last), or press **Alt+↑/↓** with a row focused. The order is a **per-viewer preference**, not a workspace fact: it persists to `localStorage` (`agenthub.workspace.laneOrder`) and writes nothing server-side — `GET /api/lanes` and `agenthub workspaces` keep answering in id order, and the order does not follow you to another browser. A workspace the stored order has never seen lands at the END; a stored id whose workspace is gone is skipped.
- **Renaming a workspace** — double-click its sidebar row (or Rename in the row menu) to edit in place. **Enter** saves via `PATCH /api/lanes/:id` with `{title}`, **Escape** and clicking away both cancel, and saving an empty name clears the title, which puts the folder name back in its place. Renaming is display-only — a managed workspace's branch name was slugified from its title once, at creation.
- **No header row** — the session view has no header of its own: the console's tab bar is it (status, the workspace's name and the back arrow are gone — the name is on the sidebar row and the home). Only a workspace with an installed pipeline template shows its time-on-stage, pipeline picker and feature picker above the console; there are no stage chips — the declared stage and the `auto: <stage>` detection both render in the PipelineMap. While one pane is shown, its split right/down menus sit at the right end of the tab bar; each offers **New terminal** and the same AI tools as the `+` menu. The detail block (the workspace card, the PipelineMap, the proof gallery) is always shown, and Delete lives in the sidebar row menu. There is no Tools menu any more: MCP sync and the sync-base check are the CLI's `agenthub workspace mcp sync` and `agenthub workspace sync-base`, and a readme refresh is a skill typed into a session.
+71 -3
View File
@@ -115,7 +115,8 @@ their own workspaces (`useWorkLanes`); other users' work is read on Sessions,
Analytics and Users, where Resume, Fork, Snapshot and "Open in workspace" are
hidden for a session that is not theirs. The sidebar's workspace list shows an
admin or analyst EVERY workspace, grouped under its owner (each lane carries
`owner_name`; yours first, no owner last, each group folds): for an analyst
`owner_name` and `owner_display_name`; the group shows the display name when
the owner set one, else the username; yours first, no owner last, each group folds): for an analyst
another user's workspace there is read only — no menu, rename or new session,
no live tabs, and a session opens its analysis page (`/sessions/<id>`). An analyst's alert feed
(`GET /api/alerts`) carries `can_ack` on each alert — true only for their own
@@ -259,13 +260,36 @@ settings file that lacks the variable falls back to `USERS_ROOT` in
## Accounts
An admin manages accounts on the **Users** page (sidebar; admins and analysts
see it, an analyst only reads): add a user, change a role, disable or enable,
reset a password, give a workspace. Every change asks for the admin's OWN
see it, an analyst only reads): **Add user**, and one **Edit** form per account
that changes, in a single save, its display name and email, role,
active/disabled, password (typed or generated), the workspaces it owns (give
one, or take one back — it is then left with no owner) and "sign out
everywhere". Every field is checked before any is written, so a bad email or
a workspace the user does not own changes nothing. Every change asks for the admin's OWN
password again — the server refuses without it (`EREAUTH`, and `ETOOMANY`
after 10 wrong tries in 15 minutes) — an admin can never demote or disable
themselves (`ESELF`), and each change is written to the audit log. In local
mode these routes refuse (`ELOCALMODE`): there is no login to check.
Wherever the dashboard names a person — the sidebar's sign-out tooltip, the
workspace list's owner groups, the Users page and its audit log, the Edit user
form — it shows their display name when one is set, else the username (the
Users page keeps `@username` beside a display name). Logging in, the CLI and
the Linux account always use the username.
Under S3 an admin's folder browser opens on their own home too; ticking
**Server folder** opens the host account's home (`/home/<host>`), where a
workspace runs as the host account that every admin shares. The tick is never
remembered, so the default is always the admin's own account.
Every signed-in user has **Settings → Account**: their username, role, member
since, workspace and session counts and active logins; their own display name
and email; **Change password** (the current password is required, the login
it is changed from stays and every other one ends); and **Sign out other
devices**. These are taken from the login cookie only — an agent in a pane,
which acts as its workspace owner through its pane token, cannot change its
owner's password or logins.
The same from the host's shell:
```bash
@@ -296,6 +320,43 @@ agenthub accounts list # live panes, $ in the last 5 h / 7 d, quota, stat
agenthub accounts remove team-1
```
In server mode an admin can do the same on the **Users** page, in the
**Account pool** card: **Add account** and the bin icon to remove one. A Claude
account comes from one of three sources:
- **A saved login**: a profile from Settings → Claude config → Switch account
(log in with `claude auth login`, then save it). It is MOVED into the pool, so
the profile leaves the switcher. The account this server's own Claude Code is
logged into can be moved in too. It moves with the login the CLI holds right
now, keeps working on the server, and the server's own `.credentials.json`
becomes one more home the pool refreshes and writes into, keeping its other
keys. Only a login kept in the macOS Keychain is refused (`EACTIVE`). Removing
that account later leaves the server logged in.
- **A pasted `.credentials.json`**: from `~/.claude/.credentials.json`, or on a
Mac `security find-generic-password -s "Claude Code-credentials" -w`. Stop
using that login where you copied it from.
- **A `claude setup-token` token** (`sk-ant-…`), as with `agenthub accounts add`.
A Codex account is an OpenAI API key, or the whole `~/.codex/auth.json` after
`codex login`.
A **login** (the first two sources) works like a Codex ChatGPT login. Claude
Code reads it from no env var, so before a member's pane starts, AgentHub writes
it as that member's `~/.claude/.credentials.json`, as the member (0600). It is
never written into a host pane's home, never over a login the member made
themselves, and never a second account into a home where a pane of theirs runs
on one. Host panes therefore get only setup-token accounts. The refresh token of
a login is single use, so the server is the one that refreshes it, before it
expires (`claude-pool.refreshLogins`, checked every 60 s; `DASHBOARD_POOL_REFRESH_MS=0` turns it off). It then writes the new pair into every
home it placed the login in, and Claude Code picks up tokens another process
refreshed. A pair a member's Claude refreshed first is copied back only after
Anthropic confirms it is the same account. Removing the account deletes it from
the homes that still hold AgentHub's copy. Each
change asks for the admin's own password again and is written to the audit log
by account name. The token only goes in; the page, like `agenthub accounts list`,
never shows it again. An analyst sees the card read-only; a developer does not
see it. Every user shares the pool: a user created later needs no setup of their own.
How a new session picks its account (`server/lib/claude-pool.js`):
1. An account whose last quota reading is at or above 90% in the 5-hour or
@@ -308,6 +369,13 @@ How a new session picks its account (`server/lib/claude-pool.js`):
- The pane gets the token through tmux `-e CLAUDE_CODE_OAUTH_TOKEN=…`. That
token outranks the machine's own `/login` for that process only.
- A session on an AI Proxy keeps the proxy's key and does not use the pool.
- With one Linux user per account (S3), a member's home starts empty, and a
fresh Claude Code shows its "Select login method" screen even with the token
set. So before a member's pane starts on a pool account or an AI Proxy,
AgentHub adds `hasCompletedOnboarding: true` to that member's
`~/.claude.json`, written as the member, with every other key kept
(`run-as.markClaudeOnboarded`). With an empty pool and no proxy, a member's
pane still needs that member's own `/login`.
- A token more than a year old is skipped.
- A resumed session picks an account again, like a new one.
- Quota is read with each account's own token, in the background, at most every
+1 -1
View File
@@ -63,7 +63,7 @@ metadata:
**2026-10-05 v2.0.1 RELEASED:** b0cf213 Windows AI-CLI detection (which() tries PATHEXT, skips npm sh shim; spawnTarget = shell:true + quoted path for .cmd in ai-tool-jobs/versions) + c067abc bump, tag, forge release 544. NOT verified on a real Windows box — user (machine kingg, Win11) must /agenthub-update then Refresh in Settings → Agents.
**2026-10-05 v2.0.0 RELEASED:** 198fbb7 (all pending work) + 094d9c4 bump pushed to origin main, tag v2.0.0, forge release 543 (EN+VI notes). docs/plans/analytics-parity/ deliberately left UNTRACKED: it names Claude-Code-Agent-Monitor, which repo-links.test.js forbids in tracked files. Pre-commit hook runs the full suite (~4 min) — run git commit in background.
**2026-10-05 v2.0.0 RELEASED:** 198fbb7 (all pending work) + 094d9c4 bump pushed to origin main, tag v2.0.0, forge release 543 (EN+VI notes). docs/plans/analytics-parity/ deliberately left UNTRACKED: it names the retired repository, which repo-links.test.js forbids in tracked files. Pre-commit hook runs the full suite (~4 min) — run git commit in background.
**2026-10-05 (65fe680 daily cost by spend day — token_usage_hourly; ad6e801 right-click menus everywhere + status bar Workspace $/Session $ + long-term DB safety; unpushed):** user first chose to keep all event data; on 2026-10-05 (later) REVERSED for payloads only: trim events.data at insert + one-time sweep (whole files/long output dropped, kept in transcripts), still no row prune and no VACUUM, transcript snapshots kept. See [[analytics-parity-plan]]. Live DB 1.2 GB (events 1.08 GB) + 346 MB WAL until a dashboard runs ad6e801.