docs(plans): workspace sharing — self-review findings (R1 blocks)

This commit is contained in:
2026-11-06 10:27:43 +07:00
parent e7573f13d9
commit 045e46e111
+73 -1
View File
@@ -1,6 +1,6 @@
# Workspace & session sharing — plan
Status: **reviewed, open questions answered** (2026-11-06). Nothing here is built.
Status: **self-reviewed 2026-11-06 — BLOCKED on finding R1 (below)**. Nothing here is built.
Server mode only. In local mode there are no users, so there is nothing to share
and every piece below is a no-op (`isServerMode()`).
@@ -181,6 +181,78 @@ with and without a share), `server-mode-review-fixes.test.js` style 404s for a
non-collaborator, users/quota attribution, websocket audience, and the client
dialog/sidebar tests.
## Self-review against the code (2026-11-06)
Checked every claim above against `server/lib/scope.js`, `auth.js`
(`userFromPaneToken`), `hook-auth.js` (`peerIsPaneAccount`), `pty-run.js`
(`spawnRun`), `user-quota.js`, `lane-sources.js` (`launchExtras`),
`websocket.js` (pty attach) and `routes/users.js`.
**R1 — CRITICAL: sharing a workspace hands out a shell as the owner's whole
account, not just that workspace.** A collaborator who types into a pane (or
whose prompt makes Claude run Bash) runs as `ah-alice`, whose home holds ALL of
Alice's workspaces, `~/.ssh` keys, `~/.config/agenthub/` HTTPS store, Claude and
Codex logins (`.credentials.json`, `auth.json`) and the env of her live panes.
So Bob can read Alice's other projects, copy her SSH key and her Claude
subscription token, and — via `$AGENTHUB_PANE_TOKEN` in any of her panes — call
the CLI/MCP **as Alice** (`userFromPaneToken` returns the lane owner, and is
accepted from Alice's uid, which Bob's typing runs as). This also defeats
decision 1 ("never the owner's subscription"): Bob can simply use her login.
The section "The one fact everything else follows from" understated this.
Options:
- **(a) Accept it, warn hard.** Share = "give this person your account". Simple,
honest, no isolation.
- **(b) Team workspaces in their own Linux account.** A shared workspace lives
under a dedicated account (`ah-team-<id>`, provisioned by `agenthub-provision`
like a member, its own home, git identity, logins = the pool's `token` kinds
only). Every member of the team works there; nobody's personal home is
exposed. Real isolation; more work (provisioning, git identity per team,
`assertOwnerFits` learns team folders, moving an existing workspace = clone).
- **(c) Chat-only collaborators.** No terminal for collaborators — but Claude's
own Bash tool still runs as Alice, so this isolates nothing. Not proposed.
**R2 — Typing into the owner's live pane spends the owner's subscription.**
Unavoidable while the pane runs on her login; contradicts the spirit of
decision 1. Under (b) it disappears (team panes use pool `token` accounts).
**R3 — Pane tokens.** `userFromPaneToken` maps a token to the LANE OWNER. Needs
`dashboard_runs.started_by` → the token's user. And every pane token of a shared
lane (the owner's too, see R1) must be limited to that lane, or one leaked token
is a whole account.
**R4 — Session-only sharing does not fit the current checks.** `canSeeRun` and
every `/api/lanes/:id/...` route decide by LANE (`canSeeLane`). Granting a
session collaborator `canSeeLane` would show them the whole workspace. Needs a
run/session-level predicate and an allowlist of lane routes usable with
`?session=<shared id>` only. Recommend: drop session-only sharing from the first
release, or limit it to that session's pane + chat (no Changes panel).
**R5 — Per-account limits are shared.** `MAX_AI_SESSIONS` (default 4) and
`member-guard.js` count per Linux account, so collaborators' sessions eat the
owner's 4 (no issue under (b): the team account has its own limit).
**R6 — Spend-quota flag is one file per account** (`quota.json` in the member's
handoff dir, `flagEnv`). Payer-based quota needs one file per payer
(`quota-u<id>.json`) and `AGENTHUB_QUOTA_FILE` pointing at the payer's.
**R7 — `launchExtras` puts the lane OWNER's personal instructions** first in
every pane; a collaborator's session should get the starter's.
**R8 — AI Proxy.** `resolveProxy(tool, proxy, laneOwner)` resolves by owner: a
collaborator could spend the owner's proxy key. Resolve by the starter — and a
collaborator's OWN proxy profile is a valid "own account" besides a `token` pool
account (decision 1 should allow it).
**R9 — Other `owner_id = ?` call sites** to extend or keep owner-only, checked
one by one: `scope.js` (`laneWhere`, `canSeeLane`, `visibleLaneIds`,
`insideOwnWorkspace`), `auth.js` (demote/disable ends a user's host panes —
must also end runs they `started_by` elsewhere), `pool-usage.js`,
`member-tmux.js`, `me.js` counts, `users.js`.
**R10 — Simplification found:** the pty attach socket already re-checks its user
periodically ("still owning the pane's workspace", `websocket.js`), so revoking
a share closes live attaches by just changing the predicate — no new kill path.
## Answered questions (2026-11-06)
1. A collaborator's run uses **their own `token`-kind pool account, or is