fa156d6a59
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.
176 lines
9.8 KiB
Markdown
176 lines
9.8 KiB
Markdown
# 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)
|
||
|
||
1. Each user installs **AgentHub local** on their own machine.
|
||
2. The **full conversation** is uploaded to the hub — only for sessions started
|
||
through AgentHub.
|
||
3. 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.
|
||
4. Use without AgentHub is **detected and warned about**, and **blocked when an
|
||
admin turns blocking on**.
|
||
5. The hub is reached over the company LAN at `http://10.1.13.7:4820` — **no
|
||
HTTPS**.
|
||
6. An admin sees, in one click from the Users page, which machines a user has,
|
||
which workspaces they used, and what each session did.
|
||
7. `server-s3` is 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, within `REMOTE_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=1` in 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*.json` `env` overrides the pane's
|
||
`ANTHROPIC_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): `/usage` quota 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; `block` stops a plain `claude` using 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_CERTS` for 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.
|