Files
AgentHub/docs/SERVER-MODE.md
T
nntrivi2001 a9c71cd905 feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
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.
2026-10-08 12:22:14 +07:00

12 KiB

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). 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:

# 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:

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

# 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:

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.

# 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

agenthub server off
systemctl --user restart agenthub

Accounts are kept, so agenthub server setup turns server mode back on with the same users.