feat(server-mode): share a workspace with other users (sharing phase 1)

A workspace's owner (or an admin) shares it from its ⋯ menu → Share…
(lane_shares, GET/PUT /api/lanes/:id/shares, audited). The users it is shared
with see it, its sessions, Changes, history and live updates, join and type
into its open terminals, commit and push — every check through scope.js
(canSeeLane, sessionWhere, laneWhere, broadcastAudience viewers). Rename,
reset, remove, delete and sharing stay the owner's or an admin's
(403 ENOTOWNER), and a collaborator starts no AI session there yet
(403 ECOLLABSTART; a plain terminal tab is fine). Budgets and Today stay on
owned rows only (ownedOnlySessionWhere). Taking a share back sends lane_access
to those users and their terminals close at the next attach re-check.

Under S3 a collaborator works as the owner's Linux account; the Share dialog
says so plainly (docs/plans/workspace-sharing/, R1 option a).
This commit is contained in:
2026-11-06 10:43:16 +07:00
parent 045e46e111
commit 87ba935c6e
25 changed files with 1026 additions and 40 deletions
+17
View File
@@ -2098,6 +2098,23 @@ POST /api/locks/:name/release
- **409** `{ "error": { "code": "ENOTHOLDER", "message": "...", "currentHolder": "lane1" } }` — `holder` doesn't match the current owner; a release never trusts its caller, same rule every other destructive path in this project follows.
- **404** `{ "error": { "code": "ENOLOCK", "message": "no such lock: build" } }` — nothing by that name is held.
#### Sharing a workspace (server mode)
```http
GET /api/lanes/:id/shares → { shares: [{id, username, name, sharedAt}], canManage }
PUT /api/lanes/:id/shares {"userIds": [2, 5]} → { shares }
```
The users a workspace is shared with (`lane_shares`, names only). `PUT` takes the
WHOLE list — at most 200 enabled users, never the owner (`400 EBADUSER`) — from the
owner or an admin only (`403 ENOTOWNER`), same-origin, each change audited. Local
mode: `GET` answers an empty list, `PUT` `400 ELOCALMODE`. A user it is shared with
sees and works in it like its owner, but `PATCH {title}`, `reset`/`remove`/`purge`,
`DELETE` and `PUT …/shares` answer them `403 ENOTOWNER`, and a non-shell
`POST /api/lanes/:id/start` or `POST /api/run` `403 ECOLLABSTART`. Users taken off
get the WebSocket message `lane_access` `{laneId, userIds}` (sent to them only). See
`docs/SERVER-MODE.md` → Sharing a workspace.
#### Workspace context: instructions, folders, context files, memory
```http
+31 -1
View File
@@ -152,7 +152,8 @@ Workspaces made before server mode, or by the host account, have none.
- A session belongs to the workspace that owns it: `sessions.lane_id`, or the
workspace whose pane holds it. Its agents, events, tokens, cost, alerts,
snapshots and workflow runs belong to that session.
- A **developer** sees only rows of their own workspaces, everywhere: lists,
- A **developer** sees only rows of their own workspaces (and of workspaces
shared with them — see [Sharing a workspace](#sharing-a-workspace)), everywhere: lists,
counts, charts, cost, the Sessions/Events/Agents tables, exports, live
WebSocket updates and terminals. A request naming someone else's row (in the
path, the query or the body) gets `404 ENOTFOUND`, the same answer as a row
@@ -235,6 +236,35 @@ hides them for developers. The command and skill lists show a developer only the
own workspaces' project items: `?cwd=` outside one of them answers
`403 EFOLDER`, and with no `cwd` they list no project items at all.
### Sharing a workspace
A workspace's owner (or an admin) shares it with other users from its `⋯`
menu → **Share…** (`WorkspaceShareModal`, `GET/PUT /api/lanes/:id/shares`,
table `lane_shares`). There is one level: **collaborate**. A user it is shared
with sees the workspace, its sessions, Changes, history and Files like its
owner, joins its open terminals, types into them, commits and pushes, edits its
instructions and context — every row and live update, through the same
`scope.js` predicates (`canSeeLane`, `sessionWhere`, `laneWhere`,
`broadcastAudience`'s `viewers`). Only the owner or an admin renames, resets,
removes, deletes or shares it (`403 ENOTOWNER`), and for now a collaborator
starts no AI session in it (`403 ECOLLABSTART`; a plain terminal tab is fine) —
phase 2 of `docs/plans/workspace-sharing/` bills a collaborator's own sessions
to them. Their own figures stay their own: budgets and Today count only
workspaces they own (`ownedOnlySessionWhere`, `ownSessionWhere`).
**Under S3 a collaborator works as the OWNER's Linux account** — the folder
decides the account. They can read everything in the owner's home (the owner's
other workspaces, SSH keys, AI logins, the pane tokens of the owner's panes)
and their commits carry the owner's git identity; only AgentHub's `audit_log`
records who did what. The Share dialog says so. Share only with people you
would hand your keyboard to.
Every share and unshare is audited (`share workspace <id>` /
`unshare workspace <id>`). Taking it back sends the users taken off
`lane_access` (their list re-reads) and closes their open terminals on it at
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.
### 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: **self-reviewed 2026-11-06 — BLOCKED on finding R1 (below)**. Nothing here is built.
Status: **phase 1 built 2026-11-06** (workspace sharing; R1 decided as option **(a)** — accept and warn hard). Phases 2–4 not 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()`).