docs(plans): workspace sharing — self-review findings (R1 blocks)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user