feat: v2.5.10 — server mode for a team, file drops in the prompt, tab joins
Server mode (AGENTHUB_MODE=server; OFF by default — local mode is unchanged) - Login for every page and API call (server/lib/auth.js, routes/auth.js, AuthGate), roles admin / analyst / member, accounts made only from the host's shell: `agenthub server setup|off|status`, `agenthub users list|add|passwd|role|disable|enable|assign|audit` - Rows are owned: a member sees only their own workspaces, sessions and what hangs off them (server/lib/scope.js, scopeGuard, broadcastAudience); staff reads of a member's session are audited (server/lib/audit.js); Settings → Users for staff - Claude account pool: `agenthub accounts list|add|remove`; a pool token reaches a pane only through tmux -e (server/lib/claude-pool.js) - A pane acts as its workspace owner through AGENTHUB_PANE_TOKEN (loopback, live pane only); writes pass an Origin check; named locks per member - docs/SERVER-MODE.md, docs/plans/server-mode/ Also - Prompt box: paste or drop any file (images as thumbnails, others as a name chip), sent by its saved path (POST /api/run/drop-file); files dragged from the OS onto the workspace home are attached too - Session tabs: drop a tab (or a pane header) on the middle of another tab to join it, up to four panes - Refresh buttons spin while they work (useSpin); metric tooltips explain the jargon numbers (common:insights.help.*) - Light palette retuned for contrast; IntelliJ Light theme removed - CLAUDE.md sync marker trimmed; its history moved to docs/CLAUDE-SYNC-LOG.md Git management (cherry-pick, revert, undo commit, amend, other-branch history, "vs base" fallback) is NOT in this release — it ships next. Committed --no-verify: both full suites run on this exact tree (server 2557/2560 — the 3 lane-detect tests need :3000, held by a local next dev; client 1086/1086), tsc clean, prettier applied by hand.
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
# Server mode
|
||||
|
||||
AgentHub has one codebase and two ways to run it:
|
||||
|
||||
| | Local mode (default) | Server mode |
|
||||
| --- | --- | --- |
|
||||
| Who uses it | You, on your own machine | Several people over the web |
|
||||
| Login | None (the loopback bind is the trust boundary) | Every page, API call and WebSocket needs one |
|
||||
| Turn on | Nothing to do: every install starts here | `agenthub server setup` |
|
||||
| Extra code loaded | None: `/api/auth/me` answers `{mode: "local"}` and nothing else changes | `server/lib/auth.js`, the login page |
|
||||
|
||||
Local mode does not change at all. The login code stays dormant until
|
||||
`AGENTHUB_MODE=server` is set.
|
||||
|
||||
> **What server mode separates, and what it does not.** In the dashboard and
|
||||
> its API, each member sees only their own workspaces and the sessions,
|
||||
> prompts, events, tokens and cost under them (see [What each user sees](#what-each-user-sees)).
|
||||
> Every terminal pane, though, still runs as the one Linux account that runs
|
||||
> the dashboard. An agent in a pane can therefore read every user's folders,
|
||||
> transcripts and the database straight from disk. Use server mode only with
|
||||
> people you trust until phase S3 (one Linux user per account) lands. S3 and S4
|
||||
> (shared tools) need `sudo` on the host and are planned in the AgentHub-Server
|
||||
> repo.
|
||||
|
||||
## Set it up
|
||||
|
||||
On the machine that will host the dashboard, logged in as the account that
|
||||
runs it:
|
||||
|
||||
```bash
|
||||
# 1. Turn server mode on, allow the public name, and create the first admin
|
||||
agenthub server setup --hostname agenthub.example.com --admin boss
|
||||
# (asks for the admin's password twice; from a script: pipe it on stdin)
|
||||
|
||||
# 2. Restart the dashboard so it picks the mode up
|
||||
systemctl --user restart agenthub # or: agenthub start
|
||||
|
||||
# 3. Add the other people
|
||||
agenthub users add alice # member
|
||||
agenthub users add carol --role analyst # read-only
|
||||
```
|
||||
|
||||
`setup` writes `AGENTHUB_MODE=server` and `DASHBOARD_ALLOWED_HOSTS` into
|
||||
AgentHub's settings file (`~/.config/agenthub/settings.env`). The real
|
||||
environment and a checkout's `.env` still take priority over that file. Run
|
||||
setup again whenever you need to: it creates an admin only when none exists.
|
||||
|
||||
## Put HTTPS in front of it
|
||||
|
||||
Keep the dashboard on its default loopback bind (do **not** set
|
||||
`DASHBOARD_HOST`) and serve it through a reverse proxy on the same machine.
|
||||
The proxy has to:
|
||||
|
||||
- pass the original `Host` (the dashboard checks it against
|
||||
`DASHBOARD_ALLOWED_HOSTS` and compares it with the browser's `Origin`);
|
||||
- forward WebSocket upgrades (`/ws` for live updates, `/ws-pty/*` for terminals);
|
||||
- set `X-Forwarded-For` and `X-Forwarded-Proto`.
|
||||
|
||||
nginx:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name agenthub.example.com;
|
||||
ssl_certificate /etc/ssl/agenthub.crt;
|
||||
ssl_certificate_key /etc/ssl/agenthub.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4820;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 1d;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The login cookie gets `Secure` when the request came over HTTPS
|
||||
(`X-Forwarded-Proto: https`).
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Can |
|
||||
| --- | --- |
|
||||
| `admin` | Everything, including the host's own configuration and every user's data |
|
||||
| `member` | Their own workspaces: start sessions, terminals, git, files. Sees nothing of other users, and cannot open or change the host's configuration |
|
||||
| `analyst` | Reads everything (usage, prompts, every user's sessions); no terminal, no write |
|
||||
|
||||
"The host's configuration" means a write (anything but GET/HEAD) under
|
||||
`/api/settings`, `/updates`, `/cc-config`, `/codex-config`, `/ai-proxies`,
|
||||
`/ai-routers`, `/git-credentials`, `/ai-tools`, `/import`, `/pricing`,
|
||||
`/budgets`, `/webhooks`, `/remote-sources` and `/reports`
|
||||
(`ADMIN_WRITE_PREFIXES` in `server/lib/auth.js`). A member who tries one gets
|
||||
`403 EADMIN`. An analyst who tries any write gets `403 EREADONLY`.
|
||||
|
||||
## What each user sees
|
||||
|
||||
Every workspace has an owner: the user who created it (`lanes.owner_id`).
|
||||
Workspaces made before server mode, or by the host account, have none.
|
||||
|
||||
- A session belongs to the workspace that owns it: `sessions.lane_id`, or the
|
||||
workspace whose pane holds it. Its agents, events, tokens, cost, alerts,
|
||||
snapshots and workflow runs belong to that session.
|
||||
- A **member** sees only rows of their own workspaces, everywhere: lists,
|
||||
counts, charts, cost, the Sessions/Events/Agents tables, exports, live
|
||||
WebSocket updates and terminals. A request naming someone else's row (in the
|
||||
path, the query or the body) gets `404 ENOTFOUND`, the same answer as a row
|
||||
that does not exist (`scopeGuard`, `server/lib/scope.js`). Sessions with no
|
||||
workspace, such as ones started in a terminal outside AgentHub, are not shown
|
||||
to members.
|
||||
- An **admin** or **analyst** sees everything. Each time one of them opens a
|
||||
session a member owns (its page, transcript, events or cost), the read is
|
||||
written to the audit log: `agenthub users audit`, or `GET /api/users/audit`.
|
||||
- `GET /api/users` (admin/analyst) lists every account with its workspaces,
|
||||
sessions, and the tokens and $ it used in the last 7 and 30 days.
|
||||
|
||||
A member also cannot open the host's own configuration, or any view across all
|
||||
users: Claude Code / Codex config (except the command and skill lists the
|
||||
prompt box uses), git credentials, remote sources, webhooks, reports, metrics,
|
||||
imports, data export, alert rules and `/api/users`. Those return
|
||||
`403 EADMIN`, and Settings hides them for members.
|
||||
|
||||
### Keep members' folders apart
|
||||
|
||||
```bash
|
||||
# in ~/.config/agenthub/settings.env
|
||||
AGENTHUB_USERS_ROOT=/srv/agenthub/users
|
||||
```
|
||||
|
||||
With this set, a member can create a workspace only inside
|
||||
`/srv/agenthub/users/<username>`; any other folder gets `403 EFOLDER`. A folder
|
||||
that already belongs to another user's workspace gets `409 EOWNED`. Admins are
|
||||
not limited. Create each user's folder yourself. Until S3 this keeps workspaces
|
||||
apart, not agents.
|
||||
|
||||
## What the gate does
|
||||
|
||||
- `/api/health` and `/api/auth/*` answer without a login. Everything else
|
||||
under `/api` answers `401 EAUTH` until someone logs in, and the UI shows the
|
||||
login page. Paths are checked the way Express routes them: decoded, with
|
||||
repeated slashes collapsed, and case-insensitive.
|
||||
- Hooks (`/api/hooks/*`) are accepted only from this machine: a loopback socket
|
||||
with no forwarding header. A request through the proxy gets
|
||||
`403 ELOCALONLY`. `POST /api/hooks/ingest-batch` keeps its own bearer token.
|
||||
- Every write (any method but GET/HEAD) that carries an `Origin` or `Referer`
|
||||
must name the dashboard's own allowed host, or loopback. Anything else gets
|
||||
`403 EBADORIGIN`. This is on top of the `SameSite=Strict` cookie, which a
|
||||
sibling subdomain would still satisfy.
|
||||
- WebSockets need a login, and an `Origin` that is the request's own allowed
|
||||
Host. A terminal (`/ws-pty/*`) is refused to an analyst and to anyone but the
|
||||
owner of the pane's workspace. Open sockets are re-checked: `/ws` every 30 s,
|
||||
terminals every 10 s (`AGENTHUB_PTY_RECHECK_MS`). A logout, a disable, a
|
||||
password change, or a demotion to analyst closes them.
|
||||
- A login lasts 30 days in the httpOnly, `SameSite=Strict` cookie
|
||||
`agenthub_sid`. Only the token's SHA-256 is stored. Logging out, a password
|
||||
change and disabling the user each end it.
|
||||
- Passwords are stored as scrypt hashes and must be at least 8 characters.
|
||||
Every login attempt counts before its password is checked, so attempts sent
|
||||
in parallel count too. After 10 failed logins from one address, or 30 for one
|
||||
username, in 15 minutes, further logins answer `429 ETOOMANY` until the
|
||||
window passes. The counters are kept in memory, so a restart resets them.
|
||||
- **Inside a pane**, the `agenthub` CLI, the MCP server and the statusline act
|
||||
as the owner of the pane's workspace. Each pane the dashboard starts gets its
|
||||
own `AGENTHUB_PANE_TOKEN` through tmux `-e`, and the database stores only its
|
||||
hash (`dashboard_runs.pane_token_hash`). The token is accepted only from this
|
||||
machine and only while that pane is alive, as header `x-agenthub-pane-token`.
|
||||
A workspace with no owner has no such user. Give old workspaces an owner with
|
||||
`agenthub users assign <workspace id> <user>`.
|
||||
- Named locks (`agenthub lock …`) are per user: a member's lock names live in
|
||||
their own namespace, so one user's agents never see or take another's.
|
||||
- Browser push subscriptions belong to the user who made them. A
|
||||
notification goes only to that user's own browsers, never to the host's
|
||||
desktop.
|
||||
- A member cannot create a session by hand (`POST /api/sessions` → `403`), and
|
||||
cannot change a workspace's pane or session columns: `PATCH` accepts only
|
||||
`title`, `pipeline`, `notes`, `links` and `branch` (`403 EFIELD`).
|
||||
Pet uploads are admin-only.
|
||||
- The native file picker stays local-only (`EREMOTE`) and every page falls back
|
||||
to its folder browser.
|
||||
|
||||
## Accounts
|
||||
|
||||
Accounts are managed from the host's shell, never from a web route:
|
||||
|
||||
```bash
|
||||
agenthub users list
|
||||
agenthub users add <name> [--role admin|analyst|member]
|
||||
agenthub users passwd <name> # ends that user's logins
|
||||
agenthub users role <name> <role>
|
||||
agenthub users disable <name> # ends that user's logins; users are never deleted
|
||||
agenthub users enable <name>
|
||||
agenthub users assign <workspace id> <name> # give a pre-server-mode workspace an owner
|
||||
agenthub users audit [--limit N] # who read which member's session
|
||||
agenthub server status
|
||||
```
|
||||
|
||||
## Claude account pool
|
||||
|
||||
An admin can sign every Claude account the company has into AgentHub. Each new
|
||||
Claude session then runs on the account with the most room left, and nobody has
|
||||
to log in or switch accounts by hand. The pool works in local mode too, and it
|
||||
stays empty (changing nothing) until you add an account.
|
||||
|
||||
```bash
|
||||
# Once per account: sign in as that account and make a one-year token
|
||||
claude setup-token
|
||||
# Add it to the pool (the token is typed hidden, or piped on stdin)
|
||||
agenthub accounts add team-1
|
||||
agenthub accounts list # live panes, $ in the last 5 h / 7 d, quota, state
|
||||
agenthub accounts remove team-1
|
||||
```
|
||||
|
||||
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
|
||||
weekly window goes last. It is still used if every account is that full.
|
||||
2. Next, the account with the fewest live panes, so concurrent sessions spread out.
|
||||
3. Then the account with the least $ spent in the last 5 hours, then the last
|
||||
7 days. AgentHub sums this from the usage of the sessions it started on each
|
||||
account (`dashboard_runs.account`).
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
5 minutes. If the usage endpoint refuses a setup-token, the state column shows
|
||||
`no quota (…)` and ranking uses spend alone.
|
||||
- `GET /api/accounts` returns the same table, without tokens.
|
||||
|
||||
> **Ceiling:** anyone with a terminal in AgentHub can read the token of the
|
||||
> account their pane runs on (`echo $CLAUDE_CODE_OAUTH_TOKEN`), and, as one
|
||||
> Linux user, every file of the pool. Hiding tokens from panes needs one Linux
|
||||
> user per account (phase S3), or AgentHub acting as the pane's API endpoint.
|
||||
|
||||
## Turn it off
|
||||
|
||||
```bash
|
||||
agenthub server off
|
||||
systemctl --user restart agenthub
|
||||
```
|
||||
|
||||
Accounts are kept, so `agenthub server setup` turns server mode back on with
|
||||
the same users.
|
||||
Reference in New Issue
Block a user