Files
AgentHub/docs/plans/remote-clients/README.md
T
nntrivi2001 c946b73d11 chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes
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.
2026-11-04 15:16:49 +07:00

222 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
1. Each user may install **AgentHub local** on their own machine and enroll it
with the hub once. **S3 stays** alongside it.
2. **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.
3. 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.
4. 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.
5. 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.
6. Use of a device token outside AgentHub is **detected**; `remote_gate`
decides `warn` (default: forward, mark the session unmonitored, alert) or
`block` (403).
7. The hub is reached over the company LAN only (`10.1.13.7`); a device token
**expires after 90 days** (`agenthub remote login` again). 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.
8. 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.
9. 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`).
10. 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=1` in 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*.json`
`env` overrides 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 `claude` is 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 `usage` from 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_TOKEN` once: 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 for
`AGENTHUB_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=block` is 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.