Chat UI: - Each Claude/Codex session keeps its own chat/terminal view; Settings only sets how new sessions open. - Composer + menu: upload, project files and folders (#54, also dragged from the Files/Changes tree), context files, recent uploads, skills, plugins. - Activity side panel (progress, outputs, files read, uploads, memory, skills, connectors) with a file preview; edited-file counts per turn. - Permission-mode chip (Shift+Tab steps, read back from the pane: server/lib/pane-mode.js, GET|POST /api/run/:id/permission-mode). - Stop (Esc) while the main agent works; the status is checked per agent (with session_id /api/agents ignores its status filter) and re-read every 5 s while Stop shows. - Dropped images shown in the conversation (GET /api/run/drop/:name, the requester's own drop dir), full-size viewer. - A plain-text paste over 5,000 characters is attached as pasted-text.txt, so no prompt reaches tmux/argv limits (#68). Changes panel: every /api/lanes/:id/git* route takes ?source=<lane_sources id> (sourceFolder; ENOSOURCE otherwise) and a folder picker switches the whole panel to a workspace's extra folder (#70). Server mode S3 (one Linux user per account) — experimental, verified with fake sudo plus a P6 managed-settings spike on a test account: - member tmux listing runs in the background (cached, 15 s => unknown); - transcript snapshot copy is async, chunked, temp file + rename; - Express Layer patch only on Express 4; - tmux capturePane/sendKey go to the member's own server. Docs: the multi-user design moved in as docs/plans/server-mode/README.md (the AgentHub-Server repo is gone); S3-PLAN P6 result; LANES/API/openapi.
9.8 KiB
Remote clients — work runs on each user's machine, the hub only manages
Status: PLAN, not built. Decided 2026-10-09. Branch server-s3 (one Linux
user per account on the hub) stays as it is until this plan is proven or dropped.
Why
Everyone logging into one machine (server mode, S3) does not scale past ~10 concurrent users: every pane, build and test runs on the hub. Here the compute moves to each user's machine; the hub keeps identity, the shared accounts, and every workspace/session/conversation/token record.
Decisions (user, 2026-10-09)
- Each user installs AgentHub local on their own machine.
- The full conversation is uploaded to the hub — only for sessions started through AgentHub.
- Shared accounts (the Claude pool, shared AI Proxies) are usable only through AgentHub. The company has at least as many seats as people; a seat may be used by more than one person.
- Use without AgentHub is detected and warned about, and blocked when an admin turns blocking on.
- The hub is reached over the company LAN at
http://10.1.13.7:4820— no HTTPS. - An admin sees, in one click from the Users page, which machines a user has, which workspaces they used, and what each session did.
server-s3is kept until this architecture is settled.
Shape
user machine — AgentHub LOCAL hub 10.1.13.7 — AgentHub SERVER mode
┌───────────────────────────────────┐ ┌──────────────────────────────────┐
│ pane: claude │─hooks +▶│ /api/remote/ingest (device token)│
│ ANTHROPIC_BASE_URL = hub gateway│ transcr.│ → sessions/events/tokens, owned │
│ ANTHROPIC_AUTH_TOKEN = ahdev_… │ │ by the device's user │
│ │─/v1/… ─▶│ /api/proxy-gw → pool token added │
└───────────────────────────────────┘ └──────────────────────────────────┘
No pool token or proxy key ever reaches a user's machine; it holds one device
token. Plain HTTP means anyone sniffing the LAN can read that token and the
conversations — accepted for an internal network. Moving to HTTPS later is a
reverse proxy in front of the hub (Caddy tls internal), no code change.
What the hub can see (per tool, checked 2026-10-09)
Uploads cover every session the machine's AgentHub started, in ANY of its workspaces — company repo or the user's own project alike. A session run outside AgentHub on the user's own login is invisible; only pool use is gated.
| Tool | Conversation | Tokens / cost | Pool / gate | Reader today |
|---|---|---|---|---|
| Claude Code | yes | yes | yes (R3) | hooks + transcript |
| Codex | yes | yes | not yet (Codex pool = later) | codex-usage.js rollouts |
| Antigravity | no — needs a renderer for transcript_full.jsonl |
yes | no (own login) | antigravity-usage.js |
| Oh My Pi, other catalog tools | no | no | no | none: only the pane run (workspace, time) |
| DeepSeek CLI / not in catalog | no | no | no | not launchable as a tool; a shell pane records nothing |
R2 therefore uploads Claude transcripts, Codex rollouts and agy conversation
files (remote-sync.js today mirrors only the first two). Any tool beyond that
needs its own reader first — one per tool, on demand.
Gating
Verified 2026-10-09 against Claude Code 2.1.295 with a capture server: every
/v1/messages call carries x-claude-code-session-id: <id> (same id in
metadata.user_id), and --session-id <uuid> sets it — spawnRun already
starts Claude panes that way.
The gateway checks each request:
- bearer = a device token that is not revoked, of a user who is not disabled — else always 401;
x-claude-code-session-id= a session the hub received uploads for, from THAT device, withinREMOTE_LIVE_SEC(300 s; 60 s grace for a new session).
A request failing the second check is a session AgentHub is not uploading
(token copied out of AgentHub, or uploads switched off). Setting
remote_gate decides: warn (default) forwards it and records the session
as unmonitored with its token count from the response's usage, and
fires an alert (existing fireAlert); block refuses it with 403. Ceiling:
someone who also fakes the upload stream passes — detection, not proof.
Relay spike (2026-10-09, Claude Code 2.1.295, script kept in
~/.cache/agenthub-spike/relay.js): a client with NO login and
ANTHROPIC_AUTH_TOKEN=<fake> worked through a relay that swapped in a team
subscription's OAuth token (+ anthropic-beta: oauth-2025-04-20). Checked:
answers, --resume, a local Bash tool call, prompt cache reads on every turn
(18–26k tokens), anthropic-ratelimit-unified-* headers passed back. Three
findings for R3:
- Through a base URL Claude Code writes 5-minute cache entries; set
ENABLE_PROMPT_CACHING_1H=1in the pane (verified: 1h entries) to keep the subscription's 1-hour TTL. - The cache is per ACCOUNT (a fresh client read 18k cached tokens on its first call), so the gateway must keep a session on ONE pool account — switching accounts mid-session re-writes the whole cache.
- A repo's
.claude/settings*.jsonenvoverrides the pane'sANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN(seen in this repo) — give the gateway through--settings=<file>, the layer that outranks project settings, as AI Proxy panes already do. Not checked yet (interactive features that call claude.ai with the user's own login, likely unavailable):/usagequota view, claude.ai MCP connectors, Remote Control / web sessions, org plugins from claude.ai.
What is and is not guaranteed:
- Guaranteed: no hub account (or a disabled user / revoked device) → no
issued credential, because the credential never leaves the hub. This holds
only while the ADMIN alone holds each shared seat's login (setup-tokens
made by the admin). A seat whose login (email/SSO) an employee knows can
always be used directly at claude.ai or in a plain
claude— AgentHub cannot stop that. - Not guaranteed: that the AgentHub APP was used. The device token sits on
a machine the user controls;
blockstops a plainclaudeusing it, unless the user also fakes the upload stream. Hard enforcement needs managed, locked-down machines. - Closes the visibility gap (R3b): the gateway sees every request body, so
it can record the conversation of every pool request ITSELF — new messages
per request by session id, plus the streamed answer and its
usage— no matter which client sent it. Then nothing that uses an issued credential is invisible to the admin, AgentHub app or not. - Plain HTTP: a LAN sniffer can take a device token and act as that user.
If that matters, HTTPS (reverse proxy + an internal CA every machine trusts,
NODE_EXTRA_CA_CERTSfor Claude Code).
Phases
Each one ships alone and changes nothing for a local-only install.
R0 — user activity view (hub, useful today). Users page row → a user
page: their devices (once R1 exists), workspaces, and sessions by day with
tokens/cost; a session opens the existing session page (conversation, tool
calls). Backend: an owner=<user id> filter for staff on GET /api/sessions
and GET /api/lanes, reusing sessionWhere/laneWhere for that user — no
new query logic. Admin reads are audited as they already are.
Verify: server test for the filter (developer cannot use it); client snapshot.
R1 — devices (hub + CLI). Table devices (id, user_id, name, token_hash,
created_at, last_seen_at, revoked_at). agenthub remote login http://10.1.13.7:4820 signs in with the user's hub password (throttled like
/api/auth/login), receives an ahdev_ token once, stores it 0600 in the
runtime dir. The user page lists devices; an admin revokes one (re-auth +
audit_log).
Verify: mint/hash/revoke tests; a revoked token gets 401.
R2 — upload (both sides). An enrolled local AgentHub uploads each of ITS
sessions (dashboard_runs): hook events as they arrive, the session files of
the table above (Claude transcript, Codex rollout, agy conversation)
incrementally (byte offset, retry queue on disk, never blocking a hook). The
hub stores transcripts under <data dir>/remote/<user id>/<device id>/projects/
and imports them with the SAME importFromDirectory remote-sync.js uses, so
tokens and cost match local sessions. Sessions are owned by the device's user
(source = device:<id>); workspaces are mirrored as records (title, folder,
tool) — shown, not driven. The existing ingest-batch stays as it is.
Verify: two local instances → one hub; R0's page shows both users' work.
R3 — remote gateway. The local spawnRun points an enrolled machine's
Claude pane at the hub (ANTHROPIC_BASE_URL, device token as
ANTHROPIC_AUTH_TOKEN, through tmux -e). proxy-gateway.js (from
origin/main since 84e2a9f0) gains the remote path and checks above; for the pool it picks an
account with pickAccount and sends its OAuth token. Spike first on ONE
real pool account: which headers a setup-token needs through a relay.
Verify: a pane gets "pong"; a curl with the device token and a session id
nobody uploads is logged (warn) or 403 (block).
R4 — shared skills/plugins. No AgentHub code: the company marketplace (git)
plus a managed-settings.json template (extraKnownMarketplaces,
enabledPlugins) and an install script for user machines.
Codex comes after R3 works for Claude: ChatGPT-account auth differs from an API key, and its session-id header still has to be checked.
Open risks
- A hub outage stops every pool session; a machine can still use its own login.
- The hub stores every conversation of an AgentHub session; tell users.