docs(plans): workspace sharing — open questions answered
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Workspace & session sharing — plan
|
||||
|
||||
Status: **draft, waiting for review** (2026-11-06). Nothing here is built.
|
||||
Status: **reviewed, open questions answered** (2026-11-06). 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()`).
|
||||
@@ -15,6 +15,11 @@ session. Decisions taken with the repo owner (2026-11-06):
|
||||
| What may a person it is shared with do? | **Always collaborate** — the same as the owner inside that workspace/session: type into panes, send prompts, start sessions, commit, push. No view-only level. |
|
||||
| Unit of sharing | **Both** — a whole workspace (every session, present and future) or a single session. |
|
||||
| Who pays for a session a collaborator starts? | **The person who opens it** — its tokens, cost and spend quota are theirs, not the workspace owner's. |
|
||||
| Whose AI subscription does a collaborator's run use? | **Their own, or none**: only a pool account of kind `token` that the collaborator may use (passed by env). With a `login`-kind pool or no pool the run is **refused** (`ENOCOLLABACCOUNT`) — a collaborator never spends the owner's subscription. |
|
||||
| Spend quota | **Only the payer's** (the person who started or resumed the run). The owner's quota is not involved. |
|
||||
| A collaborator resumes someone else's session | **The resumer pays from then on** — billing is per RUN, not per session (below). |
|
||||
| Session-only share and git | **Allowed** — commit/push in that session's tree, like everything else. |
|
||||
| Sharing with an analyst | **Allowed** — a shared workspace counts as their own for writes. |
|
||||
|
||||
## The one fact everything else follows from (S3)
|
||||
|
||||
@@ -93,12 +98,30 @@ everything else becomes "owner **or** collaborator".
|
||||
- `spawnRun` takes `startedBy = req.user.id`; writes `dashboard_runs.started_by`.
|
||||
- `bindRunSession` (`server/routes/hooks.js`) stamps `sessions.owner_id = started_by` when it differs from the lane owner. `sessions.owner_id` already exists (remote clients) and `scope.js`, `session-account.js`, `users.js` honour it.
|
||||
- `server/routes/users.js` ownership CTE: `sessions.owner_id` must WIN over the lane owner (today it is a UNION — a session could count twice). Same in `user-quota.js` spend (`hourlyTokenRows` + `calculateCost`, never a second formula).
|
||||
- Spend quota: `assertSpawnAllowed(startedBy)` instead of the lane owner, and `hook-handler.js`'s `UserPromptSubmit` check reads the flag of the session's billed user (the quota file is per Linux account today → becomes per account + billed user id; see open question 2).
|
||||
- A resume by a collaborator: billed to the session's existing owner (the opener stays the payer), unless decided otherwise (open question 3).
|
||||
3. **AI login of a collaborator's session.** Which subscription is actually
|
||||
consumed is separate from who is billed in AgentHub:
|
||||
- Pool account of kind `token`: passed as `CLAUDE_CODE_OAUTH_TOKEN` through tmux `-e` / the launch file, so the collaborator's **own** pool pick (`pickAccount(startedBy)`) is used — real separation.
|
||||
- Pool account of kind `login` (or no pool): the login lives in the owner's `~/.claude/.credentials.json`; a second login cannot be placed there while the owner's panes run (existing pool rule). The session then runs on the **owner's** subscription quota, only billed to the collaborator in AgentHub. The session row shows this.
|
||||
- Spend quota: `assertSpawnAllowed(startedBy)` instead of the lane owner, and `hook-handler.js`'s `UserPromptSubmit` check reads the flag of the RUN's payer (the quota file is per Linux account today → per account + payer user id, passed to the pane as `AGENTHUB_PAYER_ID`).
|
||||
- **Billing is per run, not per session.** A session can be paid by several
|
||||
people over time: whoever starts or resumes a run pays for everything that
|
||||
run does, including what anyone types into its live pane (keystrokes cannot
|
||||
be attributed; the run's starter can). New table
|
||||
`session_payers(session_id, user_id, from_at)` — one row per run start whose
|
||||
payer differs from the previous one. `hourlyTokenRows` books a session's
|
||||
tokens per UTC hour already; each hour goes to the payer whose `from_at` is
|
||||
the latest ≤ that hour (a run started mid-hour takes the hour from the next
|
||||
one on — documented rounding, never a second cost formula). With no row the
|
||||
session's owner pays, as today. `sessions.owner_id` keeps meaning "who
|
||||
opened it" (visibility); `session_payers` only moves money.
|
||||
3. **AI login of a collaborator's run — their own or refused.**
|
||||
- The collaborator's pool pick (`pickAccount(startedBy)`, their group, pin
|
||||
and own choice) must be of kind `token`: it goes in as
|
||||
`CLAUDE_CODE_OAUTH_TOKEN` through tmux `-e` / the launch file, overriding
|
||||
the owner's home login for that pane only.
|
||||
- A `login`-kind pick, or no pool account for them: `spawnRun` refuses with
|
||||
`409 ENOCOLLABACCOUNT` before anything starts, for a new session AND for a
|
||||
resume. The home's Start button says why. The owner's login is never used
|
||||
for a collaborator's run.
|
||||
- Codex / other tools: same rule with their own env-only kinds (Codex API key
|
||||
`AGENTHUB_POOL_KEY`, Cursor `CURSOR_API_KEY`); a ChatGPT login (written into
|
||||
the home as `auth.json`) is refused like a Claude `login`.
|
||||
4. **Pane token.** A collaborator's session gets a pane token of the
|
||||
collaborator, **scoped to that lane**: `dashboard_runs.pane_token_hash` plus a
|
||||
`scope_lane_id`, and `userFromPaneToken` refuses anything outside that lane.
|
||||
@@ -146,9 +169,10 @@ collaborator"), `README.md` feature list.
|
||||
1. **S1 — Workspace sharing, visibility + live panes.** Tables, scope.js,
|
||||
broadcastAudience, Share dialog, sidebar group, attach/type, git writes.
|
||||
Collaborators can join live panes but cannot start sessions yet.
|
||||
2. **S2 — Collaborator sessions billed to the opener.** `started_by`,
|
||||
`sessions.owner_id` stamping, users.js / quota precedence, pane token scope,
|
||||
pool `token` env.
|
||||
2. **S2 — Collaborator runs on their own account, billed per run.**
|
||||
`started_by`, `sessions.owner_id` stamping, `session_payers`, users.js /
|
||||
quota precedence (and the double count fixed), pane token scope, pool
|
||||
`token` env, `ENOCOLLABACCOUNT`.
|
||||
3. **S3 — Session-only sharing.** `session_shares`, its scope rules and UI.
|
||||
4. **S4 — Presence + audit of prompts.**
|
||||
|
||||
@@ -157,18 +181,21 @@ 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.
|
||||
|
||||
## Open questions
|
||||
## Answered questions (2026-11-06)
|
||||
|
||||
1. **Which subscription does a collaborator's session use** when the pool is
|
||||
`login`-kind (or empty)? Proposal: the owner's, shown on the session; the
|
||||
alternative (refuse a collaborator session without a `token` pool account) is
|
||||
stricter.
|
||||
2. **Spend quota of a collaborator inside someone else's workspace** — enforce
|
||||
the collaborator's own quota (proposal) or both theirs and the owner's?
|
||||
3. **A collaborator resumes the owner's ended session** — billed to the session's
|
||||
owner (proposal) or to whoever resumes?
|
||||
4. **Session-only share and git**: may a session collaborator commit/push, or
|
||||
only use the Changes panel read-only? (The table above says commit is
|
||||
allowed, following "always collaborate".)
|
||||
5. **Analysts**: does sharing to an analyst let them write in that workspace
|
||||
(proposal: yes — it becomes "their own" for writes)?
|
||||
1. A collaborator's run uses **their own `token`-kind pool account, or is
|
||||
refused** (`ENOCOLLABACCOUNT`) — never the owner's subscription.
|
||||
2. Spend quota: **only the payer's**.
|
||||
3. Resume by someone else: **the resumer pays from then on** (per-run billing,
|
||||
`session_payers`).
|
||||
4. Session-only share: **commit/push allowed**.
|
||||
5. Analysts: **sharing gives write** in that workspace.
|
||||
|
||||
## Still to settle while building
|
||||
|
||||
- The per-hour rounding of `session_payers` (a run started at 10:40 by Bob in a
|
||||
session Alice paid at 10:05): the hour goes to the earlier payer and Bob pays
|
||||
from 11:00, or token rows get a finer key. Phase 2 measures how often it
|
||||
matters before adding a finer key.
|
||||
- Typing into a live pane started by someone else costs the run's starter. The
|
||||
pane's header shows "billed to <name>" so nobody is surprised.
|
||||
|
||||
Reference in New Issue
Block a user