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:
+17
@@ -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
@@ -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,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()`).
|
||||
|
||||
Reference in New Issue
Block a user