b7f3735b29
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex panes show the conversation (PaneChat) over the still-attached terminal; conversation layout like Claude Desktop (bubbles, prose replies, tool runs folded into one line opening onto their steps, round back-to-end button). - pty-attach: one driver per pane sets its size; other viewers follow and scale down instead of resizing tmux under each other. - Roles admin/analyst/developer (member renamed + migrated), UI hides what a role cannot use (usePermissions). - Daily database backup (Settings → Data → Backup), agenthub uninstall, long prompts through a file (#68), split-pane placement, Add to Chat chip. Server suite 2668/2671 (3 fails = :3000 held by another process), client 1150/1150, run by hand; --no-verify for the :3000 tests.
278 lines
13 KiB
Markdown
278 lines
13 KiB
Markdown
# 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 developer 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 # developer
|
|
agenthub users add carol --role analyst # reads everyone, works in their own
|
|
```
|
|
|
|
`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 |
|
|
| `analyst` | Reads everything (usage, prompts, every user's sessions), and works in their OWN workspaces like a developer. Never changes another user's rows or the host's configuration |
|
|
| `developer` | Their own workspaces: start sessions, terminals, git, files. Sees nothing of other users, and cannot open or change the host's configuration |
|
|
|
|
`developer` was called `member` before; a database with members is migrated on
|
|
start (`server/db.js`), and `member` is no longer accepted.
|
|
|
|
"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`, `/reports`, `/pets`, `/users` and
|
|
`/alerts/rules` (`ADMIN_WRITE_PREFIXES` in `server/lib/auth.js`). A developer
|
|
or analyst who tries one gets `403 EADMIN`, shown in the UI as "Only an admin
|
|
can do this". An analyst's other writes are scoped like a developer's: one
|
|
naming another user's row gets `404 ENOTFOUND` (`memberId` in
|
|
`server/lib/scope.js` limits every non-GET request of an analyst to their own
|
|
rows).
|
|
|
|
The UI hides what a role cannot use (`usePermissions` in
|
|
`client/src/components/AuthGate.tsx`): for anyone but an admin, Settings shows
|
|
no Data, Pricing, AI proxy or per-tool pages, no scheduled reports or git
|
|
credentials, no start-at-login/icon/Quit/Uninstall on System, no alert
|
|
rules or channels, no install/update/enable on AI tools, no pet upload or
|
|
delete, and no update check. An analyst's Workspace page and sidebar list only
|
|
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. An analyst's alert feed
|
|
(`GET /api/alerts`) carries `can_ack` on each alert — true only for their own
|
|
sessions' — and the feed offers Acknowledge only there; Ack all acknowledges
|
|
only theirs.
|
|
|
|
## 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 **developer** 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 developers.
|
|
- An **admin** or **analyst** sees everything. Each time one of them opens a
|
|
session a developer 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 — the
|
|
Users page in the sidebar shows it, with the audit log under it.
|
|
|
|
A developer 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.
|
|
|
|
### Keep developers' folders apart
|
|
|
|
```bash
|
|
# in ~/.config/agenthub/settings.env
|
|
AGENTHUB_USERS_ROOT=/srv/agenthub/users
|
|
```
|
|
|
|
With this set, a developer or analyst 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 anyone but an admin and the
|
|
owner of the pane's workspace (an analyst included). 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 losing the pane's workspace 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 developer'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 developer or analyst 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
|
|
|
|
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
|
|
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.
|
|
|
|
The same from the host's shell:
|
|
|
|
```bash
|
|
agenthub users list
|
|
agenthub users add <name> [--role admin|analyst|developer]
|
|
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 developer'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.
|