feat: v2.6.0 — Chat UI composer (+ menu, project files, activity panel, permission mode, Stop), git on a workspace's extra folders (#70), long paste as a file (#68), S3 server mode (experimental)

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.
This commit is contained in:
2026-10-09 15:51:19 +07:00
parent ce773a3f5a
commit fa156d6a59
114 changed files with 5831 additions and 885 deletions
+44 -1
View File
@@ -694,6 +694,20 @@ folder, and `git worktree list` of the workspace's repo must register it —
otherwise `400 ENOWORKTREE`, never a fall back to the main folder. With it,
`GET /git` also answers `worktree: {path, branch}`.
#### An extra folder's own repo (`?source=`)
Every `/api/lanes/:id/git…` route also takes `?source=<id>`: the read or write
then works in one of the workspace's extra folders (Context → Folder), each
its own repo — the Changes panel's folder picker. The tree is resolved from the
`lane_sources` ROW (`sourceFolder` in `server/lib/lane-sources.js`): a `folder`
row of THIS workspace that is still a directory — otherwise `404 ENOSOURCE`
(another workspace's row and a context-file row included), never a fall back
to the main folder. The folder is its own boundary: every guard above
(`resolvePaths`, the secret check, the `confirm: true` gates, `ESUBDIR` for a
folder below its repo root) applies to it as to a workspace folder, and
`GET /git` reports `base` as that repo's own default branch. A folder that is
not a git repo answers `{available: false}`.
#### Remove a session's worktree
```http
@@ -1551,6 +1565,32 @@ curl http://localhost:4820/api/sessions/sess_abc123
---
#### Get Session Outline
```http
GET /api/sessions/:id/outline
```
The session's plan, the files it changed and what it used, read off its successful tool calls (`PostToolUse` events) and its prompts (`UserPromptSubmit`) — the activity panel of a Workspace Chat pane shown alone. `progress` follows `TaskCreate`/`TaskUpdate` by task id (a `TodoWrite` replaces the list wholesale); `outputs` are the paths written or edited with `Write`/`Edit`/`MultiEdit`/`NotebookEdit`; `files` the paths it only `Read` (a file it later changed moves to `outputs`); `uploads` the files attached to its prompts through the composer (paths under the `agenthub-drops-<uid>` dir that `POST /api/run/drop-image|drop-file` made); `memory` the Claude Code memory files (`.claude/**/memory/`) it read or wrote, kept out of the other lists. The path lists are newest first. `skills` are `Skill` calls; `mcp` are MCP server names (`mcp__<server>__<tool>`, a `plugin_` prefix dropped). A file made by a Bash command is not an output here. 404 for an unknown session.
```bash
curl http://localhost:4820/api/sessions/sess_abc123/outline
```
```json
{
"progress": [{ "id": "1", "subject": "Build the report", "status": "completed" }],
"outputs": ["/home/me/work/Progress report.xlsx"],
"files": ["/home/me/work/data/kpi.csv"],
"uploads": ["/tmp/agenthub-drops-1000/3f9a0c1d2e4b5a6c-minutes.xlsx"],
"memory": ["/home/me/.claude/projects/-home-me-work/memory/launch.md"],
"skills": ["xlsx"],
"mcp": ["vercel_vercel"]
}
```
---
#### Get Session Stats
```http
@@ -2540,9 +2580,12 @@ GET /api/run/tmux { available: bool } — whether tmux is on
GET /api/run/binary?tool= { found, path } for the `claude`, `codex` or `agy` binary
GET /api/run/cwds Suggested cwds (dashboard, home, recent)
GET /api/run/history?limit=&laneId=&tool= Persisted run history; laneId narrows to one lane's runs, tool (with laneId) to one tool's — `shell` = Terminal tabs
GET /api/run/files?cwd=&q= Fuzzy file search inside cwd for the @-file autocomplete
GET /api/run/files?cwd=&q=&dirs=1 Fuzzy file search inside cwd for the @-file autocomplete (dirs=1 adds folders as `dir/`, for the + menu)
POST /api/run/drop-image Raw image body (png/jpeg/gif/webp, ≤20 MB) → {path}: saved 0600 under <data dir>/drops/ (0700; S3: the member's ~/.agenthub/handoff/drops/) with a random name, pruned after a day; the terminal pastes the path
POST /api/run/drop-file?name=&laneId= Any raw file body (≤20 MB) → {path}: same dir (S3: the handoff dir of the account workspace `laneId`'s panes run as, else the requester's), 0600, named <random>-<name reduced to a [A-Za-z0-9._-] basename>; what the terminal, the prompt box and its paste use for every dropped file or image
GET /api/run/drop/:name A dropped png/jpg/gif/webp served back (nosniff, sandbox CSP) from the requester's own drop dir (S3: their member account's, owner-checked); only a name drop-image/drop-file made → 404 otherwise
GET /api/run/:id/permission-mode A live claude pane's mode, read off its mode line → {mode} (null while a menu hides it)
POST /api/run/:id/permission-mode {mode} → Shift+Tab steps, each read back, until the pane shows it; 409 EMODEUNKNOWN/EMODEUNAVAILABLE/EBUSY
(skips node_modules, .git, dist, build, .next, .cache, coverage, vendor)
POST /api/run Start/attach run — Body: { laneId, cwd?, model?, permissionMode?, effort?, resumeSessionId?, initialPrompt?, sandbox?, approval?, resumeLast?, bypass? }
GET /api/run/:id Run handle (returns live run state)
+13 -14
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -85,8 +85,10 @@ Internal build — all rights reserved.
light: Default, Apple Light, Canvas, Orca Light. Each mode
remembers its own theme, so the sidebar's quick toggle swaps between the two.
It also picks the workspace terminals' font: JetBrains Mono (default), the
rounded Maple Mono, or the browser's own fixed-width font (which may lack some
Vietnamese letters). Per-browser only.
rounded Maple Mono, or the browser's own fixed-width
font (which may lack some Vietnamese letters). Per-browser only. Text across
the UI and the terminals is one step heavier than stock (regular 500, bold
700), and the Chat UI default sits at the top of the page.
- **Notification sounds.** Settings → Notifications picks a sound (None, Chime,
Pop, Alert — off by default) played with every notification shown, and each
notification type can pick its own.
+8 -1
View File
@@ -113,7 +113,11 @@ check; the AI proxy page shows them only their personal profiles' Edit. An
analyst's Workspace page and sidebar list only
their own workspaces (`useWorkLanes`); other users' work is read on Sessions,
Analytics and Users, where Resume, Fork, Snapshot and "Open in workspace" are
hidden for a session that is not theirs. An analyst's alert feed
hidden for a session that is not theirs. The sidebar's workspace list shows an
admin or analyst EVERY workspace, grouped under its owner (each lane carries
`owner_name`; yours first, no owner last, each group folds): for an analyst
another user's workspace there is read only — no menu, rename or new session,
no live tabs, and a session opens its analysis page (`/sessions/<id>`). An analyst's alert feed
(`GET /api/alerts`) carries `can_ack` on each alert — true only for their own
sessions' — and the feed offers Acknowledge only there; Ack all acknowledges
only theirs.
@@ -200,6 +204,9 @@ user makes the folder (it is the Linux account's home).
under `/api` answers `401 EAUTH` until someone logs in, and the UI shows the
login page. Paths are checked the way Express routes them: decoded, with
repeated slashes collapsed, and case-insensitive.
- The login page follows the OS's light/dark. After a login the dashboard
wears that account's own appearance (Settings → Appearance, saved in
`/api/me/prefs`); an account that never chose one follows the OS.
- Hooks (`/api/hooks/*`) are accepted only from this machine: a loopback socket
with no forwarding header. A request through the proxy gets
`403 ELOCALONLY`. `POST /api/hooks/ingest-batch` keeps its own bearer token.
+3 -3
View File
@@ -31,10 +31,10 @@ Asked 2026-10-08 (modelled on Orca's 3-step welcome). Decisions:
3. Pet: none or an installed pet, and its size.
4. Notifications: enable, sound and preview, send a test.
The footer has Skip, Back and Continue (Ctrl+Enter). The last step's button is "Add your first workspace", which starts the tour.
The footer has Skip, Back and Continue (Ctrl+Enter). The last step's button is "Add your first workspace", which starts the tour on the Dashboard (`/`).
- [x] `components/onboarding/Tour.tsx`: a spotlight on `data-tour` targets. Each step moves on when its condition holds, and the step reached is saved in prefs (`agenthub.tour`). The steps:
- Add workspace
- the folder field
- Add workspace: the sidebar's **+** (the rail's tree icon while the sidebar is folded)
- the folder field (its suggestions list included while it is open) — or the folder browser while it is open over it
- Create
- the prompt box
- sending the prompt
+175
View File
@@ -0,0 +1,175 @@
# 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.