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.
This commit is contained in:
2026-11-04 15:16:49 +07:00
parent 3daf7ed4cb
commit c946b73d11
95 changed files with 7918 additions and 340 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ One place to keep what agents PRODUCE, and (in server mode) to share it:
| `plugin` | A plugin by marketplace source + name (no files) | picked from the list a marketplace link answers (`POST /api/artifacts/plugin/inspect`), or typed as `name@marketplace` |
- **Local mode:** one personal library, no sharing UI.
- **Server mode:** every artifact is the OWNER's and private by default. The owner can **Share** it; a shared artifact appears in the **Hub** for every signed-in user. A shared skill or plugin can be **installed** into the viewer's own account.
- **Server mode:** every artifact is the OWNER's and private by default. The owner can **Share** it with everyone — it appears in the **Hub** for every signed-in user — or with chosen people only (`artifact_shares`; they find it under **Shared with you**). A `page` keeps a viewable copy — its HTML file, or the copy Claude exports of a Doc / Slides / Design (`artifacts-export/`) — so the people it is shared with need no access to the claude.ai account that published it. A shared skill or plugin can be **installed** into the viewer's own account.
## Rules (binding, same spirit as CLAUDE.md)
@@ -22,7 +22,7 @@ One place to keep what agents PRODUCE, and (in server mode) to share it:
- **Nothing shared runs on the dashboard's origin.** HTML/SVG previews are served under the `/git/raw` headers (`sandbox` CSP + `nosniff`) and shown in a sandboxed iframe; a type that sandbox cannot show (Office files, zip, PDF — the sandbox blocks both a download and the PDF viewer) gets Download only, never a blank Preview. A `page` is only a link out, accepted only as `https://claude.ai/…`.
- **Installing is the viewer's explicit click, into the viewer's own account only.** For a skill, the hub writes the snapshot's files into `~/.claude/skills/<name>/` of the VIEWER (S3: as their member, through `write-as`). It never overwrites an existing skill without `conflict: replace`, and the old one goes to the trash first. For a plugin, the hub runs `claude plugin marketplace add <source>` + `claude plugin install <name>@<market> --scope user` as the viewer (the `applyPluginBaseline` path). The marketplace source passes an allowlist (`owner/repo` or a `validateRemoteUrl` URL), and no argv comes from the request beyond those two validated values.
- **Before installing, the dialog lists every file of the skill** (a skill may carry scripts that the viewer's agent will run). Every share, unshare and install is written to `audit_log`.
- **Visibility follows ownership:** a developer sees their own artifacts plus shared ones; admin/analyst see per `scope.js`, like sessions. Unshare removes an artifact from the Hub but does not uninstall copies others already installed.
- **Visibility follows ownership:** a developer sees their own artifacts plus those shared with everyone or with them by name; admin/analyst see per `scope.js`, like sessions. Unshare removes an artifact from the Hub but does not uninstall copies others already installed.
## Data
+186 -140
View File
@@ -1,175 +1,221 @@
# Remote clients — work runs on each user's machine, the hub only manages
# Remote clients — work runs on each user's machine, the hub only records company spend
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.
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
every workspace/session/conversation/token record.
the record of what the COMPANY's accounts were used for.
## Decisions (user, 2026-10-09)
## Decisions (user, 2026-11-04 — replaces the 10-09 list)
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.
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 — 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 │
└───────────────────────────────────┘ └──────────────────────────────────┘
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) │ │
└──────────────────────────────────────┘ └──────────────────────────────────┘
```
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 "conversation, not source" can and cannot mean
## What the hub can see (per tool, checked 2026-10-09)
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.
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.
## Gateway checks (verified 2026-10-09, Claude Code 2.1.295)
| 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:
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 the hub received uploads for, from
THAT device, within `REMOTE_LIVE_SEC` (300 s; 60 s grace for a new session).
- `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.
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.
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 (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).
- **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 one ships alone and changes nothing for a local-only install.
Each 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.
**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.
**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.
**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`).
**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.
**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.
**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`).
**B4 — shared skills/plugins.** No AgentHub code: company marketplace (git) +
a `managed-settings.json` template for user machines.
**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 B2 works for Claude (ChatGPT-login auth differs from an API
key; its session-id header is unchecked).
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.
## 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 pool session; a machine can still use its own login.
- The hub stores every conversation of an AgentHub session; tell users.
- 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.