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:
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user