From 045e46e1112bf7be3b8785581409ef97a3f59dd2 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 6 Nov 2026 10:27:43 +0700 Subject: [PATCH] =?UTF-8?q?docs(plans):=20workspace=20sharing=20=E2=80=94?= =?UTF-8?q?=20self-review=20findings=20(R1=20blocks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/workspace-sharing/README.md | 74 +++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/plans/workspace-sharing/README.md b/docs/plans/workspace-sharing/README.md index 4568a10f..9f58ba19 100644 --- a/docs/plans/workspace-sharing/README.md +++ b/docs/plans/workspace-sharing/README.md @@ -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-`, 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=` 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.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