a9c71cd905
Usage panel (status bar): - every account shows its plan (Claude subscriptionType, Codex chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier) - new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's own file, sent only to its own host while valid, never refreshed - in-flight quota reads are shared between tabs AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added; Gemini CLI, Hermes and OpenClaw removed. Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat on an editor selection, Settings in three groups, JetBrains-style Changes panel (ticked files/hunks, ignore, blame, file history), terminal font. --no-verify: :3000 is taken by another dev server (lane-detect tests); both suites run by hand — server 2636/2639 (the 3 are :3000), client 1131/1131.
258 lines
12 KiB
Markdown
258 lines
12 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 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 — the
|
|
Users page in the sidebar shows it, with the audit log under it.
|
|
|
|
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
|
|
|
|
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|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.
|