Skip to content

Commit 4865722

Browse files
Rowan Trollopeclaude
authored andcommitted
Add Volume + Workspace IA design plans
Two paired plans for the upcoming information architecture rework: the atomic file tree becomes a Volume; Workspace becomes a manifest composing one or more mounted volumes with per-volume permissions. Splits the work into a model plan (data, API, tokens, daemon, Redis migration) and a surfaces plan (CLI, UI, nav reshuffle, docs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6e2fa75 commit 4865722

2 files changed

Lines changed: 608 additions & 0 deletions

File tree

plans/volume-workspace-model.md

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# Volume + Workspace Data Model
2+
3+
Status: draft
4+
Owner: rowan
5+
Created: 2026-05-08
6+
Updated: 2026-05-08
7+
8+
Pair plan: [`volume-workspace-surfaces.md`](volume-workspace-surfaces.md) (CLI + UI rollout).
9+
10+
## Goal
11+
12+
Introduce a two-layer information model:
13+
14+
- **Volume** — one named, checkpointable, forkable file tree. Atomic unit of
15+
persistence and content history. Today's `WorkspaceMeta` renamed.
16+
- **Workspace** — a manifest composing one or more mounted volumes with
17+
per-volume permissions. Has no file content of its own. The natural unit
18+
of agent configuration.
19+
20+
This is the substrate the surfaces plan builds on.
21+
22+
## Scope
23+
24+
**In scope:**
25+
26+
- Go data model: rename `Workspace*` types to `Volume*`; introduce
27+
`Workspace*` (composition) types
28+
- Redis schema for volumes and workspace manifests
29+
- HTTP API: `/v2/volumes/...` and `/v2/workspaces/...`
30+
- Access token model: (scope, capability) two-axis design
31+
- Daemon: singleton-per-user-per-machine, multiplexes mount sessions
32+
- Workspace bookmarks (composition-level checkpoint references)
33+
- MCP backwards-compat layer for tool names and capability profile strings
34+
- Migration script + cutover plan for existing data and tokens
35+
36+
**Out of scope:**
37+
38+
- All user-facing surfaces (CLI, UI, copy, docs) — see surfaces plan
39+
- MCP wire identifier rename (`workspace_*` tools, `workspace-*` profile
40+
strings) — separate future plan
41+
- SDK rewrites beyond the minimum needed to back the new endpoints
42+
- Workspace templates (composition templates) — future
43+
44+
## Concepts
45+
46+
### Volume
47+
48+
- Same shape as today's `WorkspaceMeta` (`internal/controlplane/store.go:51`)
49+
- Owns its checkpoint timeline, manifest, blobs, fork lineage
50+
- Addressable by `(database_id, volume_id)`
51+
- Volume IDs prefixed `vol_` going forward
52+
53+
### Workspace
54+
55+
- A manifest of mount entries, ordered for deterministic application
56+
- Owned by a database; addressable by `(database_id, workspace_id)`
57+
- Addressable IDs prefixed `ws_` going forward
58+
- Carries no file content; all content flows through mounted volumes
59+
60+
### Mount entry
61+
62+
```
63+
{
64+
volume_id: string, // which volume to mount
65+
mount_path: string, // relative path under workspace root, e.g. "/skills"
66+
readonly: bool, // forces ro even if token would allow rw
67+
volume_token_id?: string // optional, for cross-owner volume access
68+
}
69+
```
70+
71+
Mount paths are *relative*. The absolute root is supplied by the client at
72+
mount time, so the same workspace works across machines.
73+
74+
### Mount conflict policy
75+
76+
- Same `(volume_id, mount_path)` mounted twice → idempotent no-op
77+
- Same volume at different paths → two independent sessions, allowed
78+
- Two volumes at same `mount_path` within one workspace → reject at API write
79+
- Overlapping paths within one workspace (`/a` and `/a/b` from different
80+
volumes) → reject at API write
81+
82+
## Access tokens
83+
84+
Two orthogonal axes.
85+
86+
### Scope
87+
88+
| Scope | Reaches |
89+
|---|---|
90+
| `volume:<id>` | Exactly one volume |
91+
| `workspace:<id>` | All volumes mounted by that workspace, with manifest readonly applied |
92+
| `database:<id>` | All volumes and workspaces in the database |
93+
| `control-plane` | Everything in the user's control plane |
94+
95+
### Capability
96+
97+
| Capability | Allows |
98+
|---|---|
99+
| `ro` | Read files, read checkpoints |
100+
| `rw` | `ro` + write/edit files |
101+
| `rw-checkpoint` | `rw` + create/restore volume checkpoints, create/restore workspace bookmarks |
102+
| `admin` | `rw-checkpoint` + manage volumes, workspaces, tokens |
103+
104+
### Rules
105+
106+
- A token is `(scope, capability, optional expiry)`
107+
- Capability is uniform across the scope
108+
- Per-volume `readonly` in a manifest is enforced *in addition to* the
109+
token capability — a workspace `rw` token cannot write to a volume mounted
110+
readonly
111+
- Cross-owner mounts: when a workspace mounts a volume not owned by the
112+
workspace owner, the manifest entry must reference a `volume_token_id`
113+
granting access. Removing/expiring that volume token invalidates the
114+
mount entry but leaves the workspace intact
115+
- Multi-token agents: agents needing mixed access across multiple workspaces
116+
hold multiple tokens. No compound policy in a single token
117+
118+
### Mapping from current MCP profiles
119+
120+
| Today | Tomorrow |
121+
|---|---|
122+
| `workspace-ro` | scope `volume:<id>`, capability `ro` |
123+
| `workspace-rw` | scope `volume:<id>`, capability `rw` |
124+
| `workspace-rw-checkpoint` | scope `volume:<id>`, capability `rw-checkpoint` |
125+
| `admin-ro` | scope `control-plane`, capability `ro` |
126+
| `admin-rw` | scope `control-plane`, capability `admin` |
127+
128+
The wire-level `workspace-*` profile strings remain valid identifiers in
129+
MCP for backwards compat. Internal Go switches to (scope, capability)
130+
tuples; a translation layer maps incoming legacy strings.
131+
132+
## Checkpoints
133+
134+
- **Volumes own checkpoint timelines.** No content history at the workspace
135+
layer.
136+
- A **workspace bookmark** is a named tuple
137+
`(workspace_id, name, [{volume_id: checkpoint_id}, ...])` capturing one
138+
head per mounted volume at bookmark time.
139+
- "Restore workspace bookmark X" iterates the recorded list and restores
140+
each volume to its referenced checkpoint. Atomic intent at the workspace
141+
level; canonical truth still at the volume.
142+
- Bookmarks soft-pin referenced checkpoints (volumes record back-references
143+
so checkpoint GC honors active bookmarks).
144+
145+
## Daemon model
146+
147+
- One daemon process per user per machine. Singleton; ref-counted lifecycle.
148+
- Daemon holds N mount sessions across any number of volumes and workspaces.
149+
- `afs vol mount` and `afs ws mount` register sessions with the same daemon.
150+
`ws mount` expands its manifest into N session registrations.
151+
- Per-session lease (existing model). On revocation/expiry, the affected
152+
session unmounts; other sessions continue.
153+
- Scaling: 100 mounts → 100 sessions in 1 process. Real cliffs are Redis
154+
pubsub fan-out and FUSE handle limits, not OS process count.
155+
- New ops surface: `afs daemon status`, `afs daemon stop` (see surfaces plan).
156+
157+
## API surface
158+
159+
New `/v2/` namespace. `/v1/workspaces` (current) keeps working through the
160+
deprecation window and reads identical data to `/v2/volumes` (same backing
161+
record).
162+
163+
```
164+
POST /v2/volumes
165+
GET /v2/volumes
166+
GET /v2/volumes/{id}
167+
DELETE /v2/volumes/{id}
168+
POST /v2/volumes/{id}:fork
169+
POST /v2/volumes/{id}:restore
170+
GET /v2/volumes/{id}/checkpoints
171+
POST /v2/volumes/{id}/checkpoints
172+
POST /v2/volumes/{id}/checkpoints/{name}:restore
173+
174+
POST /v2/workspaces
175+
GET /v2/workspaces
176+
GET /v2/workspaces/{id}
177+
DELETE /v2/workspaces/{id}
178+
PUT /v2/workspaces/{id}/mounts # bulk replace manifest
179+
POST /v2/workspaces/{id}/mounts # add one mount entry
180+
DELETE /v2/workspaces/{id}/mounts/{volume_id}
181+
GET /v2/workspaces/{id}/bookmarks
182+
POST /v2/workspaces/{id}/bookmarks
183+
POST /v2/workspaces/{id}/bookmarks/{name}:restore
184+
```
185+
186+
Token operations gain `scope` and `capability` fields; the legacy `profile`
187+
field is read-only and computed from (scope, capability) for compat.
188+
189+
## Redis schema
190+
191+
Today:
192+
193+
```
194+
afs:database:{db}:workspace:{id}
195+
afs:database:{db}:workspace:{id}:savepoints
196+
```
197+
198+
After:
199+
200+
```
201+
afs:database:{db}:volume:{id} # was workspace:{id}
202+
afs:database:{db}:volume:{id}:checkpoints
203+
afs:database:{db}:workspace:{id} # NEW shape: composition
204+
afs:database:{db}:workspace:{id}:bookmarks
205+
```
206+
207+
The `workspace:{id}` namespace is reused for the *new* concept. Migration
208+
moves existing data out of that key first.
209+
210+
## Migration
211+
212+
Breaking change at API and Redis-key level. Mitigated via:
213+
214+
1. **Versioned API.** `/v1/workspaces` continues during the deprecation
215+
window, served by a compat layer that reads from the new volume keys.
216+
`/v2` is authoritative.
217+
2. **One-shot Redis migration script.** For each existing workspace:
218+
- Copy `WorkspaceMeta` payload to a new `volume:{id}` key
219+
- Rewrite token records from `workspace:<id>` scope to `volume:<id>` scope
220+
- Leave the old `workspace:{id}` key in place for the duration of the
221+
deprecation window, then delete after cutover
222+
3. **No automatic workspace creation.** Migration does not auto-create a
223+
composition workspace per existing volume. Volumes work standalone via
224+
`afs vol mount` for users who don't need composition.
225+
4. **Token compat.** Existing MCP tokens preserve effective access. Their
226+
wire `profile` string is unchanged; a server-side mapping populates
227+
(scope, capability) on read.
228+
229+
## Phases
230+
231+
### Phase 1 — Internal types and storage (3–4 days)
232+
233+
- [ ] Rename `WorkspaceMeta``VolumeMeta` in `internal/controlplane/`
234+
- [ ] Rename `WorkspaceSessionRecord``VolumeSessionRecord` (sessions
235+
remain volume-bound; workspace sessions are aggregations)
236+
- [ ] Add `WorkspaceMeta` (new) carrying the manifest and bookmarks
237+
- [ ] Update Redis key constants; add new key shape for compositions
238+
- [ ] Update `internal/controlplane/store.go`, `service.go` accordingly
239+
- [ ] Backwards-compat read shim: `/v1/workspaces` reads from new volume
240+
keys without exposing the new shape
241+
242+
### Phase 2 — API endpoints (3–4 days)
243+
244+
- [ ] `/v2/volumes/...` endpoints (mostly direct rename of `/v1/workspaces`)
245+
- [ ] `/v2/workspaces/...` endpoints (NEW)
246+
- [ ] Manifest CRUD: PUT/POST/DELETE on `/v2/workspaces/{id}/mounts`
247+
- [ ] Bookmarks: list, create, restore
248+
- [ ] Validation: mount-path uniqueness, path overlap rejection,
249+
cross-owner volume_token_id enforcement
250+
- [ ] HTTP integration tests
251+
252+
### Phase 3 — Token model (2–3 days)
253+
254+
- [ ] Add `Scope` and `Capability` fields on `mcpAccessTokenRecord`
255+
- [ ] Compute `Profile` (legacy field) from (scope, capability) for compat
256+
- [ ] Token issuance API accepts new fields; old `profile` accepted for compat
257+
- [ ] Capability ladder enforcement in HTTP handlers and MCP tool gating
258+
- [ ] Manifest readonly enforcement (workspace token + volume mount readonly
259+
= effective ro)
260+
261+
### Phase 4 — Daemon multiplexing (4–5 days)
262+
263+
- [ ] Daemon refactor to host N concurrent mount sessions
264+
- [ ] Singleton lifecycle with reference counting
265+
- [ ] `afs daemon status` and `afs daemon stop` support endpoints
266+
- [ ] Lease watchdog per session; one session failure does not affect others
267+
- [ ] Stress test at 50 and 100 concurrent mounts
268+
269+
### Phase 5 — Migration (2–3 days)
270+
271+
- [ ] Redis migration script (idempotent; safe to rerun)
272+
- [ ] Token scope rewrite (`workspace:<id>``volume:<id>`)
273+
- [ ] Compat read layer for `/v1/workspaces`
274+
- [ ] Cutover runbook (staging → production)
275+
- [ ] Deprecation window: 90 days from launch, then `/v1/workspaces` 410s
276+
277+
### Phase 6 — MCP compat (1–2 days)
278+
279+
- [ ] Add `volume_*` tool aliases for `workspace_*` tools; both route to the
280+
same handlers against volumes
281+
- [ ] Add new `workspace_compose_*` tools (names tentative; review before
282+
merge): `_create`, `_mount_add`, `_mount_remove`, `_show`,
283+
`_bookmark_create`, `_bookmark_restore`
284+
- [ ] Profile-string ↔ (scope, capability) translation in
285+
`mcp_profiles.go`
286+
287+
## Open questions
288+
289+
- **Bookmark retention semantics.** Hard-pin or soft-pin? Recommend soft-pin
290+
with explicit warning when GC would orphan a bookmark.
291+
- **Workspace ownership transfer.** When a workspace is shared, are embedded
292+
volume_token_ids transferred? Recommend no — receiver supplies their own
293+
tokens for any volumes they don't already own.
294+
- **Empty workspaces.** Allowed? Recommend yes.
295+
- **Mount-path namespace policy.** Reject overlap (`/a` and `/a/b`)?
296+
Recommend yes; ship as a hard validation error.
297+
- **`/v1/workspaces` deprecation horizon.** Recommend 90 days. Confirm with
298+
ops/comms.
299+
- **Daemon location.** Per-user-per-machine vs per-machine. Recommend
300+
per-user — cleaner permission boundaries.

0 commit comments

Comments
 (0)