feat(server-s3): one Linux user per account (S3) — experimental
Server mode can now run each account as its own Linux user (ah-<name>) when `agenthub server install-system` has set S3 up. The dashboard reaches a member only through sudo -u / write-as / pane-launch, reads a member's files only through owner- and type-checked opens, and hooks are authenticated by peer uid. Local mode and server mode without S3 are unchanged. EXPERIMENTAL: tested with fake sudo and fake accounts only. It has not yet been verified on a real install with real member accounts, sudo or a real Codex login. Do not offer it to real users yet. - S3 core: provisioning, per-member tmux servers, the proxy key gateway, shared tools in /opt, dropped capabilities for children (P0–P5) - P4: plan and quota per member home - P6: ~/.claude.json seed; opt-in plugin baseline installed as the member; managed-settings spike script (not run) - P7: Codex account pool (`agenthub accounts add --tool codex`) - Roles merged with main's admin / analyst / developer: analysts write only their own rows, under S3 as well - Per-account prefs: one implementation (Chat UI + onboarding) - Security reviews F1–F49 (five rounds), each fix with a regression test: symlink/TOCTOU-safe member reads, no host writes in a member home, credentials and the data dir never served, owner and folder account must agree, pane tokens bound to the pane's uid, bounded sudo/tmux/git calls and member reads (no hang or crash from a member), async route rejections answer 500 instead of exiting - Docs: docs/SERVER-MODE.md (S3 model, accepted ceilings, operator runbook), docs/plans/server-mode/S3-PLAN.md, CLAUDE.md S3 rules Tests: server 2838/2841 (3 need 127.0.0.1:3000, held on this box); client 1162/1162; tsc clean; headers check passes. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+602
-29
@@ -15,12 +15,11 @@ Local mode does not change at all. The login code stays dormant until
|
||||
> **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.
|
||||
> Until [one Linux user per account](#one-linux-user-per-account) is installed
|
||||
> (`agenthub server install-system`), every terminal pane runs as the one Linux
|
||||
> account that runs the dashboard. An agent in a pane can then read every
|
||||
> user's folders, transcripts and the database straight from disk. Without it,
|
||||
> use server mode only with people you trust.
|
||||
|
||||
## Set it up
|
||||
|
||||
@@ -93,22 +92,25 @@ The login cookie gets `Secure` when the request came over HTTPS
|
||||
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
|
||||
`/api/settings`, `/updates`, `/cc-config`, `/codex-config`,
|
||||
`/git-credentials`, `/ai-tools`, `/import`, `/pricing`, `/budgets`,
|
||||
`/webhooks`, `/remote-sources`, `/reports`, `/pets`, `/users` and
|
||||
`/alerts/rules` (`ADMIN_WRITE_PREFIXES` in `server/lib/auth.js`), plus a write
|
||||
to a shared AI Proxy (see [AI Proxy profiles](#ai-proxy-profiles)). 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).
|
||||
rows), and under S3 an analyst's workspace runs as their own Linux account
|
||||
exactly like a developer's.
|
||||
|
||||
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
|
||||
no Data, Pricing 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; the AI proxy page shows them only their personal profiles' Edit. 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
|
||||
@@ -140,9 +142,38 @@ Workspaces made before server mode, or by the host account, have none.
|
||||
|
||||
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.
|
||||
prompt box uses), the AI tools' config files (`/api/ai-tools/<id>/config…`),
|
||||
git credentials, remote sources, webhooks, reports, metrics, imports, data
|
||||
export, alert rules and `/api/users`. Those return `403 EADMIN`, and Settings
|
||||
hides them for developers. The command and skill lists show a developer only their
|
||||
own workspaces' project items: `?cwd=` outside one of them answers
|
||||
`403 EFOLDER`, and with no `cwd` they list no project items at all.
|
||||
|
||||
### AI Proxy profiles
|
||||
|
||||
Each user may keep **personal** AI Proxy profiles beside the **shared** ones:
|
||||
|
||||
- A developer's or analyst's new profile is always personal. An admin's is shared unless
|
||||
they choose **Only me (personal)** in the form (`scope: "personal"`).
|
||||
- A personal profile is named `u<user id>_<slug>`. Only its owner lists,
|
||||
edits, deletes or tests it, and only the owner's workspaces can start a
|
||||
session on it. Anyone else, an admin included, gets `404`, the same answer
|
||||
as a profile that does not exist. A resume of a session that ran on someone
|
||||
else's personal profile goes back to native.
|
||||
- Only an admin writes a shared profile. Everyone can choose one.
|
||||
- **No pane is given a proxy's key, and no route returns one.**
|
||||
- A session's CLI talks to AgentHub's own loopback gateway,
|
||||
`http://127.0.0.1:<port>/api/proxy-gw/<profile>/v1/…`. Its key there is
|
||||
its pane's token (`ANTHROPIC_AUTH_TOKEN` for Claude, `AGENTHUB_PROXY_KEY`
|
||||
for Codex).
|
||||
- AgentHub swaps in the real key and streams the request to the profile's
|
||||
endpoint.
|
||||
- The gateway answers only on this machine, only while the pane lives, and
|
||||
only for the profile the pane was started on.
|
||||
- An agent can still spend through the profile while its pane runs, but it
|
||||
cannot carry the key away.
|
||||
- Claude account-pool tokens are not covered: they still reach the pane (see
|
||||
[Claude account pool](#claude-account-pool)).
|
||||
|
||||
### Keep developers' folders apart
|
||||
|
||||
@@ -152,10 +183,16 @@ 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
|
||||
`/srv/agenthub/users/<username>`; any other folder gets `403 EFOLDER`. The
|
||||
same root limits a developer's folder browser (`GET /api/lanes/browse`, which
|
||||
opens on their own folder) and `GET /api/lanes/branches`, both judged by real
|
||||
path, so a link cannot point them elsewhere. Under S3 the home itself is no
|
||||
workspace (its dot folders hold the member's logins); use a folder in it. 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.
|
||||
not limited. Without [one Linux user per account](#one-linux-user-per-account)
|
||||
you create each user's folder yourself, and this keeps workspaces apart, not
|
||||
agents. `install-system` sets this value to `/srv/agenthub/users`, and adding a
|
||||
user makes the folder (it is the Linux account's home).
|
||||
|
||||
## What the gate does
|
||||
|
||||
@@ -187,9 +224,17 @@ apart, not agents.
|
||||
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`.
|
||||
machine and only while that pane is alive, as header `x-agenthub-pane-token`
|
||||
(with one Linux user per account on: only from the Linux account that pane
|
||||
runs as, read from the kernel like a hook's sender).
|
||||
A workspace with no owner has no such user. Give old workspaces an owner with
|
||||
`agenthub users assign <workspace id> <user>`.
|
||||
`agenthub users assign <workspace id> <user>`. With one Linux user per
|
||||
account on, a workspace inside a member's home can be owned by that member
|
||||
only, and one outside the users root (its panes run as the host account) by
|
||||
an admin only: creating, assigning or starting one against that answers
|
||||
`409 EOWNERFOLDER` — by the user's role at that moment, and demoting or
|
||||
disabling a user ends their panes that run as the host. A pane's folder must
|
||||
be its workspace's account's too (`409 ECWDACCOUNT`).
|
||||
- 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
|
||||
@@ -262,9 +307,537 @@ How a new session picks its account (`server/lib/claude-pool.js`):
|
||||
- `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.
|
||||
> account their pane runs on (`echo $CLAUDE_CODE_OAUTH_TOKEN`). Without one
|
||||
> Linux user per account they can also read every file of the pool. With it,
|
||||
> they still see their own pane's token (a member's pane gets it through its
|
||||
> launch file instead of tmux `-e`), but no other account's process or file.
|
||||
> The AI Proxy gateway does not cover pool tokens.
|
||||
|
||||
## Codex account pool
|
||||
|
||||
The same for Codex (`server/lib/codex-pool.js`), with two kinds of account:
|
||||
|
||||
```bash
|
||||
# An OpenAI API key (typed hidden, or piped on stdin)
|
||||
agenthub accounts add team-key --tool codex --api-key
|
||||
# A ChatGPT account: sign in in a scratch home, then hand that home over
|
||||
CODEX_HOME=$(mktemp -d) codex login
|
||||
agenthub accounts add team-chatgpt --tool codex --from /tmp/tmp.XXXX
|
||||
agenthub accounts list --tool codex
|
||||
agenthub accounts remove team-key --tool codex
|
||||
```
|
||||
|
||||
- **API key**: any number of panes at once. The pane gets the key through
|
||||
tmux `-e AGENTHUB_POOL_KEY=…`, and Codex is pointed at it with
|
||||
`-c model_providers.agenthub_pool={…env_key="AGENTHUB_POOL_KEY"…}` — the
|
||||
argv names the variable, never the key.
|
||||
- **ChatGPT**: Codex reads this login only from `$CODEX_HOME/auth.json`, so it
|
||||
is used only for a pane that runs as its developer (one Linux user per
|
||||
account): AgentHub writes the account's `auth.json` into that developer's
|
||||
`~/.codex` as them (0600) just before the pane starts.
|
||||
- A host pane never gets one: its `CODEX_HOME` holds the host's own login.
|
||||
- A developer who signed Codex in themselves keeps their own login.
|
||||
- While a pane of theirs runs on a pool account, their next pane uses the
|
||||
same account (one `auth.json` per home).
|
||||
- Codex rotates refresh tokens. Every 10 s AgentHub copies a developer's
|
||||
`auth.json` back into the pool when its `last_refresh` is newer, so a new
|
||||
pane starts from the newest login — only from a home AgentHub placed that
|
||||
account in, never with a `last_refresh` in the future, and only when its
|
||||
`id_token` names the same account and email. The `id_token` signature is
|
||||
not checked: a developer the account was placed with can still spoil it
|
||||
for others until a real refresh from another home wins it back. Several panes may share one account
|
||||
(checked on codex 0.161: an older refresh token still refreshes).
|
||||
- Ranking is the Claude pool's: quota ≥ 90% last, then fewest live Codex
|
||||
panes, then $ in 5 h / 7 d. The quota is the newest `rate_limits` in the
|
||||
rollouts of the sessions that account's runs started (no network), each
|
||||
read only from the `CODEX_HOME` of the account its workspace runs as, and
|
||||
never one dated in the future.
|
||||
- A session on an AI Proxy keeps the proxy and does not use the pool.
|
||||
|
||||
> **Ceiling:** a pane can read its own account's key (`$AGENTHUB_POOL_KEY`) or
|
||||
> `auth.json`, like the Claude pool's token.
|
||||
|
||||
## One Linux user per account
|
||||
|
||||
Every pane of a user, an agent's shell included, runs as that user's own Linux
|
||||
account (`ah-<username>`). An agent in Alice's terminal then cannot read Bob's
|
||||
folders, transcripts or the dashboard's database: the OS stops it, not just the
|
||||
UI. This is phase S3 of `docs/plans/server-mode/S3-PLAN.md`. It is ON when
|
||||
server mode is on AND `/etc/agenthub/server.conf` exists
|
||||
(`server/lib/linux-accounts.js`, `s3Enabled`); without that file everything
|
||||
below is a no-op and server mode behaves as described above. To install it,
|
||||
follow the [operator runbook](#operator-runbook).
|
||||
|
||||
> **Status (2026-10-09):** built and checked on a real machine: the host
|
||||
> setup, the accounts, panes, git and file saves as the member, hooks checked
|
||||
> by account, reads across homes, shared tools in `/opt`, and the fixes from
|
||||
> the 2026-10-08 security review (P8 F1–F11). Built and tested with fake
|
||||
> accounts only: per-member quota (P4), the opt-in plugin baseline (P6) and
|
||||
> the Codex account pool (P7). The second review (2026-10-09, P2–P7) is fixed
|
||||
> (see [What the second review closed](#what-the-second-review-closed)).
|
||||
>
|
||||
> Not done yet (`docs/plans/server-mode/S3-PLAN.md`):
|
||||
> - the admin's plugin baseline through managed settings (P6 spike, not run):
|
||||
> the opt-in fallback ([Plugin baseline](#plugin-baseline-opt-in)) is
|
||||
> built; otherwise a member installs plugins in their own terminal
|
||||
> (`/plugin`);
|
||||
> - Codex and Antigravity hooks for members: provisioning installs none, so a
|
||||
> member's Codex and Antigravity sessions are tracked from their journals
|
||||
> (the pollers read every member's home), not from hooks;
|
||||
> - per-member quota, the plugin baseline and the Codex account pool (P4, P6,
|
||||
> P7) are not yet tried with real member accounts or a real Codex login.
|
||||
|
||||
### The model
|
||||
|
||||
- **Identity.** AgentHub user `alice` is Linux user `ah-alice`, home
|
||||
`/srv/agenthub/users/alice` (`0750`, owner `ah-alice`, group `agenthub`).
|
||||
The name must be lower case: a letter, then up to 28 letters, digits, `_` or
|
||||
`-` (`LINUX_NAME_RE`). While S3 is on, a user with any other name cannot be
|
||||
created (`400 EBADUSER`). Admins and analysts get a Linux account too. A
|
||||
workspace's folder decides who runs it (`server/lib/run-as.js`,
|
||||
`accountForPath`): a folder under `/srv/agenthub/users/<name>/` runs as
|
||||
`ah-<name>`; any other folder runs as the host account, as before (a
|
||||
workspace made before S3 outside the users root, for example). A folder under the users root whose
|
||||
user is unknown, disabled or not provisioned is an error (`409
|
||||
EMEMBERGONE`), never "run it as the host".
|
||||
- **The dashboard.** A SYSTEM unit, `agenthub.service`, runs it as the host
|
||||
account with `CAP_DAC_READ_SEARCH` (it reads every member's tool files —
|
||||
Claude writes transcripts `0600` — but writes none of them). Every program
|
||||
it starts goes through `setpriv --inh-caps=-all --ambient-caps=-all` first
|
||||
(`server/lib/drop-caps.js`), so host panes, git hooks and npm do not inherit
|
||||
the capability.
|
||||
- **Run-as through sudo.** Everything done AS a member is
|
||||
`sudo -n -u ah-<name> <command>`, and the sudoers file allows exactly
|
||||
`tmux`, `git`, `gh` (by the absolute paths in `server.conf`),
|
||||
`/opt/agenthub/libexec/write-as` and `/opt/agenthub/tools/bin/claude` (the
|
||||
plugin baseline) that way — never root. As root, the host may
|
||||
run only `/opt/agenthub/sbin/agenthub-provision` (create, disable or enable
|
||||
a member) and `systemctl start|stop|restart|enable|disable
|
||||
agenthub.service`. Members are never in sudoers.
|
||||
- **sudo logs every command line**, so nothing secret or private crosses it:
|
||||
a pane's environment (its pane token, a pool token) and its command (the
|
||||
prompt) go into a launch file that `write-as` writes as the member (`0600`,
|
||||
from stdin) and `/opt/agenthub/libexec/pane-launch` reads, deletes and
|
||||
execs. File contents go to `write-as` on stdin.
|
||||
- **hidepid is required.** `/proc` is mounted `hidepid=invisible`, with group
|
||||
`agenthub-proc` (the host account, `polkitd`) exempt. Without it any account
|
||||
could read another's command lines. `install-system` stops if the remount
|
||||
fails.
|
||||
- **Hooks are authenticated by the sender's uid**, read from the kernel's
|
||||
table of loopback connections (`/proc/net/tcp*`, `server/lib/hook-auth.js`),
|
||||
not from anything the hook says.
|
||||
- **The AI Proxy gateway.** A pane on an AI Proxy profile gets
|
||||
`http://127.0.0.1:<port>/api/proxy-gw/<profile>` and its own pane token as
|
||||
the key; AgentHub swaps in the real key upstream
|
||||
(`server/lib/proxy-gateway.js`, see [AI Proxy profiles](#ai-proxy-profiles)).
|
||||
A Claude pane gets the token in a per-run `--settings` file, so a user or
|
||||
project `env.ANTHROPIC_*` cannot override it.
|
||||
- **Shared tools.** Members run every AI tool, node and AgentHub's own scripts
|
||||
from `/opt/agenthub` (`server/lib/shared-tools.js`), first on their pane's
|
||||
`PATH`, with self-update off (`DISABLE_AUTOUPDATER=1`).
|
||||
|
||||
### What install-system sets up
|
||||
|
||||
The pieces, and what each is for (the install steps are in the
|
||||
[runbook](#install)):
|
||||
|
||||
| Piece | What it is |
|
||||
| --- | --- |
|
||||
| `ah-<name>` | Made by `agenthub-provision`, not by the installer. One Linux user per AgentHub user, home `/srv/agenthub/users/<name>` (`0750`, group `agenthub`): the member owns it, the dashboard reads it, nobody else gets in. `.agenthub/{tmux,handoff}` (0700) is made by the member: their tmux socket, and the files the dashboard writes for their panes (as them) |
|
||||
| groups | `agenthub` (reads member homes: the host account), `agenthub-members` (every `ah-*` account), `agenthub-proc` (sees every process: the host account, `polkitd`) |
|
||||
| `agenthub.service` | A SYSTEM unit, `User=<host>`, with `SupplementaryGroups=agenthub agenthub-proc` and `AmbientCapabilities=CAP_DAC_READ_SEARCH`: the dashboard can read every member's tool files (Claude writes transcripts `0600`), but cannot write them. Every program it starts has the capability removed first (`setpriv`), so host panes, git and npm do not inherit it. It reads node and the plugin root from `<data dir>/runtime/agenthub.system.env`, a file the host owns, so a plugin update repoints it without root |
|
||||
| `/etc/sudoers.d/agenthub` | The host may run only `tmux`, `git`, `gh` (by the absolute paths recorded in `/etc/agenthub/server.conf`), `/opt/agenthub/libexec/write-as` and `/opt/agenthub/tools/bin/claude` (the [plugin baseline](#plugin-baseline-opt-in)) AS a member, plus, as root, only `/opt/agenthub/sbin/agenthub-provision` and `systemctl start/stop/restart/enable/disable agenthub.service`. Members are never in sudoers |
|
||||
| `/etc/agenthub/server.conf` | Root's, `0644`: the host account and uid, the users root, the port, and the `tmux`/`git`/`gh` paths sudoers lists. Its presence turns S3 on |
|
||||
| `write-as`, `pane-launch` | `/opt/agenthub/libexec/`, root's. `write-as <path> [append\|replace\|private\|mkdir\|remove]` runs AS a member and does the one change, content on stdin. `pane-launch <file>` starts a member's pane from its launch file |
|
||||
| `agenthub-provision` | The one root command besides `systemctl` on this unit: `create`, `disable` or `enable` `ah-<name>`. It refuses an account that exists outside `agenthub-members` and never deletes anything |
|
||||
| `/etc/claude-code/managed-settings.json` | Members' Claude Code hooks, through `/opt/agenthub/libexec/member-hook`, which does nothing for the host account (its plugin reports). A foreign file there is left alone |
|
||||
| `hidepid=invisible` | `/proc` (now and in `/etc/fstab`): an account sees only its own processes, so no member can read another's command line. A token passed with `tmux -e` is in one |
|
||||
| `/opt/agenthub/{tools,app}` | Owned by the host account: the dashboard updates them, every account runs them. `tools/bin` holds node and every installed AI tool; this install's hook handler, CLI, MCP server and statusline go in `app`, with the `agenthub` launcher in `tools/bin`. The AI tools are published there after each install or update from the AI Tools page, at server start, and by `agenthub server tools`. A publish copies a native binary, an npm package's folder, or a script's own version folder. A tool that is not published yet answers a member's start with `409 ENOTSHARED` |
|
||||
|
||||
What runs as the member, and how:
|
||||
|
||||
- **Panes.**
|
||||
- Each member has their own tmux server (`~/.agenthub/tmux/sock`). Every tmux command on it, attaching a browser terminal included, runs as `sudo -u ah-<name> tmux …`. The dashboard never connects to it as itself: the member controls that socket.
|
||||
- sudo logs every command line it runs, so a pane's environment (its tokens) and its command (the prompt) are not on it. `write-as` writes them to a launch file (`0600`, from stdin), and `/opt/agenthub/libexec/pane-launch` reads it, deletes it and starts the command. A prompt pasted later goes in on stdin.
|
||||
- A pane gets the member's `HOME`, `CODEX_HOME` and `PATH` (`/opt/agenthub/tools/bin` first), with `DISABLE_AUTOUPDATER=1` and none of the host's desktop variables.
|
||||
- **Git, `gh` and file saves.**
|
||||
- In a member's folder, git and `gh` run as `sudo -n -u ah-<name> git …`, so git's own ownership check passes and every file stays the member's.
|
||||
- The editor and `.gitignore` saves go through `/opt/agenthub/libexec/write-as`.
|
||||
- AgentHub's stored git logins are never handed to a member's git, and a password typed in the dashboard is refused (`400 EMEMBERCRED`): the member signs in to their git host from their own terminal.
|
||||
- **Files a pane reads.** These are written into `~/.agenthub/handoff/` AS the member (`write-as`). The dashboard never writes, changes or deletes anything in a member's home itself: the member could have swapped the name for a link. The files are:
|
||||
- the workspace prompt and its context files;
|
||||
- the AI Proxy settings: no key, only that pane's own token for the gateway;
|
||||
- files dropped into the prompt box;
|
||||
- a pane's launch file.
|
||||
|
||||
Codex gets its proxy as `-c` overrides, never a file in the member's `CODEX_HOME`.
|
||||
- **Reading a member's files.** A transcript, a Codex, Antigravity or Cursor journal, or a file in the workspace is read only when the opened file is a regular file the member owns. A link to another account's file is refused, and so is a FIFO. A journal speaks only for its own account: its session's folder must be inside that home. A member's Antigravity conversation (SQLite, which opens its `-journal`/`-wal` by name) is read from a private copy; the walk that turns a Cursor project slug back into a folder follows no link and gives up after 500 folders.
|
||||
- **Hooks.** A hook is matched to its sender's Linux account, read from the kernel's table of loopback connections.
|
||||
- A member's Claude Code reports through `/etc/claude-code/managed-settings.json` → `/opt/agenthub/libexec/member-hook` → `/opt/agenthub/app/scripts/hook-handler.js`.
|
||||
- The host's hooks are accepted as before.
|
||||
- A member's hook may report only work inside their own home: another account's folder, session or pane is `403 EFOREIGN`, and a transcript path outside the home is dropped.
|
||||
- Any other account is `403 EPEER`.
|
||||
- **Liveness.** A member's session counts as alive while the pane holding it lives, because the dashboard cannot read another account's process directory. A session the member started in their own shell ends once it has been idle for the idle timeout.
|
||||
|
||||
Not offered for a workspace in a member's folder (`403 EMEMBERLANE`): the
|
||||
Add-workspace **Worktree** mode and the runtime layer (`lanes up/down/hook`,
|
||||
profiles, proof, lane MCP). Both run as the dashboard's own account. Use
|
||||
**Repo** mode and the prompt box's **New worktree** instead.
|
||||
|
||||
**First sign-in.** Every account gets AgentHub's own setup once, in the
|
||||
browser: the default agent and permission mode, the look (System, Dark or
|
||||
Light, a theme for each, language, terminal font), the pet and notifications,
|
||||
then a guided tour to the first workspace and session. The choices are stored
|
||||
per account (`/api/me/prefs`), so they follow the user to any browser; an
|
||||
account that already has a workspace skips it, and Settings → Appearance →
|
||||
**Run setup again** brings it back.
|
||||
|
||||
A member's own Claude Code skips its first-run questions: `agenthub-provision`
|
||||
marks its onboarding done and trusts the member's home (which covers every
|
||||
folder under it), and the dark/light mode picked in AgentHub becomes Claude's
|
||||
theme. Accepting Bypass Permissions mode stays the member's own choice, made
|
||||
in their terminal.
|
||||
|
||||
### Plugin baseline (opt-in)
|
||||
|
||||
Write `/etc/agenthub/plugin-baseline.json` (as
|
||||
root) to give every NEW member your plugins:
|
||||
|
||||
```json
|
||||
{ "marketplaces": ["owner/repo"], "plugins": ["name@marketplace"] }
|
||||
```
|
||||
|
||||
After a user is created, AgentHub runs, as that member and in the
|
||||
background, `claude plugin marketplace add <source>` for each marketplace and
|
||||
then `claude plugin install --scope user <plugin>` for each plugin; each
|
||||
result is logged as `[plugin-baseline]`. `agenthub users provision <name>`
|
||||
(or `--all`) runs it again in the foreground. No file, nothing runs. `-y` is
|
||||
never passed, so a plugin that installs by running a marketplace-declared
|
||||
command fails here; the member installs it from their own terminal. Members
|
||||
add their own plugins with `/plugin` as usual.
|
||||
|
||||
### What the second review closed
|
||||
|
||||
The 2026-10-09 review looked for ways a member can point the dashboard's own
|
||||
rights (`CAP_DAC_READ_SEARCH`, the host account, sudo as any member) at files
|
||||
that are not theirs. Each case below is refused, with a test in
|
||||
`server/__tests__/server-s3-review2.test.js`:
|
||||
|
||||
- **A workspace folder swapped for a link.** The file view, image view, blame
|
||||
and the file editor resolve the workspace's real path and refuse it
|
||||
(`403 EFOREIGN`) when it lands in another account's files — another
|
||||
member's folder, the host's home, `/etc`. The account a read or save runs as
|
||||
is never taken from where a link points.
|
||||
- **A member's git dir naming a path outside their home.** The `info/exclude`
|
||||
line AgentHub adds for a new worktree or `.mcp.json` is written only when the
|
||||
repo's real git dir is the same account's; a FIFO there cannot hang the
|
||||
dashboard.
|
||||
- **Browsing and branch lists.** A developer's `GET /api/lanes/browse` and
|
||||
`GET /api/lanes/branches` stay inside their own folder (`403 EFOLDER`), and
|
||||
so does a workspace's extra folder (`POST /api/lanes/:id/sources/folder`,
|
||||
refused before the path is looked at, so the answer says nothing about
|
||||
another member's files).
|
||||
- **The top of a member's home.** Its dot folders (`.claude`, `.codex`,
|
||||
`.config`, `.ssh`, `.agenthub`…) hold logins and pane tokens: no workspace
|
||||
file view serves them to anyone (`403 ESECRET`), and the home itself is no
|
||||
workspace.
|
||||
- **Transcripts by session id.** A scan by id looks only in the session
|
||||
owner's home, so a file another member plants under that id is never shown.
|
||||
- **Codex journals.** A rollout journal sets the status only of a lane in its
|
||||
own account's home, and the newest-40 window is per home.
|
||||
- **A disabled member's sessions.** Opening one reads from their home instead
|
||||
of crashing the server, and the liveness reaper skips them instead of
|
||||
stopping.
|
||||
- **Host terminals.** A browser terminal's `tmux attach` (node-pty, which
|
||||
forks outside `child_process`) is started through `setpriv` too, so it does
|
||||
not keep `CAP_DAC_READ_SEARCH`.
|
||||
- **AI Proxy edits.** Editing a profile rewrites a running gateway pane's own
|
||||
settings file (with its own token), and deletes the file of a pane that has
|
||||
ended.
|
||||
|
||||
### What review round 5 closed
|
||||
|
||||
The fifth review (2026-10-09) looked at the dashboard's ONE thread and at who
|
||||
a pane really is. Each case below is refused or bounded, with a test in
|
||||
`server/__tests__/server-s3-review5.test.js`:
|
||||
|
||||
- **AgentHub's data dir is never a config file.** `GET /api/cc-config/file`
|
||||
refuses anything in it — the database, the push keys, and each gateway
|
||||
pane's settings file, which holds that pane's live token.
|
||||
- **A member's stopped or flooding tmux server.** Every call on it has a 3 s
|
||||
limit and a 1 MiB answer cap; one that hit either is not asked again for a
|
||||
minute. Meanwhile only that member's panes count as unknown (kept, not
|
||||
ended); everyone else's are still checked. Every other synchronous call
|
||||
that runs as a member (git in their folder, `write-as`) stops after 5 s.
|
||||
- **A member's huge or sparse files.** Transcripts and journals are read line
|
||||
by line with a 64 MiB cap per line (a 512 MB line used to crash the server);
|
||||
a whole member file is read only up to a cap (1 MiB for a login or a
|
||||
settings file, 16 MiB for an index or a workflow journal, 256 MiB for a
|
||||
SQLite copy, 64 MiB otherwise); a member transcript over 256 MiB is neither
|
||||
parsed nor snapshotted, and the snapshot keeps a sparse file's holes.
|
||||
- **A request that makes a route throw** answers 500 instead of stopping the
|
||||
server (a member folder of a gone user crashed `POST /api/lanes/worktree`).
|
||||
- **Who a pane runs as.** A non-admin owns no workspace outside their home
|
||||
(above), a pane's folder is its workspace's account's, and a pane token
|
||||
works only from that account's uid.
|
||||
- **A path an analyst names.** Browsing, branch lists and the `@` file search
|
||||
stay in their own folder or workspaces, like a developer's.
|
||||
- **Drops** are saved where the pane that reads them can read them: as the
|
||||
workspace's account (the client names the workspace), not the dropper's.
|
||||
- **The Codex pool and ChatGPT Teams.** A login is the pool's only when
|
||||
AgentHub placed it in that home AND it is the same person (email): every
|
||||
seat of a Team shares one `account_id`, so a member's own seat is theirs.
|
||||
|
||||
### Accepted ceilings
|
||||
|
||||
- The dashboard process can read any file, `/etc/shadow` included. `sudo`
|
||||
needs that file, so the unit cannot hide it (`InaccessiblePaths` hides only
|
||||
`/root`). Treat the host account as privileged, and keep it out of the
|
||||
`sudo` group on a real server: there the sudoers file above is all it may
|
||||
do.
|
||||
- Anything that runs as the host account can run anything as any member: the
|
||||
sudoers file lets it start `tmux` as them. A host compromise is every
|
||||
member's compromise.
|
||||
- `install-system` installs and runs code from the host account's install, as
|
||||
root. Re-run it only from a tree you trust.
|
||||
- sudo's log (readable by root and `adm`) shows members' commit messages,
|
||||
branch names, pull-request titles and file paths, never a token or a prompt.
|
||||
- A member may point a personal AI Proxy at any URL, so the dashboard will
|
||||
POST to it. They have a shell on the machine anyway.
|
||||
- The gateway forwards any path under `/v1/` to the profile's upstream, and an
|
||||
agent can spend through a profile while its pane lives.
|
||||
- A Claude pool token is in its pane's environment: the member of that pane
|
||||
can read it (see [Claude account pool](#claude-account-pool)). The same goes
|
||||
for a Codex pool API key, and for a Codex ChatGPT login written into the
|
||||
member's `~/.codex/auth.json` ([Codex account pool](#codex-account-pool)).
|
||||
- The Codex pool's quota reading comes from rollout journals, which their
|
||||
member writes: a member can forge the reading of their OWN session (say a
|
||||
pool account is full, or empty) and so steer the balancer for everyone
|
||||
until a newer reading wins. They cannot speak for another account's run:
|
||||
a run's rollout is read only from its own account's `CODEX_HOME`, and never
|
||||
one dated in the future.
|
||||
- A member session started in the member's own shell, outside a dashboard
|
||||
pane, counts as alive only until its idle timeout: the dashboard cannot read
|
||||
another account's `/proc/<pid>`.
|
||||
|
||||
## Operator runbook
|
||||
|
||||
Every command below runs as the **host account** (the one that runs the
|
||||
dashboard) unless it says `sudo`. `agenthub server install-system` calls sudo
|
||||
itself, once.
|
||||
|
||||
### What a new machine needs before `install-system`
|
||||
|
||||
`install-system` does not do any of this:
|
||||
|
||||
1. A Linux machine with systemd, kernel ≥ 5.8 (for `hidepid=invisible`),
|
||||
`sudo` with `visudo`, the shadow tools (`useradd`, `usermod`, `groupadd`),
|
||||
util-linux (`runuser`, `setpriv` at `/usr/bin/setpriv`, `mount`), `pkill`,
|
||||
`sha256sum`.
|
||||
2. A host account that is not root, with sudo rights for this one run (take
|
||||
them away again afterwards, see step 9 below).
|
||||
3. Node (the version AgentHub needs), `tmux`, `git` and, if you want it, `gh`
|
||||
on that account's `PATH`. Their absolute paths are recorded once, in
|
||||
`/etc/agenthub/server.conf` and the sudoers file.
|
||||
4. AgentHub installed for the host account, as the plugin or a checkout
|
||||
(`npm run setup && npm run build`), and running once
|
||||
(`agenthub start`).
|
||||
5. Server mode on, with the first admin:
|
||||
`agenthub server setup --hostname agenthub.example.com --admin boss`
|
||||
(see [Set it up](#set-it-up)).
|
||||
6. The AI tools members will use, installed for the host account (the AI
|
||||
Tools page or by hand).
|
||||
7. The HTTPS reverse proxy (see [Put HTTPS in front of it](#put-https-in-front-of-it)).
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
agenthub server install-system --dry-run # print the one sudo command it will run
|
||||
agenthub server install-system # install, then switch to the system unit
|
||||
agenthub users provision --all # Linux accounts for users made earlier (the admin from setup)
|
||||
agenthub server tools # publish the AI tools to /opt now (the server also does it at start)
|
||||
```
|
||||
|
||||
What `agenthub server install-system` does, in order (`installSystem` in
|
||||
`bin/agenthub.js`, root half `scripts/server/install-system.sh`; running it
|
||||
again changes nothing that is already in place):
|
||||
|
||||
1. Refuses to run as root. Resolves, as the host account: its name, node, this
|
||||
install's root, the data dir, the port (`DASHBOARD_PORT`, else 4820) and
|
||||
the paths of `tmux`, `git` and `gh`.
|
||||
2. As root (`sudo bash install-system.sh …`), after checking each value:
|
||||
1. creates groups `agenthub`, `agenthub-members`, `agenthub-proc`; adds the
|
||||
host account to `agenthub` and `agenthub-proc`, and `polkitd` (if it
|
||||
exists) to `agenthub-proc`;
|
||||
2. creates `/srv/agenthub/users` (`0751` root), `/opt/agenthub/{sbin,libexec}`
|
||||
and `/etc/agenthub` (root), `/opt/agenthub/{tools,tools/bin,app}` (the
|
||||
host account's);
|
||||
3. writes `/etc/agenthub/server.conf` (`HOST_USER`, `HOST_UID`,
|
||||
`USERS_ROOT`, `PORT`, `TMUX_BIN`, `GIT_BIN`, `GH_BIN`);
|
||||
4. installs `agenthub-provision`, `write-as` and `pane-launch` from
|
||||
`scripts/server/`;
|
||||
5. copies node to `/opt/agenthub/tools/node/<version>` (linked from
|
||||
`tools/bin/node`) and this install's `scripts/` and `server/lib/` to
|
||||
`/opt/agenthub/app`;
|
||||
6. writes `/opt/agenthub/libexec/member-hook` (the port is baked in);
|
||||
7. writes `/etc/sudoers.d/agenthub`, only after `visudo -c` accepts it;
|
||||
8. writes `/etc/claude-code/managed-settings.json` (the member hooks) unless
|
||||
a file that is not AgentHub's is already there;
|
||||
9. `chmod o-rwx` the host's `~/.claude`, and warns when the host's home is
|
||||
open to other accounts;
|
||||
10. adds the `hidepid=invisible,gid=agenthub-proc` line to `/etc/fstab`
|
||||
(backup: `/etc/fstab.agenthub-bak`) and remounts `/proc` with it;
|
||||
11. writes `/etc/systemd/system/agenthub.service` and runs
|
||||
`systemctl daemon-reload`. It does not start it.
|
||||
3. As the host account again: writes `<data dir>/runtime/agenthub.system.env`
|
||||
(node, plugin root, port, client dist), sets `AGENTHUB_USERS_ROOT` in
|
||||
AgentHub's settings file, and disables the `systemd --user` unit (its tmux
|
||||
panes stay).
|
||||
4. Stops a dashboard still running without the system unit, then
|
||||
`sudo -n systemctl enable` and `start --no-block agenthub.service`, and
|
||||
fails unless the unit is active 3 seconds later.
|
||||
|
||||
What a new machine still needs by hand after it:
|
||||
|
||||
1. `agenthub users provision --all`: users created before S3 was on (the
|
||||
admin from `server setup` is one) have no Linux account yet.
|
||||
2. Log out and back in once, so your own shell joins `agenthub` and
|
||||
`agenthub-proc` (the unit gets them on its own).
|
||||
3. `chmod o-rwx ~` for the host account if the installer warned: the database
|
||||
lives under it.
|
||||
4. If the installer left a foreign `/etc/claude-code/managed-settings.json`
|
||||
alone, add the member hooks to it yourself: for each of `PreToolUse`,
|
||||
`PostToolUse` (both with `"matcher": "*"`), `Stop`, `SubagentStop`,
|
||||
`Notification`, `SessionStart`, `SessionEnd` and `UserPromptSubmit`, a
|
||||
`command` hook `/opt/agenthub/libexec/member-hook <Event>` with
|
||||
`"timeout": 5`.
|
||||
5. Any other daemon that must see every process (monitoring agents, for
|
||||
example) needs group `agenthub-proc`: `sudo usermod -aG agenthub-proc <it>`.
|
||||
6. A tool `agenthub server tools` reports as `ESHAREDLAYOUT` (a script in a
|
||||
shared folder) must be installed under `/opt/agenthub/tools` by hand.
|
||||
7. Remove the host account from the `sudo` group (`sudo deluser <host> sudo`
|
||||
on Debian/Ubuntu). Re-running `install-system` later needs it back for
|
||||
that run.
|
||||
8. Pool accounts, if you use a pool: `agenthub accounts add <name>` (Claude)
|
||||
or `agenthub accounts add <name> --tool codex --api-key|--from <dir>`
|
||||
([Codex account pool](#codex-account-pool)).
|
||||
9. Optional: `/etc/agenthub/plugin-baseline.json` for new members' plugins
|
||||
([Plugin baseline](#plugin-baseline-opt-in)).
|
||||
|
||||
### Add, disable and enable a user
|
||||
|
||||
```bash
|
||||
agenthub users add alice [--role admin|analyst|developer] # or the Users page
|
||||
agenthub users disable alice
|
||||
agenthub users enable alice
|
||||
agenthub users provision alice # the Linux account only (a user made before S3, or after a failed create)
|
||||
```
|
||||
|
||||
- **Add** creates the Linux account FIRST (`agenthub-provision create`):
|
||||
`useradd ah-alice` with home `/srv/agenthub/users/alice`, group
|
||||
`agenthub-members`; the home is set to `ah-alice:agenthub 0750`; as the
|
||||
member it makes `~/.agenthub/{tmux,handoff}` (`0700`) and marks Claude
|
||||
Code's onboarding done and the home trusted in `~/.claude.json`. If any of
|
||||
that fails, no AgentHub user is made (`502 EPROVISION`, with the helper's own
|
||||
words). An account `ah-alice` that already exists outside
|
||||
`agenthub-members`, or an existing folder `/srv/agenthub/users/alice` with no
|
||||
account, is refused. With a [plugin baseline](#plugin-baseline-opt-in)
|
||||
file in place, its plugins are then installed as the member in the
|
||||
background.
|
||||
- **Disable** disables the AgentHub user and ends their logins FIRST, then
|
||||
locks the Linux account (`usermod --lock --expiredate 1`) and kills every
|
||||
process of `ah-alice` (`pkill -KILL -u`: their tmux server and every pane).
|
||||
A failed lock still answers `502 EPROVISION`, but the logins are already
|
||||
gone. A user with no Linux account (made before S3, or a name that does not
|
||||
fit) is disabled without any sudo. Their files stay.
|
||||
- **Enable** clears the expiry date (when there is a Linux account) and
|
||||
re-enables the AgentHub user.
|
||||
- **Delete**: AgentHub never deletes a user, an account or a home. To remove
|
||||
one for good, disable it, then by hand: `sudo userdel ah-alice`, and move or
|
||||
delete `/srv/agenthub/users/alice` yourself. The AgentHub user row stays.
|
||||
- **A name that does not fit** (upper case, a dot): `provision` refuses it
|
||||
(`provision --all` reports it and goes on with the next user, exit code 1).
|
||||
Create a new user with a fitting name, move the old workspaces' folders into
|
||||
their home (`/srv/agenthub/users/<name>/…`, owned by `ah-<name>`) and add
|
||||
them there again. `agenthub users assign <workspace id> <name>` gives a
|
||||
workspace only to the member whose home holds it, or — for a folder outside
|
||||
the users root, whose panes run as the host — to an admin; anything else
|
||||
answers `EOWNERFOLDER`.
|
||||
|
||||
### Upgrade
|
||||
|
||||
- Update AgentHub as usual (Settings → Updates, `/agenthub-update`). The
|
||||
bootstrap repoints the unit by rewriting
|
||||
`<data dir>/runtime/agenthub.system.env` (a host-owned file, no root) and
|
||||
restarts it with `sudo -n systemctl restart --no-block agenthub.service`.
|
||||
Panes survive (`KillMode=process`).
|
||||
- At start, the server republishes its own scripts to `/opt/agenthub/app`
|
||||
and every installed AI tool to `/opt/agenthub/tools`, in the background. An
|
||||
install or update from the AI Tools page publishes that tool right after.
|
||||
- An update never touches a root-owned file. **Re-run `agenthub server
|
||||
install-system`** when a release changes anything under `scripts/server/`
|
||||
(provision helper, `write-as`, `pane-launch`, the member hook, sudoers, the
|
||||
unit; an install from before the plugin baseline needs it once for the
|
||||
`claude` sudoers entry), when the port changes (the member hook has it baked in), and when
|
||||
`tmux`, `git` or `gh` moves to another path (sudoers allows only the
|
||||
recorded ones).
|
||||
- Keep every other AgentHub server off the same database (a dev server on
|
||||
another port): an older build cannot see members' tmux servers and marks
|
||||
their runs abandoned.
|
||||
|
||||
### Undo and uninstall
|
||||
|
||||
```bash
|
||||
agenthub server install-system --uninstall # back to the systemd --user unit
|
||||
```
|
||||
|
||||
As root it stops, disables and deletes the system unit (only if it is
|
||||
AgentHub's), deletes the sudoers file, the managed settings (only if
|
||||
unchanged since it wrote them), the `/etc/fstab` line (backup:
|
||||
`/etc/fstab.agenthub-bak`; `/proc` is remounted `hidepid=off`),
|
||||
`/opt/agenthub/{sbin,libexec}` and `server.conf`. Then, as the host account,
|
||||
it repoints the `systemd --user` unit at this install and runs `systemctl
|
||||
--user enable --now agenthub`.
|
||||
|
||||
It keeps `/srv/agenthub/users` and every home, the `ah-*` accounts, the groups
|
||||
and `/opt/agenthub/{tools,app}`. It does not stop members' running tmux
|
||||
servers: disable those users first if they should end. With `server.conf`
|
||||
gone, S3 is off and server mode works as without it.
|
||||
|
||||
To remove everything:
|
||||
|
||||
1. `agenthub server install-system --uninstall`
|
||||
2. `agenthub server off`, then `agenthub uninstall --yes` (the Uninstall
|
||||
button refuses in server mode, `ESERVERMODE`).
|
||||
3. By hand, as root, only if you want them gone: `userdel` each `ah-*`
|
||||
account, `groupdel agenthub agenthub-members agenthub-proc`, and delete
|
||||
`/srv/agenthub`, `/opt/agenthub` and `/etc/agenthub`. These hold members'
|
||||
work: AgentHub never deletes them.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom | Cause, and what to do |
|
||||
| --- | --- |
|
||||
| `install-system: generated sudoers did not validate` | Nothing was installed past that step. Check the tool paths it printed (`--dry-run`). |
|
||||
| `install-system: …/managed-settings.json exists and is not ours` | Members' Claude hooks are not installed; add them by hand (above). |
|
||||
| `install-system` stops at the `/proc` remount | The kernel does not support `hidepid=invisible` (< 5.8). The `/etc/fstab` line is already added: remove it or restore `/etc/fstab.agenthub-bak` before rebooting. |
|
||||
| `a dashboard is still running — stop it first` | A dashboard outside both units holds the port: `agenthub stop`, then run it again. |
|
||||
| `agenthub.service did not stay up` | `sudo journalctl -u agenthub -n 50`. The unit reads `<data dir>/runtime/agenthub.system.env`. |
|
||||
| `502 EPROVISION` when adding a user | The helper's own message says why (name, existing account or folder). No AgentHub user was made. |
|
||||
| `400 EBADUSER` | The username does not fit the Linux rule (above). |
|
||||
| `409 EMEMBERGONE` on a start or a git action | The workspace's folder is under the users root but its user is disabled, unknown or not provisioned: `agenthub users enable <name>` or `agenthub users provision <name>`. |
|
||||
| `409 ENOTSHARED` on a member's start | That tool is not published to `/opt/agenthub/tools` yet: `agenthub server tools`. |
|
||||
| `403 EMEMBERLANE` | Worktree mode and the runtime layer are not offered in a member's folder; use Repo mode and the prompt box's New worktree. |
|
||||
| `400 EMEMBERCRED` | A member signs in to their git host from their own terminal; AgentHub's stored git logins are not used for them. |
|
||||
| `403 EPEER` on a hook | The hook came from an account that is neither the host nor an enabled member, or the uid could not be read. |
|
||||
| `403 EFOREIGN` on a hook | A member's hook named a folder, session or pane outside their own home. |
|
||||
| `403 EFOREIGN` on a file view or save | The workspace's folder is now a link into another account's files. Put the real folder back, or make a workspace on the real one. |
|
||||
| A member's runs show as abandoned | Another, older AgentHub server on the same database (see Upgrade). |
|
||||
| `sudo: a password is required` in the server log | The sudoers file is missing, or a tool moved off its recorded path: re-run `install-system`. |
|
||||
| `[drop-caps] setpriv is missing` | Install util-linux's `setpriv` at `/usr/bin/setpriv`; until then the dashboard's children inherit `CAP_DAC_READ_SEARCH`. |
|
||||
| `[plugin-baseline] <name>: claude is not in the shared tools yet` | `claude` is not published to `/opt/agenthub/tools` yet: `agenthub server tools`, then `agenthub users provision <name>`. |
|
||||
| `[plugin-baseline] <name>: … failed` with `sudo: a password is required` | The sudoers file predates the baseline: re-run `install-system`. |
|
||||
| A member's first Claude run asks about Bypass Permissions | Expected: that consent stays the member's own. |
|
||||
|
||||
## Turn it off
|
||||
|
||||
|
||||
Reference in New Issue
Block a user