Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool): - server/hub/ holds the hub-only code (devices, remote sessions, upload ingest, LAN gateway), required only in server mode; hub-boundary.test.js keeps local code from importing it. - A local install enrolls with `agenthub remote login` or Settings → AI tools → Company hub; the "Company account" AI Proxy (reserved name agenthub.company) sends a session through the hub's gateway, which adds a pool account's token — no credential reaches the machine. Only those sessions are uploaded, hourly and at their end; secret-shaped strings are redacted on the client. - Gateway refuses: unknown device (401), a message without a session id, an unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30. - New alert rule type remote_unmonitored: a company-account session its device never registered; a late registration clears the flag instead. - The hub imports a session once per upload round (chunks carry `more`), not once per chunk. Artifacts: - Share an artifact with chosen people (artifact_shares, PATCH sharedWith, GET /api/artifacts/people) besides everyone; a "Shared with you" tab. Only the owner or an admin sees or edits who it is shared with; audited. - Folders: each account's own arrangement (agenthub.artifacts.folders pref). - A claude.ai page keeps a copy viewable without the publishing account: its HTML file (and supporting files) snapshotted at publish, or the copy Claude saves for a Doc / Slides / Design in artifacts-export/ (a synchronous PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch), attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF). Fixes: - tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's tmux (stop the last pane, start again within 10 s failed with ENOENT). - shelves: a shelf restores beside changes in other files. - chat: a finished reply ends the turn even with no hook at all. - Settings → AI tools: profile list display tweaks.
14 KiB
Remote clients — work runs on each user's machine, the hub only records company spend
Status: B1–B3 built 2026-11-04 for the Claude pool (§ Built); B4 and Codex not started. First decided 2026-10-09, re-scoped 2026-11-04 (this version). Server mode with S3 stays and runs side by side: a user with a weak machine keeps working on the hub, a user with their own machine runs AgentHub local and enrolls it.
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 the record of what the COMPANY's accounts were used for.
Decisions (user, 2026-11-04 — replaces the 10-09 list)
- Each user may install AgentHub local on their own machine and enroll it with the hub once. S3 stays alongside it.
- The account decides, not the workspace. Only a session that runs on a company account — the hub's pool or a shared AI Proxy, i.e. one that goes through the hub's gateway — is reported. A session on the user's OWN login or own proxy sends nothing: no conversation, no tokens, no heartbeat, not even the workspace's name. Neither does anything run outside AgentHub.
- What a reported session sends: its conversation and token usage, never the repository. The hub never reads the client's source tree, never runs anything on the client, never drives it (no terminal, no start/stop, no file access). The client pushes; the hub never connects to a client.
- Cadence: one registration when a company-account session starts (session id + device, no content — what the gateway checks), then conversation and tokens once an hour and once more when the session ends. No per-minute heartbeat (user, 11-04). A queue on disk carries what an offline machine could not send; it is sent back in the office. The conversation is uploaded whole (tool results and code included — user, 11-04: no stripping); only strings that look like keys, tokens or passwords are redacted before sending. The hub keeps a conversation 30 days; token and cost rows are kept.
- The company account's credential never leaves the hub. A client holds a device token only; its Claude calls go through the hub's gateway, which adds the real credential. Revoking the device token ends access at once.
- Use of a device token outside AgentHub is detected;
remote_gatedecideswarn(default: forward, mark the session unmonitored, alert) orblock(403). - The hub is reached over the company LAN only (
10.1.13.7); a device token expires after 90 days (agenthub remote loginagain). At home a company-account session simply cannot reach the hub — AgentHub local says so plainly — while the user's own login works as usual. Working from home later means a VPN, never exposing the hub. No HTTPS (user, 11-04): plain HTTP on the company LAN, accepting that someone already inside the network could sniff conversations, hub passwords and device tokens. Adding it later is a reverse proxy in front of the hub (docs/SERVER-MODE.md § Put HTTPS in front of it), no code change. - Company vs own is decided by the account picked at session start: an
enrolled machine's account menu gains "Company account", whose pane talks
to the hub's gateway; the default login and the user's own AI Proxies are
their own. It is recorded on the run (
dashboard_runs.proxy), shown as a "Company" badge, and a resume keeps it. A company account is usable only through the gateway, so neither side can be mislabelled. - Each user can see, on their local AgentHub, what was sent for each
session and when, and on the hub who (admin/analyst) opened their
sessions (
audit_log). - On the hub a client's sessions are shown grouped device › folder — no lane rows are created for them.
Shape
user machine — AgentHub LOCAL hub 10.1.13.7 — SERVER mode
┌──────────────────────────────────────┐ ┌──────────────────────────────────┐
│ pane on a COMPANY account: │─/v1/… ─▶│ gateway: device token + session │
│ ANTHROPIC_BASE_URL = hub gateway │ │ check → pool credential added │
│ ANTHROPIC_AUTH_TOKEN = ahdev_… │ │ → Anthropic; usage counted now │
│ │─start ─▶│ session registered (id only) │
│ │─hourly▶│ conversation + tokens │
│ pane on the user's OWN login │ (none) │ │
└──────────────────────────────────────┘ └──────────────────────────────────┘
What "conversation, not source" can and cannot mean
The hub stores no file of the repository. But a conversation is made of what
the agent read and wrote: a transcript's tool results hold the files Claude
Read, its Edit/Write inputs hold code, and every request through the
gateway carries the whole context (code included) on its way to Anthropic.
So "conversation only" still contains source excerpts. Decided (user,
11-04): they are NOT stripped — the transcript goes whole, only secret-shaped
strings are redacted.
Gateway checks (verified 2026-10-09, Claude Code 2.1.295)
Every /v1/messages call carries x-claude-code-session-id: <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 that SAME device registered at its start. The registration, not the hourly upload, is what this check reads.
Relay spike (2026-10-09, script ~/.cache/agenthub-spike/relay.js): a client
with no login and a fake ANTHROPIC_AUTH_TOKEN worked through a relay that
swapped in a team subscription's OAuth token (+ anthropic-beta: oauth-2025-04-20): answers, --resume, local tools, prompt-cache reads,
rate-limit headers. Rules it gave:
- set
ENABLE_PROMPT_CACHING_1H=1in the pane (a base URL drops to 5-minute cache entries otherwise); - keep a session on ONE pool account (the cache is per account);
- give the gateway through
--settings=<file>(a repo's.claude/settings*.jsonenvoverrides the pane env), as AI Proxy panes already do. Not checked:/usage, claude.ai MCP connectors, Remote Control — likely lost through a relay.
What is and is not guaranteed:
- Guaranteed: no hub account / disabled user / revoked device → no company credential, because it never leaves the hub — as long as the admin alone holds each shared seat's login.
- Not guaranteed: that the AgentHub app was used. A device token copied out
and used with a plain
claudeis caught by the registration check, unless the user also fakes a registration. Hard enforcement needs managed machines. - Quota and budget on company accounts stay real-time despite the hourly
upload: the gateway counts
usagefrom each response it relays.
What already exists (2026-11-04)
| Need | In the code | Missing |
|---|---|---|
| Hooks from another machine | POST /api/hooks/ingest-batch + push-token.js (push:<source>) |
one hub-wide token, no user/device; transcript dropped |
| Credential swap | proxy-gateway.js (pane token → real key, SSE streamed) |
loopback + pane token only |
| Shared accounts | pools, pool groups, pins, own choice, refreshLogins |
a path for a remote caller |
| Spend per user | pool-usage.js, user-quota.js, session-account.js, Administration user detail |
ownership of a session with no lane |
| Transcript import | importFromDirectory, codex-usage.js |
an upload path |
Phases
Each ships alone and changes nothing for a local-only install.
B1 — devices and ownership. Table devices (id, user_id, name,
token_hash, created_at, last_seen_at, revoked_at); agenthub remote login http://10.1.13.7:4820 (hub password, throttled like /api/auth/login) stores
an ahdev_ token 0600 in the runtime dir; an admin lists and revokes devices
on the user's page (step-up + audit_log). Column sessions.owner_id,
written from the device's user; sessionWhere honours it, so scoping needs no
lane. Verify: mint/hash/revoke tests; revoked → 401; a developer sees only
their own device sessions.
B2 — gateway for the Claude pool over LAN. proxy-gateway.js gains the
device-token path: heartbeat check, pickAccount + session-sticky account,
usage counted per response. The local spawnRun, on an enrolled machine and
only when the user picks a company account, points the pane at the hub through
--settings= and sets ENABLE_PROMPT_CACHING_1H=1. Spike first on one real
pool account. Verify: a pane answers "pong"; a curl with a device token and an
unknown session id is logged (warn) or 403 (block).
B3 — registration and hourly upload. For company-account sessions only:
one registration at start; conversation (Claude transcript, Codex rollout) and
tokens hourly and at session end, incremental by byte offset, disk queue,
never blocking a hook. The hub imports with importFromDirectory so cost
matches local sessions; shown device › folder on the user's page.
B4 — shared skills/plugins. No AgentHub code: company marketplace (git) +
a managed-settings.json template for user machines.
Codex comes after B2 works for Claude (ChatGPT-login auth differs from an API key; its session-id header is unchecked).
Built (2026-11-04)
Hub side — server/hub/, loaded only in server mode (server/index.js
requires it inside isServerMode(); server/__tests__/hub-boundary.test.js
fails if anything outside the folder requires it):
| File | Does |
|---|---|
devices.js |
ahdev_<32 bytes base64url> tokens, only the SHA-256 kept; 90 days (AGENTHUB_DEVICE_TOKEN_DAYS); revoke |
remote-sessions.js |
registration (UUID, absolute cwd, claude only); one session = one device — another user's device gets 409 EFOREIGN, the SAME user's new device (enrolled again) takes it over (moveSession moves its copy) |
remote-gateway.js |
/api/remote/gw/v1/*: device token → registered session (waits AGENTHUB_REMOTE_REGISTER_GRACE_MS, 5 s, for one in flight) → claude-pool.forRemote (sticky per session) → upstream AGENTHUB_REMOTE_UPSTREAM; usage read from a decompressed COPY of the answer (Anthropic sends it gzip/br) |
remote-ingest.js |
upload chunks at the hub's own offset (409 EOFFSET + expected), copy under <data>/remote/u<uid>/d<did>/<sid>/, importFromDirectory once per round (chunks carry more: true, the round's closing request imports; a round cut short is imported after AGENTHUB_REMOTE_FLUSH_MS, 2 min), then sessions.source = device:<id>, owner_id; purgeOld after AGENTHUB_REMOTE_RETAIN_DAYS (30) keeps token rows |
index.js |
/api/remote/device/* (device token: login, me, logout, sessions, end, files, upload), /api/remote/* (signed in: devices, revoke with password + audit, sessions, "who opened mine"), hourly purge |
Shared: sessions.owner_id (scope.js, session-account.js, users.js honour
it); claude-pool.forRemote and live remote sessions in the pool ranking;
auth.checkLogin.
Client side (local mode): server/lib/remote-client.js +
/api/remote-client + agenthub remote login|status|upload|logout, Settings →
AI tools → Company hub. The reserved AI Proxy name agenthub.company ("Company
account", Claude only) writes <data>/proxy-settings/agenthub.company.json with the
gateway URL, the device token and ENABLE_PROMPT_CACHING_1H=1. A company
session is registered at spawn, again on a new session id (SessionStart /
UserPromptSubmit hooks: /clear, a fork), uploaded hourly
(DASHBOARD_REMOTE_UPLOAD_MS) and soon after SessionEnd; every JSON line goes
through privacy.redactValue first.
Measured (2026-11-04): local mode loads none of server/hub/ (216 server
modules vs 214 before: remote-client.js + its route); startup and RSS
unchanged within noise. E2E on one real pool account: answers, sticky account,
/clear, resume, hub totals = transcript totals, no device token or key in the
hub copy, revoke → 401, hub down → 503 EHUBDOWN at start.
Known limits:
- Claude Code reads
ANTHROPIC_AUTH_TOKENonce: a pane open across a re-login (expired or revoked token) keeps failing with 401 until it is resumed. - An unmonitored session is flagged on the hub (
unmonitored, a warning in the log) and, once it has stayed unregistered forAGENTHUB_REMOTE_UNMONITORED_ALERT_MS(60 s), fires every enabled Company account outside AgentHub alert rule (remote_unmonitored). A registration that was only late clears the flag first. AGENTHUB_REMOTE_GATE=blockis covered by unit tests only.- A shared AI Proxy as a company account, and Codex, are not built.
Open questions
- Should an admin be able to mark a workspace "company account required"? (Today's answer: no — a user's own login in any workspace is their business.)
Open risks
- A hub outage stops every company-account session; the machine can still use its own login.
- Every byte of a company-account session's context passes through the hub in transit; tell users.