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
+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