feat(server-mode): share one session; scope collaborators' pane tokens (sharing phase 3)

A workspace's owner (or an admin) can share ONE session from its right-click
menu (GET/PUT /api/lanes/:id/sessions/:sid/shares, table session_shares,
audited). The user sees the workspace (access: "session") with only that
session, joins the pane holding it, resumes it on their own account in a
free slot, and uses the workspace's git and Files reads and git writes;
everything else of the workspace answers 403 ESESSIONSHARE (scope.js
sessionShareAllows in scopeGuard). Hook/runtime broadcasts stay with full
collaborators.

A collaborator's pane token (readable by the owner's Linux account) now acts
as them inside that workspace only, and never starts or bills a session
(auth.js paneLaneAllows, 403 EPANESCOPE); a Terminal tab the owner or an
admin opens acts as the owner again. Docs say plainly that under S3 a
session share hands over the owner's account inside the terminal.
This commit is contained in:
2026-11-06 11:25:40 +07:00
parent 1a365a45bd
commit 879a6bb02d
24 changed files with 838 additions and 120 deletions
+17 -1
View File
@@ -2121,7 +2121,23 @@ session hours are billed to them (`session_payers`; a Terminal tab's stay the
owner's). A user disabled since stays on the
list (`disabled: true`) and may be kept or removed; only a new one must be enabled. Users taken off
get the WebSocket message `lane_access` `{laneId, userIds}` (sent to them only). See
`docs/SERVER-MODE.md` → Sharing a workspace.
`docs/SERVER-MODE.md` → Sharing a workspace. A collaborator's pane token
answers only that workspace's reads, the workspace list (filtered to it), its
stage / up / down / hook / feature / mcp-sync writes and `/api/health`
(`403 EPANESCOPE` — never a start or resume).
```http
GET /api/lanes/:id/sessions/:sid/shares → { shares, canManage }
PUT /api/lanes/:id/sessions/:sid/shares {"userIds": [3]} → { shares }
```
One session shared on its own (`session_shares`), same rules as the workspace
list (owner or admin, enabled users, audited `share session <id>`, `404
ENOSESSION` for a session of another workspace). The user gets the workspace
in `GET /api/lanes` with `access: "session"`, only that session in its session
lists, the pane holding it, a resume of it (`start {resumeSessionId}` — no
fork, no new session, no shell), and the workspace's git/Files reads and git
writes; any other call on the workspace answers `403 ESESSIONSHARE`.
#### Workspace context: instructions, folders, context files, memory
+35
View File
@@ -301,6 +301,41 @@ enabled. Every share and unshare is audited (`share workspace <id>` /
the next attach re-check. Deleting the workspace, or the user, drops the rows;
giving the workspace to a user it was shared with drops theirs.
**A collaborator's pane token acts as them inside that workspace only.** The
owner's account can read it (it sits in the owner's home), so it answers only
reads of `/api/lanes/<that id>/…` (and `/api/workspaces/<id>/…`), the
workspace list (which then holds that workspace only), `/api/health`, and its
`stage`, `up`, `down`, `hook/<name>`, `features/activate` and `mcp/sync`
writes (`paneLane`, `403 EPANESCOPE` for everything else) — never a start,
resume or any other call that could bill them. The CLI's workspace commands
work from their pane; the MCP server and the statusline's spend line do not.
A pane the owner or an admin opens still acts as the owner.
#### Sharing one session
The owner (or an admin) can also share ONE session: right-click it in the
sidebar → **Share…** (`GET/PUT /api/lanes/:id/sessions/:sid/shares`, table
`session_shares`, audited as `share session <id>`). The user sees the
workspace in their list (`access: "session"`) with only that session in it,
attaches to the pane holding it, sends it messages (a resume, on their own
account and billed to them, as above) and uses the workspace's Changes, git
and Files reads and git writes. Everything else of the workspace — another
session, a new session, a fork, a Terminal tab, `/api/run`, its instructions,
context and settings — answers `403 ESESSIONSHARE` (`sessionShareAllows` in
`scope.js`, checked in `scopeGuard`). Their run lists show only runs that held
that session, and a pane switching to another session closes their terminal
at the next attach re-check. Taking it back, deleting the workspace or the
user drops the rows. A resume names a free slot (`slot: "auto"`, never pane
0) and a session of THAT workspace; the session's name and archive state stay
the owner's (`PATCH /api/sessions/<id>` is `ESESSIONSHARE`), and the
workspace's extra folders (`?source=`), its files list for Artifacts and its
stage history stay out of reach. **These limits hold for the dashboard's API
only.** Under S3 the pane runs as the owner's Linux account: inside it the
user can `/resume` any session of the folder, run shell commands and read the
owner's home — a session share hands over as much as a workspace share
there. The Share dialog says so; share only with people you would hand your
keyboard to.
### AI Proxy profiles
Each user may keep **personal** AI Proxy profiles beside the **shared** ones:
+1 -1
View File
@@ -1,6 +1,6 @@
# Workspace & session sharing — plan
Status: **phases 1–2 built 2026-11-06** (workspace sharing; collaborators run on their own account, billed per payer by hour — `session_payers`; R1 decided as option **(a)** — accept and warn hard). Phases 3–4 not built.
Status: **phases 1–3 built 2026-11-06** (workspace sharing; collaborators run on their own account, billed per payer by hour — `session_payers`; one session can be shared — `session_shares`; R1 decided as option **(a)** — accept and warn hard). Phase 4 not built.
An independent review of phase 1 found and got fixed: a collaborator's Terminal
tab acted as the owner through its pane token (now `dashboard_runs.started_by`),
`POST /api/run {shell:true}` bypassed `ECOLLABSTART`, a disabled collaborator