# Multi-user server — AgentHub as a shared AI-tools host _Status: the original design, written 2026-10-02. It lived for a while in a separate AgentHub-Server repo, which has since been deleted; since 2026-10-09 it is kept here again, and AgentHub is the only source. Kept as the design record. For what was built and what is still open, see [`S3-PLAN.md`](S3-PLAN.md) and [`CHECKLIST.md`](CHECKLIST.md). The operator runbook is in [`docs/SERVER-MODE.md`](../../SERVER-MODE.md)._ > Two things changed after this design was written: > - The `member` role is now `developer`. > - An `analyst` is no longer read-only. An analyst reads every user's rows and > writes only their own. ## Where the code lives (decided 2026-10-02) - **No fork while the local AgentHub is still moving.** A fork would need every local fix carried over by hand, and S1–S3 touch nearly every route, the DB and `pty-run.js` — two lines editing those in parallel conflict on every sync. - **Server features go into the AgentHub codebase as a server MODE**, off by default: login, roles and per-user scoping are additive; with the mode off the local app behaves as one implicit admin and nothing changes for it. - **What only a server deployment needs ships in this repo too**: - provisioning, the sudoers rule and the pane helper are in `scripts/server/`, installed by `agenthub server install-system`; - the shared tools layout is `/opt/agenthub/tools`; - the operator docs are the runbook in `docs/SERVER-MODE.md`. (This first went to a separate AgentHub-Server repo. That repo is gone.) - If a separate codebase ever becomes necessary, fork from a tagged, stable AgentHub release, never from work in progress. ## Goal Several people (A, B, C) use one AgentHub over the web. One Linux account (the **host account**) has installed every AI tool and holds the shared logins: AI Router profiles, several Claude native accounts, several Codex accounts. - Each person logs in to AgentHub, works only in their own folder, and can switch Claude/Codex account, pick any AI Router and use any installed tool — without installing a tool or logging in to one themselves. - Nobody sees anyone else's workspaces, sessions, prompts, tokens or cost. - An **admin** sees and manages everything; an **analyst** reads everything (usage, prompts) but changes nothing. ## Why one Linux user per account Isolation inside AgentHub alone covers only the web UI. An agent runs a shell (and can be put in bypass mode) as whatever Linux user started it; if A's agent runs as the same user as B's, "read /srv/agenthub/users/B" just works. Only the OS can stop an agent, so each AgentHub account maps to its own Linux user, and the folders are `0750` with the AgentHub service in the group. The cheaper variant — one Linux user, isolation in the UI only — is phase S1-S2 on their own. It is acceptable only where everyone is trusted and "cannot see" means "is not shown". ## What is shared and what is not | Thing | Shared? | Where | | --- | --- | --- | | Tool binaries | **Yes**, installed once | `/opt/agenthub/tools/` (root or `agenthub` owned, world `r-x`), on every pane's `PATH`. Self-updaters off (`DISABLE_AUTOUPDATER=1` for Claude; per tool, verified in S0); AgentHub's **Update** runs as the owner of `/opt/agenthub/tools`. | | Today's install | — | `~/.local/bin/claude`, `~/.codex/packages/…/codex`, `~/.local/bin/agy` inside a `0750` home: other users cannot reach them, and Codex's binary lives in its credentials dir. Hence the move. | | Tool config / login / transcripts | **No**, per account | `/srv/agenthub/users//.claude`, `.codex`, `.gemini`, `.cursor`… (`HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` point here). Sharing this dir would share every transcript. | | Account logins (Claude, Codex, …) | **Yes**, as a vault | Host account, `agenthub`-owned, `0700`. Injected into a pane at spawn (S5). | | AI Router profiles | **Yes** | Already per workspace (`router-apply.js`); the store moves to the vault dir. | | Hooks | **Yes**, one copy | Claude: `/etc/claude-code/managed-settings.json` (applies to every user). Codex: written into each account's `CODEX_HOME` at provisioning. | | Database | One SQLite, rows owned | `owner` on every user-data table (S2). | ## Phases | # | Scope | | --- | --- | | S0 | **Spike** (needs `sudo`): two test Linux users; tools under `/opt/agenthub/tools`; each tool started with a foreign `HOME`; verify per tool how a login can be handed in (below) and how its auto-update is turned off; measure what concurrent use of one OAuth login does. Output: a table in this file, go/no-go per tool. | | S1 | **Accounts and login**, still one Linux user. `users` table (id, name, role `admin|analyst|member`, password hash via `crypto.scrypt`, disabled), `auth_sessions` (httpOnly SameSite=Strict cookie), login page, every `/api` and `/ws` behind it except `/api/hooks` (local only, see S3). First run: create the admin from the CLI (`agenthub users add --admin`). | | S2 | **Ownership.** `owner` (user id) on `lanes`, `sessions`, `token_usage`, `events`, `dashboard_runs`, `lane_sources`, alerts, ai-tools prefs. A session's owner = its workspace's owner (phase-1 `lane_id`), else derived from its cwd under `/srv/agenthub/users//`. Every query of a member is scoped (one helper, not per route); WebSocket broadcasts filtered per connection. Workspace creation refuses a folder outside the account root. Admin/analyst bypass the scope; analyst is read-only. | | S3 | **Per-account Linux user.** Provisioning (`agenthub users add`): `useradd`, `/srv/agenthub/users/` `:agenthub 0750`, `HOME` there, per-tool dirs, Codex hooks. Spawning as the account: a small privileged helper (or a `sudoers` rule limited to `tmux` under group `agenthub-users`) — per-account tmux server; `pty-run.js`/`tmux.js`/`pty-attach.js` gain a target user. Hooks keep posting to localhost; the server trusts the owner it derives from the cwd/run, never one the hook names. Transcripts read through group `agenthub`. **Security review before merge.** | | S4 | **Shared tools.** Catalog detection also looks in `/opt/agenthub/tools/bin`; AI Tools install/update write there; per-tool auto-update off in each pane's env. | | S5 | **Login vault and switching.** A member picks, per run or as a default, a Claude profile / Codex profile / AI Router. At spawn the pane gets it: Claude a long-lived token (`claude setup-token` → `CLAUDE_CODE_OAUTH_TOKEN` through tmux `-e`), Cursor `CURSOR_API_KEY`, Codex `auth.json` copied into the account's `CODEX_HOME` (or an API key), others per S0. Today's `cc-profiles.js` switch rewrites ONE live credentials file — that becomes "copy into this account", never global. Codex profiles are new. Refresh-token rotation (two people on one OAuth login) is the open risk S0 measures. | | S6 | **Admin and analyst views.** User management; every page gets a user filter for admin/analyst; prompt analytics (UserPromptSubmit events, transcripts) and token/cost per user; admin may attach read-only to any pane. Every admin read of another user's content is audit-logged. | | S7 | **Hardening.** TLS (reverse proxy), bind address, login rate limit, cookie flags, `sameOriginGuard` kept, `/api/hooks` accepted from loopback only, backup of the vault. | ## Rules this plan will change in `CLAUDE.md` - Settings/`CLAUDE_HOME` become per account; the host account's `~/.claude` is no longer "the" Claude home. - `cc-profiles.js` "a switch changes the live account" → per account. - Every new route is owner-scoped; "a route never takes X from the request" rules gain "…nor an owner". - The bootstrap/systemd rules stay for the host account; members never run the bootstrap. ## Open questions for the user (before S3) 1. With Claude Enterprise each person has a seat — should members log in their own seat (cleaner audit, no rotation risk) and the vault serve only Codex, AI Routers and spares? 2. Does an admin's live view of a pane need to be announced to the member? 3. Retention of prompts for analytics — how long, and who may export them?