From 22a40b9eaabb72e3b77d3dc28f6e6e8f24b053b7 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 16:06:24 -0400 Subject: [PATCH 1/7] docs(runtime): P2 persistent-session-volume + VirtualFS seam detailing record (RIG-2395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detailing pass for task **P2** of the [compass-elastic-session-runtime](docs/designs/infra/runtime/compass-elastic-session-runtime/design.md) parent (RIG-1717, frozen PR #446), incorporating the [VirtualFS descope amendment](docs/designs/infra/runtime/compass-elastic-session-runtime/virtualfs-descope-amendment.md) (RIG-2395, PR #459) that moved the `VirtualFS` source-of-tree seam, the `WorkspaceSource` variant, and the provision-materialize wiring from S1 into P2 and deferred the clone-model / clone-credential-location decision to this record. ### What it designs - A **per-session persistent volume** (working tree + derived state — `target/`, `node_modules`, caches) that survives suspend/resume/eviction and mounts at a stable absolute in-container path on every launch. Local-directory subtree with session→box stickiness, box-loss-as-accepted-degradation, and a 14-day-after-close expiry reaper. - The **`VirtualFS` source-of-tree seam** (`go/internal/vfs`, created fresh — the S1 draft was closed by the descope), mirroring the shipped `go/internal/compute` layering: `Materialize`/`Release`, a `TreeSource` selecting checkout | sparse | snapshot | customer-mount, destination-as-binding-state, and reserved-not-implemented `Archive`/`Restore` on the sentinel discipline. - A **six-task plan** (W1–W6): volume lifecycle API + local-dir backend → snapshot backends (reflink + rsync) with a repo→snapshot index → the `VirtualFS` seam → the `WorkspaceSource` variant + mount doc-amendment → provision/teardown wiring → expiry reaper + the parent-mandated acceptance suite. ### The load-bearing decision (OQ-1) The one load-bearing open question — **who clones, and where the clone-read credential lives** — is presented for ratification as a single package: **1a** the clone posture (recommended **A**, keep the agent self-clone, zero new credential class; vs B host-side clone, vs C host-side mirror) and **1b** the snapshot-amortization provenance mechanism under A (recommended: snapshot only a provably-clean post-clone tree via a clone-complete signal, keyed in a repo→snapshot index). The record is drafted against A + provenance-(a); Matt ratifies the package. This draft was red-teamed by an adversarial design-critic pass before review (per `skill://design`): one genuine fork (the 1b provenance mechanism) folded into OQ-1's ruling package, four clear improvements folded into the record (Option C alternative, close-stamp race/crash/suspend hardening, seam post-condition + path-triple ownership, determinate W6 probes + snapshot-store retention). Spec-impact: none Ledger-impact: none Refs RIG-2395 Co-authored-by: Matt Wilkinson --- .../p2-persistent-session-volume.md | 681 ++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md new file mode 100644 index 00000000..7d65a8e9 --- /dev/null +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -0,0 +1,681 @@ +# P2 — Persistent Session Volume + VirtualFS Seam + +> **Design record.** Detailing pass for task **P2** of +> [compass-elastic-session-runtime](./design.md) (RIG-1717, frozen in PR #446; +> its Plan § P2, design.md:603-653), incorporating the +> [VirtualFS descope amendment](./virtualfs-descope-amendment.md) (RIG-2395, +> ruled by Matt 2026-08-19, PR #459), which moved the `VirtualFS` +> source-of-tree seam, the `WorkspaceSource` variant, and the provision- +> materialize wiring from S1 into P2 and deferred the clone-model / +> clone-credential-location decision to this record +> (virtualfs-descope-amendment.md:57-72,116-129). Where this record and the +> frozen parent disagree, the amendment governs. Every `go/internal/*` +> citation is a path in the **`RigelBuild/compass`** monorepo at main +> `17111cc0` (line numbers drift; resolve against that commit). + +Status: PROPOSED — details P2 under the frozen parent + the active amendment. +Tracking: RIG-2395 + +Ledger impact: none intended. The clone/credential fork (OQ-1) is +load-bearing and, once ruled, may warrant a ledger row beside DL-052 (it is a +credential-**location** decision DL-052 does not speak to, +virtualfs-descope-amendment.md:62-67); that row lands with the ruling, not +with this draft. + +## Problem / Intent + +A session's working tree and derived state (`target/`, `node_modules`, build +caches) must live on a **per-session persistent volume** that survives +suspend / resume / eviction and mounts at a **stable absolute path** on every +launch — today both die with the container, because the tree exists only +inside it (the agent self-clones post-launch into a container-local dir, +`go/internal/runtime/agent.go:354-358`). P2 builds that volume and its +lifecycle API, plus the `VirtualFS` source-of-tree seam that materializes the +tree onto it (descoped here from S1), and resolves the load-bearing decision +the amendment deferred: **who clones, and where the clone-read credential +lives**. + +## Approach + +### Code reality this record grounds on + +The tree today is wholly container-internal, written by the agent itself: + +- `AgentRuntime.Launch` "creates + starts the container, arms egress as root, + installs scoped credentials, and creates the checkout dir as the agent + user" (`go/internal/runtime/agent.go:173-177`); the checkout dir is an + **empty** `mkdir -p` — "creates the in-container checkout directory as the + agent user, so an agent that self-clones post-launch has an owned working + dir" (`ensureCheckoutDir`, `go/internal/runtime/agent.go:354-358`). +- The self-clone model is a documented package invariant: "Each agent gets + its own full git clone, created inside the container — not a shared + checkout and not a host worktree … The clone's credentials live in the + agent's $HOME/.gitconfig credential helper, never in the workspace + .git/config" (`go/internal/runtime/workspace.go:1-8`); `Workspace.CheckoutDir` + is "the absolute path inside the container where the agent's checkout dir + is created (the agent self-clones into it post-launch)" + (`go/internal/runtime/workspace.go:44-47`). +- The agent's clone credential is a per-agent machine-user token installed + into its scoped `$HOME` via `CredentialSetupScript` — "the token lives in + the agent's $HOME, never the workspace .git/config" + (`go/internal/runtime/workspace.go:58-62`) — fed over stdin, never argv + (`workspace.go:64-66`), the same 0600/stdin discipline as `WriteAgentFile` + ("the body is fed over stdin, never argv … the file lands 0600 under umask + 077", `go/internal/runtime/agent.go:238-244`). +- The self-clone happens *under* an already-armed default-deny egress + firewall: `Launch` arms egress before the agent runs, and `NftScript`'s + base ruleset fails closed — "`set -eu` makes the base ruleset fail closed — + if any table / set / chain / policy-drop rule fails to install, the script + aborts non-zero and the caller tears the container down rather than + running it unfirewalled" (`go/internal/runtime/egress.go:76-83`, the + script builder at `:87-107`). Any Runner-side clone (Option B below) runs + *outside* this per-session firewall — a posture difference the fork must + weigh, not just a credential one. +- Host mounts already carry a per-mount writability bit: `Mount` is + "a host→container bind mount. ReadOnly maps to :ro" with + `ReadOnly bool` (`go/internal/runtime/podman.go:60-66`; the `:ro,Z` vs `:Z` + suffix at `podman.go:846-850`). Only the *doc contract* on + `AgentSpec.Mounts` says "Mounts is read-only host mounts (e.g. a host cache + mounted read-only)" (`go/internal/runtime/agent.go:43-44`) — a comment, not + a shape. +- `SpecBuilder` "maps a provision request to a complete runtime.AgentSpec — + the image, per-agent workspace, and egress policy" + (`go/internal/runner/host.go:40-48`) — the one seam where the volume mount + and `WorkspaceSource` derivation land. +- The resume contract the volume composes with: `COMPASS_RESUME_SESSION_FILE` + "is the absolute in-container path of a server-reconstructed session file + the agent loads to resume" (`go/internal/runner/agent_exec.go:40-42`, field + `:65-67`, exported at `:92-94`). Suspend/resume durability = transcript + + volume (parent §Spine 4, design.md:123-128). +- `go/internal/vfs` **does not exist** on this checkout (verified by glob — + no such directory). The descope removed the S1 draft (PR #456 closed, + virtualfs-descope-amendment.md:144); P2 creates the package fresh. +- The seam style to mirror is the shipped `go/internal/compute` package: a + doc-commented layering ("compute.go — the ComputeRuntime interface plus the + value types that cross it … Every consumer depends on the interface, so a + … backend can replace the S1 one without touching a caller", + `go/internal/compute/compute.go:10-14`), session-scoped construction ("a + backend is constructed with the session's container handle, its + container-runtime engine, and its egress policy … without threading it + through every Exec call", `compute.go:92-96`), and reserved-not-implemented + surface returning an honest sentinel (`compute.go:26-29,106-111`; the + `Resize` precedent, `go/internal/runtime/podman.go:387-396`). + +### The volume: a local directory subtree with session→box stickiness + +As the parent pins (design.md:609-617): the volume is a **directory subtree +on the box's fast local storage**, NOT network block storage. Vendor-neutral +(Global Constraint 1), no storage fabric, matches today's single-box +deployments; the accepted tradeoff — a burst cannot land on a different box +until a network-volume backend exists — is parent OQ 2 and is not re-opened +here. The lifecycle (create / attach / reattach / snapshot / archive / +restore / expire) is owned Runner-side beside the container lifecycle, in a +new `go/internal/vfs` package. + +Concretely: + +- **Layout.** A volume root under an operator-configured base dir (default + under the Runner's state dir), one subtree per session keyed by session id. + `Attach` bind-mounts it into the container at the **stable absolute + in-container path** (the same path on every launch and on every burst + environment — the invariant that keeps `target/` and sccache valid, parent + design.md:297-299). The in-container path becomes `Workspace.CheckoutDir`'s + parent; the host side rides `AgentSpec.Mounts` with `ReadOnly: false`. +- **Stickiness.** While a volume lives, the session (and its C3 bursts) + relaunch on the volume's box. In the OSS single-box Runner this is + trivially true (there is one box); the invariant P2 encodes is *box-local*: + a Runner never attaches a volume it does not host, and `Attach` of an + absent volume is an error, never a silent recreate. Multi-box placement is + a control-plane concern outside this repo's scope (OQ-4). +- **Derived state always persists** (Global Constraint 5): `target/`, + `node_modules`, caches live on the volume because the *whole working + subtree* is the volume — there is no include-list to maintain. Eviction + reclaims compute, never the volume; the volume dies only by `Expire` + policy. +- **Expiry**: default **14 days after session close**, tunable per + deployment; M0's working-set-GB distribution calibrates the value (parent + design.md:618-621, OQ 3 — not re-opened). `Expire` reaps only volumes whose + session is closed AND whose close-stamp is past the deadline; a live or + suspended session's volume is never eligible. +- **Snapshots for first-clone amortization** (the Codespaces prebuild model, + parent design.md:622-626): a freshly materialized, **provably-clean** tree + is snapshotted at the FS layer where the box's filesystem supports + reflink/snapshot (btrfs/XFS/bcachefs reflink copy), by rsync-clone + otherwise; a later session on the same repo restores the snapshot and starts + warm on `git fetch` + checkout-delta instead of a full clone. *What* gets + snapshotted, *when* (the clean-tree stamp point), and the repo→snapshot + index the provision path reads are the load-bearing sub-fork OQ-1b — the + amortization mechanism, not just the clone posture, is what Matt ratifies. + Detection of the copy primitive is a runtime capability probe, not a config + knob (OQ-3). `VolumeSnapshotID` stays an **opaque string** (frozen so by the + parent, design.md:536-537); its P2 production shape is the snapshot store's + key, never parsed by callers. +- **sccache is a cross-session complement, not a replacement** — incompatible + with incremental compilation (`CARGO_INCREMENTAL=0`), never caches linking, + path-sensitive; a warm `target/` is strictly more; run both (parent + design.md:627-629). P2 carries no sccache code; the stable-path invariant + is what keeps it viable. +- **Box loss is an accepted degradation** (vs eviction): losing the box loses + the volume; the session resumes from the transcript + (`COMPASS_RESUME_SESSION_FILE`) and re-materializes through `VirtualFS`, + paying one cold clone + cold build (parent design.md:630-633). `Attach` on + a lost volume returns a typed not-found the provision path converts into a + fresh `CreateVolume` + cold materialize — an error-shaped signal, never a + silent recreate, so the cold path is observable. + +### The `VirtualFS` seam + +Created in `go/internal/vfs`, mirroring the `compute` package's shape (doc- +commented file layering, interface + crossing value types in one file, +backends in siblings, session-scoped construction): + +- `Materialize(ctx context.Context, src TreeSource) (root string, err error)` + and `Release(ctx context.Context, root string) error`. +- `TreeSource{Repo string, Ref string, Sparse []string, Snapshot VolumeSnapshotID, CustomerMount string}` + selects checkout | sparse-checkout | volume snapshot | mounted customer VFS + (sparse-checkout is a parameter of the checkout backend, not a second + backend — parent design.md:516-518). +- **The destination is binding state of the `VirtualFS` instance** — + constructed with the target root, which at P2 is the session's attached + volume — not a `Materialize` parameter (parent design.md:533-536), so a + later destination (a customer-VFS interop root) swaps behind the frozen + signature. +- `Release` detaches/cleans the materialized root without destroying volume + contents (volume destruction is `Expire`'s, never `Release`'s). + +Under the recommended clone model (next section), the P2 checkout backend's +`Materialize` **prepares** the destination — snapshot-restore when +`TreeSource.Snapshot` is set, else an empty owned root — and the agent +completes the tree (clone or fetch-delta) in-container. If OQ-1 is ruled the +other way, the same signature holds and the backend performs the full +host-side clone; the seam shape is decision-proof, which is exactly why the +amendment let it wait for P2. + +### The clone/credential fork — the record's central decision (OQ-1) + +The amendment defers to P2 "who clones, and where the clone (read) +credential lives" (virtualfs-descope-amendment.md:66-72). DL-052 governs the +**write** credential only: "Only the Server holds forge write credentials, as +a `server_only` declared secret filtered out of container injection … The +agent keeps a push-scoped git credential" (`docs/designs/DECISIONS.md:83`; +the parent cites it as `docs/designs/product/DECISIONS.md:75` — the file +lives at `docs/designs/DECISIONS.md` on this checkout). The decision here +must be *consistent with* that posture, not governed by it. + +**Option A — keep the agent self-clone (recommended).** The Runner +creates/attaches the volume and mounts it writable at the stable path; +`Materialize` prepares it (empty, or snapshot-restored host-side); the agent +clones/fetches into it with the machine-user token already in its scoped +`$HOME` (`workspace.go:58-62`), exactly today's model +(`ensureCheckoutDir`, `agent.go:354-358`) with the destination moved from a +container-local dir onto the volume. + +- *Credential blast radius: zero delta.* No new credential class; no forge + token ever exists host-side; the DL-052 split (Server = write, + agent = push-scoped in-container) is untouched. +- *Global Constraint 8: trivially green.* The launch path changes only in + where the checkout dir lives (a mount + a `CheckoutDir` value); the clone + mechanics, credential install, and egress arming are byte-identical. +- *Snapshot amortization needs a clean stamp point (OQ-1b).* The volume is a + host directory — host-visible regardless of who wrote it — so the Runner + *can* snapshot it even though the clone happened in-container. But under A + there is no automatic clean snapshot point: `Materialize` returns before any + tree bytes exist (the agent clones asynchronously post-launch, + `agent.go:354-358`), so a naive "snapshot after materialize" captures an + empty root, and a "snapshot at session close" captures that session's + uncommitted WIP and untracked non-ignored files — which a `git fetch` + + checkout-delta restore does **not** remove, leaking one session's stray + files into a stranger's tree (the very isolation `workspace.go:1-8` exists to + hold, file contents in place of tokens). So A's amortization is sound only + with a designed provenance mechanism: the recommended one snapshots a + **provably-clean** post-clone tree (an agent→Runner clone-complete signal + plus a `git status`-clean check), keyed in a repo→snapshot index (W2) the + provision path reads. This is the load-bearing sub-fork **OQ-1b**; it rides + A's ruling because "A amortizes fine" is load-bearing in A's own + cost/benefit. +- *Cost:* `Materialize` on the fresh-clone path is preparation, not + tree-writing — a deliberate, documented reading of Global Constraint 2 + ("every working-tree materialization goes through `VirtualFS`", + design.md:423-428): the seam owns the destination and the source-selection; + the tree bytes on the cold path are written by the agent under the seam's + contract. Recorded in OQ-1 so Matt ratifies the reading, not just the + option. + +**Option B — move cloning Runner-side.** `Materialize` performs a full +host-side clone onto the volume before container start; the container gets a +ready tree. + +- *Gain:* `VirtualFS` becomes the literal materializer (Global Constraint 2 + reads plainly); the prebuild model gets a host-side materialization point + that can snapshot *before* any container exists (a true Codespaces + prebuild service — build the snapshot with no session attached); cold + start-up drops one in-container step. +- *Cost:* introduces a **host-side forge read credential** — a new credential + class living on every Runner box, distinct from both DL-052's Server-only + write secret and the in-container agent token. It needs provisioning, + rotation, scoping (read-only, but to *which* repos?), and it widens the + host's blast radius: a Runner-host compromise today yields no forge + credential; under B it yields org-wide read. It also perturbs the launch + path ordering (clone before create/start) against Global Constraint 8, and + the private-mirror/file:// cases ("a file:// clone of a local mirror needs + none", `workspace.go:68-69`) need re-plumbing host-side. And the clone + itself moves outside the session's fail-closed egress firewall + (`egress.go:76-83`) into the Runner host's own network posture — Global + Constraint 4's "egress … session-scoped" gets a host-side carve-out to + justify. + +**Recommendation: Option A.** The one capability B uniquely adds — +sessionless prebuild snapshots — is not needed by any P2/C3/D4/E5 task (the +snapshot consumers are all sessions, and A amortizes their cross-session +snapshots via OQ-1b's clean-stamp mechanism), and B's price is a standing new +credential surface plus launch-path churn. A is reversible: if a prebuild +*service* ever materializes trees with no session, that is the moment a +host-side read credential earns its existence, and the `Materialize` signature +already accommodates it. The whole fork is **OQ-1, load-bearing** — its clone +posture (1a) and snapshot provenance (1b) are ratified together; the caller +takes it to Matt, the Plan below sequences the gated tasks after the ruling, +and this record drafts against A + provenance-(a). + +### `WorkspaceSource` and the provision wiring + +`AgentSpec`/`Workspace` gain the variant the parent reserved +(design.md:246-251): a `WorkspaceSource` discriminating today's +container-local clone-dir from the volume-backed workspace, derived by +`SpecBuilder` (`go/internal/runner/host.go:46-48`) from the provision +request. The clone-based path stays intact and default (Global Constraint 8); +volume-backed is opt-in per deployment until E5 validates it. The provision +flow under volume-backed: resolve-or-create volume → `Attach` → `Materialize` +(snapshot or prepare-empty) → append the writable mount to `AgentSpec.Mounts` +→ `Launch` unchanged. Teardown stops/removes the container +(`AgentRuntime.Teardown`, `agent.go:216-236`) and never touches the volume. + +### Mounts doc-contract amendment + +`AgentSpec.Mounts`'s comment — "Mounts is read-only host mounts" +(`agent.go:43-44`) — is amended to "host mounts (read-only caches, and the +writable session volume at P2)". This is a **doc-comment change, not a shape +change**: `Mount.ReadOnly` is already a per-mount bool +(`podman.go:62-66`) and the `:ro` suffix is already conditional +(`podman.go:846-850`). Recorded as a P2-specific Global Constraint so no +reviewer treats the writable mount as a contract violation. + +## Alternatives considered + +- **Network block storage as the P2 volume backend.** Rejected by the parent + (design.md:609-617, OQ 2): not vendor-neutral without a storage fabric, + unneeded for same-box burst. Not re-opened; the lifecycle API is shaped so + a network backend can slot behind it later. +- **Whole-`$HOME` on the volume.** The scoped `$HOME` (credentials, + `.gitconfig`, agent config) stays container-local and is reinstalled by + `Launch`, per the parent's D4 resume analysis ("the scoped `$HOME` is + container-local, reinstalled by `Launch`, not on the volume", + design.md:730-735). Persisting it would put a live forge token on the + host's disk — worse credential posture for zero P2 benefit. +- **Content-addressed snapshot store / dedup across sessions.** Rejected with + the parent's content-addressed-VFS alternative (design.md:387-401); + snapshots here are dumb FS-level copies keyed by opaque id. +- **Ruling the clone fork inside this record.** Rejected — it is exactly the + decision the amendment marked load-bearing for Matt + (virtualfs-descope-amendment.md:123-129); this record recommends and + sequences around it (OQ-1) rather than deciding it. +- **Option C — a host-side read-only bare mirror + in-container `file://` + clone.** The codebase names this pattern: `mountArg`'s doc calls the + read-only mount "the shared bare-repo cache" (`podman.go:842-844`), + `AgentSpec.Mounts`'s doc example is "a host cache mounted read-only" + (`agent.go:43-44`), and `CredentialSetupScript` already returns `("", nil)` + for "a file:// clone of a local mirror" (`workspace.go:68-69`) — so the agent + clones with **no credential**. Rejected as non-dominant: someone must + populate and refresh the mirror from the forge, which either puts the same + host-side forge READ credential Option B needs on the box (merely narrowed to + fetch-into-mirror — a real narrowing, not credential-free) or lets agents + write a shared host cache other sessions read (a cross-session write channel + where one compromised agent poisons objects a stranger clones — worse than A + or B, and against `workspace.go:1-8`'s one-agent-one-clone isolation). C thus + collapses to "B with a smaller blast radius." Recorded because a reviewer or + executor will see the mirror pattern in the code and ask, and because the + fetch-scoped-vs-clone-scoped credential narrowing is a genuine input to + Matt's OQ-1 ruling. + +## Global Constraints + +**Inherited: all nine of the parent's** (design.md:416-467) apply unmodified +— vendor neutrality (1), go-through-the-seams (2, a P2-onward constraint per +the amendment, virtualfs-descope-amendment.md:111-114), fail-closed routing +(3), security floors in every environment (4), derived state always persists +(5), runtime-agnostic substrate (6), sequencing after Dogfood (7), existing +session path stays green (8), version floors (9). P2-specific additions: + +- **P2-GC-a — Mounts doc amendment.** The writable session volume rides + `AgentSpec.Mounts` with `ReadOnly: false`; P2 updates the field's doc + comment (`agent.go:43-44`), never the `Mount` shape (`podman.go:62-66`). +- **P2-GC-b — no new credential surface without the OQ-1 ruling.** Until Matt + rules the clone fork, no task may introduce a host-side forge credential; + the drafted-against Option A introduces none ever. +- **P2-GC-c — volume destruction only via `Expire`.** `Release`, `Teardown`, + eviction, crash, and failed launches never delete volume contents; the only + reclaim path is the policy reaper (the parent's GC 5 made mechanical). +- **P2-GC-d — stable-path invariant.** The in-container mount path of a + session's volume is identical across every launch, resume, and burst + environment of that session; a path change is a breaking bug (it + invalidates `target/` and sccache). +- **P2-GC-e — clone-based path stays the default.** Volume-backed + `WorkspaceSource` is opt-in until E5; the existing container-local + self-clone path keeps its regression suite green at every increment + (GC 8 made concrete for P2). + +## Plan + +Six implementation tasks (W1–W6). W1 is foundational; W3's checkout-backend +semantics and W5's wiring are **gated on the OQ-1 ruling** (they are drafted +against Option A and change little under B — the gate is about not merging a +credential posture Matt has not ruled). Hermetic unless noted. + +### W1 — volume lifecycle API + local-dir backend (`go/internal/vfs`) + +The package skeleton mirrors `go/internal/compute`'s layering +(`compute.go:10-24`): `vfs.go` holds the interfaces + crossing value types, +`localvolume.go` the local-dir backend, `snapshot_*.go` the W2 backends. + +- **Interfaces:** + + ```go + package vfs + + // Volume is a live per-session persistent volume: the session it belongs + // to and its host-side root. Opaque to callers beyond these fields. + type Volume struct { + SessionID string + HostRoot string + } + + // VolumeSnapshotID is the opaque key of a stored volume snapshot (frozen + // opaque by the parent record; never parsed by callers). + type VolumeSnapshotID string + + // ArchiveRef is the opaque reference to an archived volume in the + // object store (consumed by D4's cold-idle; signatures frozen here, + // implementation deferred — see OQ-2). + type ArchiveRef string + + // VolumeManager owns the session-volume lifecycle Runner-side, beside the + // container lifecycle. Constructed with the operator-configured base dir. + type VolumeManager interface { + CreateVolume(ctx context.Context, sessionID string) (Volume, error) + // Attach makes the volume available for mounting and returns its + // host path; attaching an absent volume returns ErrVolumeNotFound + // (typed — the provision path converts it into the cold path). + Attach(ctx context.Context, v Volume) (path string, err error) + Snapshot(ctx context.Context, v Volume) (VolumeSnapshotID, error) + Archive(ctx context.Context, v Volume) (ArchiveRef, error) + Restore(ctx context.Context, ref ArchiveRef) (Volume, error) + // Expire reaps volumes whose session is closed and whose close-stamp + // is older than olderThan. Never touches live/suspended sessions. + Expire(ctx context.Context, olderThan time.Duration) error + } + ``` + + `Archive`/`Restore` are **reserved-not-implemented** at P2 (honest + sentinel, the `ErrExecStreamingNotImplemented` discipline, + `compute.go:26-29`; see OQ-2). **Close-stamp mechanism** (the leak the + parent's 14-day policy exists to bound, design.md:619-621, made robust): a + marker file in the volume root's metadata dir, written by the teardown path + and read by `Expire`, with three invariants the W1 backend holds — + (a) **`Attach` atomically clears the stamp**, so a reopened + closed-but-unexpired session never carries a past-deadline stamp into its + new life; (b) **`Expire` takes a per-volume lock and re-verifies eligibility + under it**, so a volume is never reaped in the window between the reaper + reading its stamp and a concurrent `Attach`; (c) the stamp carries + **close-vs-suspend intent** (a suspended session is *not* eligible however + old — D4's suspend uses the same stop+remove teardown path, + design.md:714-716, so the intent bit comes from the caller, not inferred + from "container gone"). A crash between container-remove and stamp-write + leaves an **unstamped** closed volume; the startup pass reconciles unstamped + volumes against the authoritative session state rather than trusting the + marker alone, so a crash fails *safe* (eventually reaped) not *open* (never + reaped). +- **Depends:** nothing (first P2 code task). +- **Test cycle:** hermetic over tempdirs — create/attach round-trip returns + a stable path; re-attach after simulated Runner restart returns the same + path; attach-absent returns the typed not-found; **attach clears a + past-deadline stamp**; `Expire` reaps only closed-past-deadline volumes + (live, suspended, recently-closed, and reopened all survive); a + **crash-orphaned unstamped** volume is reconciled and eventually reaped; + `Archive`/`Restore` return the honest sentinel. + +### W2 — snapshot backends: reflink with rsync fallback + +- **Interfaces:** implements `Snapshot`/snapshot-restore inside the W1 + backend: an unexported `cloner` seam with two implementations — reflink + copy (`cp --reflink=always`-class, FS-supporting) and rsync-clone — chosen + by a **runtime capability probe** (attempt a reflink of a probe file in + the base dir at manager construction; cache the verdict). No config knob + (OQ-3). Snapshot store: a sibling subtree under the base dir keyed by + `VolumeSnapshotID`, plus a **repo→snapshot index** the provision path reads + to set `TreeSource.Snapshot` for a new session of an already-seen repo (the + warm path is unbuildable without this lookup — it is W2's, not left + unowned). **Retention: one current snapshot per repo, replaced atomically** + on a newer clean snapshot (the parent's own unbounded-multi-GB-liability + argument, design.md:619-621, applies to the snapshot subtree exactly as it + does to volumes); a superseded snapshot is unlinked only after the + replacement commits. What counts as a snapshot-worthy (clean) source, and at + which point it is taken, is OQ-1b. +- **Depends:** W1. +- **Test cycle:** hermetic on the rsync path (any FS); the reflink path + needs a reflink-capable FS — CI job pinned to one, plus the probe's + fallback asserted on a non-capable FS (tmpfs). Snapshot→restore + round-trips byte-identical trees; restore into a fresh volume leaves the + source snapshot immutable; the repo→snapshot index returns the current + snapshot for a seen repo and nothing for an unseen one; taking a newer + snapshot atomically supersedes the prior one (old key gone, one current key + per repo). + +### W3 — `VirtualFS` seam + backends *(gated on OQ-1)* + +- **Interfaces:** + + ```go + // TreeSource selects where a session's tree comes from. + type TreeSource struct { + Repo string // forge clone URL (empty with Snapshot/CustomerMount) + Ref string // branch/commit to check out + Sparse []string // non-empty => git sparse-checkout paths + Snapshot VolumeSnapshotID // non-empty => restore this snapshot + CustomerMount string // non-empty => interop: tree pre-mounted here + } + + // VirtualFS materializes a working tree onto its destination. The + // destination is BINDING STATE of the instance (constructed with the + // target root — the session's attached volume at P2), never a + // Materialize parameter, so a later destination swaps behind this + // signature (parent design.md:533-536). + type VirtualFS interface { + Materialize(ctx context.Context, src TreeSource) (root string, err error) + Release(ctx context.Context, root string) error + } + ``` + + Under Option A the checkout backend's `Materialize` prepares the + destination (snapshot-restore via W2, else an empty owned root) and + records the expected `Repo`/`Ref`; the agent writes the tree bytes + in-container. Under Option B it additionally performs the host-side + clone/fetch. The seam, `TreeSource`, and the tests' shape are identical + either way — only the checkout backend's body and W5's launch-order differ. + **`Materialize`'s post-condition varies with the OQ-1 ruling** — "destination + prepared, tree completed in-container by the agent" under A vs "tree present + at root" under B — so the interface doc-comment states the post-condition + explicitly against the ruling (an empty-dir return under A must not surprise + a future caller: C3 burst wiring, customer-VFS interop). +- **Depends:** W1, W2; **merge-gated on OQ-1** (the backend body encodes the + ruling). +- **Test cycle:** contract tests a fake and the real backend both pass + (the S1 seam discipline, design.md:561-565); snapshot-source materialize + restores the tree; customer-mount source validates and passes through; + `Release` never deletes volume contents (P2-GC-c asserted). + +### W4 — `WorkspaceSource` variant + Mounts doc amendment + +- **Interfaces:** `runtime.Workspace` gains + `Source WorkspaceSource` with + `type WorkspaceSource int` / `const (SourceCloneDir WorkspaceSource = iota; SourceVolume)` + — zero value = today's clone-dir path, so an un-migrated caller is + byte-identical (GC 8). Under `SourceVolume`, `ensureCheckoutDir` still + runs (idempotent `mkdir -p` on the mounted path, same uid-ownership + intent, `agent.go:354-358`). The `AgentSpec.Mounts` doc comment is amended + per P2-GC-a. No `ContainerRuntime` change (the interface stays frozen, + `podman.go:399-403`). +- **Depends:** W1 (the mount it documents); parallel with W3. +- **Test cycle:** existing launch-path regression suite green with zero-value + `Source`; a `SourceVolume` spec produces the writable mount + stable + in-container path; `Mount.ReadOnly=false` renders without `:ro` + (`podman.go:846-850`). + +### W5 — provision wiring + teardown/reprovision *(gated on OQ-1)* + +- **Interfaces:** `SpecBuilder` (`go/internal/runner/host.go:46-48`) derives + `WorkspaceSource` + the volume mount from the provision request, and is the + **single author of the P2-GC-d path triple** — the `VirtualFS` instance's + bound root (host side), `Workspace.CheckoutDir` (in-container, + `workspace.go:44-47`), and `Mount.ContainerPath` (`podman.go:62-66`) are all + derived from one value in `SpecBuilder`, never wired independently by a + caller (independent wiring is how the P2-GC-d path-drift breaking bug is + born). The provision path composes: resolve-or-create (`CreateVolume` iff + `ErrVolumeNotFound`) → `Attach` (which clears any close-stamp) → + `Materialize` → `Launch`. Teardown keeps the volume and writes the + close-stamp **carrying the caller's close-vs-suspend intent** (W1); a + suspend teardown stamps *suspended* and stays ineligible for `Expire`. + Reprovision of a closed-but-unexpired session re-attaches and re-materializes + warm. Box loss: `Attach`'s typed not-found routes to the cold path + (create + cold materialize), logged as a capability event, never an error + to the user. +- **Depends:** W1, W3, W4; **merge-gated on OQ-1** (Option B would reorder + materialize-vs-launch). +- **Test cycle:** integration — provision→session→teardown→reprovision + round-trip on the volume path; the parent's P2 cycle asserted end to end + (next task). + +### W6 — expiry reaper wiring + the P2 acceptance suite + +- **Interfaces:** a periodic `Expire` driver in the Runner (ticker + startup + pass, the reconciliation idiom); config surface for the 14-day default. + Plus the parent-mandated acceptance suite (design.md:649-653): + 1. teardown-then-reprovision keeps `target/` warm — asserted by a + **rebuild-freshness probe**, not a timer: the second build recompiles + nothing (e.g. `cargo build` emits no `Compiling ` line / + fingerprints unchanged), valid because P2-GC-d's stable path plus the + same toolchain image keeps fingerprints comparable. A wall-clock + threshold is the flaky version an executor must *not* write; + 2. stable-path invariant across reattach (P2-GC-d); + 3. a snapshot-materialized session skips the clone — asserted by + **git-invocation shape**, not traffic volume: the warm path runs + `fetch` + `checkout`, never `clone` (assert the command shape, or + object-count against the local `file://` mirror's refs); + 4. expiry reaps only closed-session volumes past the deadline, and never a + reopened, suspended-past-deadline, or crash-orphaned-then-reconciled + volume before its true eligibility (the W1 close-stamp invariants); + 5. simulated box loss (delete the volume out from under a suspended + session) resumes cold without error. +- **Depends:** W1–W5. +- **Test cycle:** the five probes above; 1–4 hermetic (local FS + a local + git mirror; `file://` clones need no credential, `workspace.go:68-69`); + 5 hermetic (volume deletion is simulable). The rebuild-freshness probe + needs a real toolchain in the test image — CI-heavy but not + hardware-gated. W2's reflink leg is the only FS-hardware-sensitive test + in P2. + +## Tasks + +- [ ] **W1** — `go/internal/vfs` volume lifecycle API + (`CreateVolume`/`Attach`/`Snapshot`/`Archive`/`Restore`/`Expire`) + + local-dir backend; `Archive`/`Restore` reserved-not-implemented + (OQ-2). Hermetic. +- [ ] **W2** — snapshot backends: reflink probe + rsync fallback, snapshot + store + repo→snapshot index, one-current-snapshot-per-repo retention, + restore path (depends: W1; reflink CI leg FS-pinned). +- [ ] **W3** — `VirtualFS` seam + checkout/snapshot/customer-mount backends + (depends: W1, W2; **merge-gated on OQ-1**). +- [ ] **W4** — `WorkspaceSource` variant + `AgentSpec.Mounts` doc amendment + + mount rendering (depends: W1; parallel with W3). +- [ ] **W5** — provision wiring: resolve-or-create → attach (clears stamp) → + materialize → launch; `SpecBuilder` owns the P2-GC-d path triple; + teardown close-stamp with close-vs-suspend intent; box-loss cold path + (depends: W1, W3, W4; **merge-gated on OQ-1**). +- [ ] **W6** — expiry reaper driver + the five-probe P2 acceptance suite + (rebuild-freshness + git-shape probes, not timers) (depends: W1–W5). + +## Open Questions + +Each tagged **load-bearing** (blocks the gated tasks' merge; the caller asks +Matt) or **non-load-bearing** (deferred with rationale). The record is +drafted against each recommendation as a stated assumption. + +1. **[load-bearing] Clone model + snapshot provenance.** Two coupled, + load-bearing sub-decisions Matt ratifies as one package — A's cost/benefit + leans on "A can amortize snapshots," so the posture cannot be ruled without + the mechanism. + + **1a — Clone model + clone-read-credential location** (the amendment's + deferred fork, virtualfs-descope-amendment.md:57-72,123-129). + **(A)** keep the agent self-clone: Runner prepares/attaches the volume + (empty or snapshot-restored), agent clones/fetches in-container with its + existing `$HOME` machine-user token (`workspace.go:58-62`, + `agent.go:354-358`) — no new credential class, GC 8 trivially green; + **(B)** Runner-side host clone before container start — `VirtualFS` + literally materializes and sessionless prebuilds become possible, at the + price of a new host-side forge READ credential (distinct from DL-052's + Server-only write secret, `docs/designs/DECISIONS.md:83`) with + provisioning/rotation/scoping burden and a wider Runner-host blast radius, + plus launch-order churn; **(C)** a host-side read-only mirror + in-container + `file://` clone (§Alternatives) — collapses to "B with a smaller blast + radius": credential-free *for the agent*, but the mirror refresh needs the + same host-side forge read credential, merely fetch-scoped. + **Recommendation: A**, revisiting B only if a sessionless prebuild service + ever exists; the `Materialize` signature already accommodates the flip. + Ratifying A includes ratifying the stated reading of Global Constraint 2 + (seam owns destination + source-selection; the cold path's tree bytes are + written by the agent under the seam's contract). + + **1b — Snapshot-amortization provenance under A** (the parent mandates the + prebuild model, design.md:622-626). Under A the clone runs in-container + *after* `Materialize` returns, so there is no automatic clean snapshot + point, and a close-time snapshot leaks one session's untracked non-ignored + WIP into the next — a `git fetch` + checkout-delta restore does not remove + it — against `workspace.go:1-8`'s isolation. A mechanism must be chosen: + **(a, recommended)** snapshot a **provably-clean** post-clone tree — the + agent signals clone-complete to the Runner, the Runner verifies a + `git status`-clean tree at the ref, then snapshots; the snapshot store keys + a repo→snapshot index (W2) the provision path reads. Amortizes the **clone** + safely with **no new credential**; build-prebuild (a warm `target/` shared + across sessions) is deferred to the moment a prebuild *service* exists — + exactly when B's host-side credential would earn its existence. + **(b)** a Runner-driven one-shot first-clone (a scoped, single-use exception + to A) — a clean *and* built snapshot, but reintroduces a host-side read + credential for that path. **(c)** close-time snapshots restored through an + **ignore-aware** `git clean -fd` (no `-x`) + `git reset --hard` — removes + the untracked-non-ignored leak while keeping ignored caches (`target/`, + `node_modules`), but still shares any *ignored* file a session left (a + gitignored `.env`), so it does not fully close the leak. + **Recommendation: (a)**; the acceptable alternative ruling is to **descope + snapshot amortization from P2 entirely** (ship the per-session persistent + volume, defer cross-session snapshots to a follow-up), dropping W2's + snapshot leg and W6 probe 3 from P2 scope. The record is drafted against + (a). +2. **[non-load-bearing] `Archive`/`Restore` implementation timing.** The + parent puts the verbs on P2's API but their consumer is D4's cold-idle + (design.md:638-641,714-723). **Recommendation:** freeze the signatures in + W1 with honest not-implemented sentinels (the `Resize`/`ExecStreaming` + discipline, `podman.go:387-396`, `compute.go:26-29`); the object-store + backend and endpoint config land with D4, which owns the archive + thresholds anyway. No P2 executor is blocked. +3. **[non-load-bearing] Snapshot FS-capability detection.** **Recommendation:** + a runtime probe at `VolumeManager` construction (attempt a reflink copy of + a probe file in the base dir; cache the verdict; fall back to rsync) — no + operator knob until a deployment demonstrates the probe mis-detecting. + Pure mechanism; W2 owns it. +4. **[non-load-bearing — out of scope] Session→box stickiness vs the + scheduler.** Multi-box placement (which box a resuming session lands on, + collision handling) is a control-plane concern outside this repo; the + parent's D4 already designs the voluntary cold-migration relief valve + (design.md:759-770). P2 encodes only the box-local invariant: a Runner + attaches only volumes it hosts, and an absent volume is a typed error + routed to the cold path. Nothing in `go/internal/vfs` assumes or names + any particular placement layer. From 17d2e06ef41e0aad6e3c26ce65caea15595e3e68 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 16:41:34 -0400 Subject: [PATCH 2/7] =?UTF-8?q?docs(runtime):=20fold=20P2=20design=20revie?= =?UTF-8?q?w=20=E2=80=94=20executor-contract=20gaps=20(RIG-2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-1 review of the P2 persistent-session-volume record (2 high, 7 medium, 3 low; ~45 citations verified exact at main 17111cc0). All 12 are clone-improvement folds — no new design fork; OQ-1 remains the only fork, with Matt. - HIGH: own the clone-complete signal (agent→Runner control message + Runner-side clean-tree verify + Snapshot call + index write) as an explicit W5 sub-unit, gated on OQ-1b — it was the only caller of the Snapshot verb and was unowned. - HIGH: scope the snapshot index to (AgentAccountID, repo), not repo alone, on a hosted multi-tenant platform (new P2-GC-f; agent.go:52-56 DL-167). - MED: gate W2's snapshot-store/index leg on OQ-1 and split out the ungated cloner volume-copy primitive; add W1 Lookup so resolve-or-create is expressible; name the Server session registry as the crash-reconciliation source (host.go:77 in-memory map is not authoritative); wire Release into W5 teardown + the parent's release round-trip; narrow P2-GC-b to the drafted-against posture; correct the suspend-intent citation to design.md:708-709 (Suspend), not :714-716 (Archive); state the keep-id uid ownership invariant for the volume root (podman.go:466-471, agent.go:356-357). - LOW: cite the rw agent-gateway-socket mount precedent (podman.go:100-103); add the OQ-N vs parent-OQ-N namespace note; name W5's own merge-time assertions. Refs RIG-2395 Co-authored-by: Matt Wilkinson --- .../p2-persistent-session-volume.md | 190 ++++++++++++------ 1 file changed, 131 insertions(+), 59 deletions(-) diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md index 7d65a8e9..b9fdb4ce 100644 --- a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -120,7 +120,16 @@ Concretely: in-container path** (the same path on every launch and on every burst environment — the invariant that keeps `target/` and sccache valid, parent design.md:297-299). The in-container path becomes `Workspace.CheckoutDir`'s - parent; the host side rides `AgentSpec.Mounts` with `ReadOnly: false`. + parent; the host side rides `AgentSpec.Mounts` with `ReadOnly: false`. The + base dir and every per-session subtree are created **by the Runner as its + own invoking host user**, which the container's `keep-id` rootless remap + (`--userns=keep-id:uid=,gid=`, `podman.go:466-471`) + maps to the agent uid in-container — so a Runner-created root appears + agent-owned inside, satisfying `ensureCheckoutDir`'s precondition + ("CheckoutDir's parent must be writable by the agent uid", `agent.go:356-357`). + A base dir placed outside the Runner's own ownership (a root-owned `/var` + path, a differently-privileged installer's dir) breaks every launch on the + volume path — the invariant is load-bearing, asserted by a W1 test. - **Stickiness.** While a volume lives, the session (and its C3 bursts) relaunch on the volume's box. In the OSS single-box Runner this is trivially true (there is one box); the invariant P2 encodes is *box-local*: @@ -158,7 +167,7 @@ Concretely: - **Box loss is an accepted degradation** (vs eviction): losing the box loses the volume; the session resumes from the transcript (`COMPASS_RESUME_SESSION_FILE`) and re-materializes through `VirtualFS`, - paying one cold clone + cold build (parent design.md:630-633). `Attach` on + paying one cold clone + cold build (parent design.md:630-633). `Lookup` of a lost volume returns a typed not-found the provision path converts into a fresh `CreateVolume` + cold materialize — an error-shaped signal, never a silent recreate, so the cold path is observable. @@ -296,8 +305,13 @@ flow under volume-backed: resolve-or-create volume → `Attach` → `Materialize writable session volume at P2)". This is a **doc-comment change, not a shape change**: `Mount.ReadOnly` is already a per-mount bool (`podman.go:62-66`) and the `:ro` suffix is already conditional -(`podman.go:846-850`). Recorded as a P2-specific Global Constraint so no -reviewer treats the writable mount as a contract violation. +(`podman.go:846-850`). The downstream `ContainerSpec.Mounts` already documents +a **read-write** bind mount in the shipped tree — "Not all read-only … the +per-container agent gateway socket is mounted read-write (the agent must +connect() to it)" (`podman.go:100-103`) — so a writable mount at the layer the +volume rides is existing precedent, not a new contract. Recorded as a +P2-specific Global Constraint so no reviewer treats the writable mount as a +contract violation. ## Alternatives considered @@ -347,10 +361,15 @@ session path stays green (8), version floors (9). P2-specific additions: - **P2-GC-a — Mounts doc amendment.** The writable session volume rides `AgentSpec.Mounts` with `ReadOnly: false`; P2 updates the field's doc - comment (`agent.go:43-44`), never the `Mount` shape (`podman.go:62-66`). -- **P2-GC-b — no new credential surface without the OQ-1 ruling.** Until Matt - rules the clone fork, no task may introduce a host-side forge credential; - the drafted-against Option A introduces none ever. + comment (`agent.go:43-44`), never the `Mount` shape (`podman.go:62-66`). The + read-write agent-gateway-socket mount (`podman.go:100-103`) is the existing + read-write-bind-mount precedent at this layer. +- **P2-GC-b — no new credential surface under the drafted-against ruling.** + Option A under provenance-(a) introduces no host-side forge credential, and + until Matt rules OQ-1 no task may introduce one. A ruling of OQ-1b(b) or of + Option B/C *does* introduce a host-side forge credential and re-opens this + constraint — the prohibition binds the drafted-against posture, not every + legal outcome of OQ-1. - **P2-GC-c — volume destruction only via `Expire`.** `Release`, `Teardown`, eviction, crash, and failed launches never delete volume contents; the only reclaim path is the policy reaper (the parent's GC 5 made mechanical). @@ -362,13 +381,27 @@ session path stays green (8), version floors (9). P2-specific additions: `WorkspaceSource` is opt-in until E5; the existing container-local self-clone path keeps its regression suite green at every increment (GC 8 made concrete for P2). +- **P2-GC-f — snapshots never cross an account boundary.** The repo→snapshot + index is keyed by `(AgentAccountID, repo)` (`agent.go:52-56`) and a restore + is never served across accounts: a snapshot produced by one account's + session is invisible to another account's session of the same repo URL. + Cleanliness is not authorization — a provably-clean tree of a private repo + is still that repo's content — so the tenancy boundary the parent puts at + "the session sandbox vs other tenants and the host" (design.md:258-259) is + held in the index key, not left to the post-restore `git fetch`. ## Plan -Six implementation tasks (W1–W6). W1 is foundational; W3's checkout-backend -semantics and W5's wiring are **gated on the OQ-1 ruling** (they are drafted -against Option A and change little under B — the gate is about not merging a -credential posture Matt has not ruled). Hermetic unless noted. +Six implementation tasks (W1–W6). W1 is foundational. **OQ-1 gating:** W3's +checkout-backend semantics, W5's wiring + clone-complete-signal sub-unit, and +W2's snapshot-store/index leg are **gated on the OQ-1 ruling** — drafted +against Option A + provenance-(a), they change little under B but must not +merge a credential posture or snapshot mechanism Matt has not ruled. +**Descope-survivors:** if OQ-1b is ruled "descope snapshot amortization," W1 +(lifecycle + local-dir backend + close-stamp), W2's `cloner` volume-copy +primitive, W4 (`WorkspaceSource`), and the non-snapshot legs of W5/W6 still +ship; what drops is W2's snapshot-store/index, W5's clone-complete sub-unit, +and W6 probe 3. Hermetic unless noted. ### W1 — volume lifecycle API + local-dir backend (`go/internal/vfs`) @@ -401,9 +434,13 @@ The package skeleton mirrors `go/internal/compute`'s layering // container lifecycle. Constructed with the operator-configured base dir. type VolumeManager interface { CreateVolume(ctx context.Context, sessionID string) (Volume, error) - // Attach makes the volume available for mounting and returns its - // host path; attaching an absent volume returns ErrVolumeNotFound - // (typed — the provision path converts it into the cold path). + // Lookup resolves a session's existing volume (with its HostRoot) or + // returns ErrVolumeNotFound. It is the "resolve" half of the provision + // path's resolve-or-create: Attach needs a resolved Volume, so a caller + // cannot produce one from a bare session id without this verb. + Lookup(ctx context.Context, sessionID string) (Volume, error) + // Attach makes the resolved volume available for mounting and returns + // its host path; it also atomically clears any close-stamp. Attach(ctx context.Context, v Volume) (path string, err error) Snapshot(ctx context.Context, v Volume) (VolumeSnapshotID, error) Archive(ctx context.Context, v Volume) (ArchiveRef, error) @@ -424,50 +461,65 @@ The package skeleton mirrors `go/internal/compute`'s layering closed-but-unexpired session never carries a past-deadline stamp into its new life; (b) **`Expire` takes a per-volume lock and re-verifies eligibility under it**, so a volume is never reaped in the window between the reaper - reading its stamp and a concurrent `Attach`; (c) the stamp carries **close-vs-suspend intent** (a suspended session is *not* eligible however old — D4's suspend uses the same stop+remove teardown path, - design.md:714-716, so the intent bit comes from the caller, not inferred + design.md:708-709, so the intent bit comes from the caller, not inferred from "container gone"). A crash between container-remove and stamp-write leaves an **unstamped** closed volume; the startup pass reconciles unstamped - volumes against the authoritative session state rather than trusting the - marker alone, so a crash fails *safe* (eventually reaped) not *open* (never + volumes against the **Server's authoritative session registry** — the same + source that reconstructs `COMPASS_RESUME_SESSION_FILE` + (`agent_exec.go:40-42`), queried at Runner boot (the in-memory `sessions` + map, `host.go:77`, is rebuilt empty on restart and is not authoritative). + The reconciler reaps an unstamped volume whose session the Server reports + closed, clears the intent of one still live, and — if the Server is + **unreachable** at startup — leaves every unstamped volume untouched and + retries, never reaping on unknown. So a crash fails *safe* (eventually + reaped) not *open* (never reaped) and never *wrong* (a live session's volume reaped). - **Depends:** nothing (first P2 code task). - **Test cycle:** hermetic over tempdirs — create/attach round-trip returns - a stable path; re-attach after simulated Runner restart returns the same - path; attach-absent returns the typed not-found; **attach clears a + a stable path; `Lookup` round-trips the resolved volume and returns the + typed not-found for an absent session; re-attach after simulated Runner + restart returns the same path; the mounted root is **writable by the agent + uid** (the keep-id ownership invariant, Layout); **attach clears a past-deadline stamp**; `Expire` reaps only closed-past-deadline volumes (live, suspended, recently-closed, and reopened all survive); a - **crash-orphaned unstamped** volume is reconciled and eventually reaped; - `Archive`/`Restore` return the honest sentinel. + **crash-orphaned unstamped** volume is reconciled against a fake session + registry (reaped when reported closed, spared when reported live, spared + when the registry is unreachable); `Archive`/`Restore` return the honest + sentinel. ### W2 — snapshot backends: reflink with rsync fallback -- **Interfaces:** implements `Snapshot`/snapshot-restore inside the W1 - backend: an unexported `cloner` seam with two implementations — reflink - copy (`cp --reflink=always`-class, FS-supporting) and rsync-clone — chosen - by a **runtime capability probe** (attempt a reflink of a probe file in - the base dir at manager construction; cache the verdict). No config knob - (OQ-3). Snapshot store: a sibling subtree under the base dir keyed by - `VolumeSnapshotID`, plus a **repo→snapshot index** the provision path reads - to set `TreeSource.Snapshot` for a new session of an already-seen repo (the - warm path is unbuildable without this lookup — it is W2's, not left - unowned). **Retention: one current snapshot per repo, replaced atomically** - on a newer clean snapshot (the parent's own unbounded-multi-GB-liability - argument, design.md:619-621, applies to the snapshot subtree exactly as it - does to volumes); a superseded snapshot is unlinked only after the - replacement commits. What counts as a snapshot-worthy (clean) source, and at - which point it is taken, is OQ-1b. -- **Depends:** W1. +- **Volume-copy primitive (ungated).** An unexported `cloner` seam with two + implementations — reflink copy (`cp --reflink=always`-class, FS-supporting) + and rsync-clone — chosen by a **runtime capability probe** (attempt a reflink + of a probe file in the base dir at manager construction; cache the verdict; + no config knob, OQ-3). Useful under every OQ-1b ruling (it is also D4's + archive/restore copy path), so it is **not** gated. +- **Snapshot store + index (gated on OQ-1b).** A sibling subtree under the base + dir keyed by `VolumeSnapshotID`, plus an **`(AgentAccountID, repo)`→snapshot + index** the provision path reads to set `TreeSource.Snapshot` for a new + session of an already-seen `(account, repo)`. The account scope is + load-bearing, not cosmetic: repo URL alone would restore one tenant's + snapshot into another tenant's volume (P2-GC-f, `agent.go:52-56`). The warm + path is unbuildable without this lookup — it is W2's, not left unowned. + **Retention: one current snapshot per `(account, repo)`**, replaced + atomically on a newer clean snapshot (the parent's own + unbounded-multi-GB-liability argument, design.md:619-621, applies to the + snapshot subtree exactly as to volumes); a superseded snapshot is unlinked + only after the replacement commits. What counts as a snapshot-worthy (clean) + source, and when it is taken, is OQ-1b (the trigger is W5's clone-complete + sub-unit); this whole leg drops if OQ-1b is ruled "descope." +- **Depends:** W1; **snapshot-store/index leg merge-gated on OQ-1**. - **Test cycle:** hermetic on the rsync path (any FS); the reflink path needs a reflink-capable FS — CI job pinned to one, plus the probe's fallback asserted on a non-capable FS (tmpfs). Snapshot→restore round-trips byte-identical trees; restore into a fresh volume leaves the - source snapshot immutable; the repo→snapshot index returns the current - snapshot for a seen repo and nothing for an unseen one; taking a newer - snapshot atomically supersedes the prior one (old key gone, one current key - per repo). + source snapshot immutable; the index returns the current snapshot for a seen + `(account, repo)` and **nothing for the same repo under a different account** + (P2-GC-f) or an unseen repo; taking a newer snapshot atomically supersedes + the prior one (old key gone, one current key per `(account, repo)`). ### W3 — `VirtualFS` seam + backends *(gated on OQ-1)* @@ -538,20 +590,36 @@ The package skeleton mirrors `go/internal/compute`'s layering `workspace.go:44-47`), and `Mount.ContainerPath` (`podman.go:62-66`) are all derived from one value in `SpecBuilder`, never wired independently by a caller (independent wiring is how the P2-GC-d path-drift breaking bug is - born). The provision path composes: resolve-or-create (`CreateVolume` iff - `ErrVolumeNotFound`) → `Attach` (which clears any close-stamp) → - `Materialize` → `Launch`. Teardown keeps the volume and writes the - close-stamp **carrying the caller's close-vs-suspend intent** (W1); a - suspend teardown stamps *suspended* and stays ineligible for `Expire`. - Reprovision of a closed-but-unexpired session re-attaches and re-materializes - warm. Box loss: `Attach`'s typed not-found routes to the cold path - (create + cold materialize), logged as a capability event, never an error - to the user. -- **Depends:** W1, W3, W4; **merge-gated on OQ-1** (Option B would reorder - materialize-vs-launch). + born). The provision path composes: resolve-or-create (`Lookup`, then + `CreateVolume` iff `ErrVolumeNotFound`) → `Attach` (which clears any + close-stamp) → `Materialize` → `Launch`. Teardown composes the reverse: + `Release` the materialized root (never deleting volume contents, P2-GC-c) → + write the close-stamp **carrying the caller's close-vs-suspend intent** (W1) + → `Teardown` the container (`agent.go:216-236`); a suspend teardown stamps + *suspended* and stays ineligible for `Expire`. Reprovision of a + closed-but-unexpired session re-attaches and re-materializes warm. Box loss: + `Lookup`'s typed not-found routes to the cold path (create + cold + materialize), logged as a capability event, never an error to the user. +- **Clone-complete signal → snapshot (sub-unit, gated on OQ-1b).** Under + provenance-(a) the snapshot is taken from a **provably-clean post-clone + tree**, so the trigger is owned here, not left implicit: (i) the agent + (`packages/compass-agent`, TypeScript) emits a **clone-complete** control + message over the AgentGateway socket once its self-clone finishes; (ii) a + Runner-side handler verifies a `git status`-clean tree at the expected `Ref` + and calls `VolumeManager.Snapshot` (W1) → writes the `(AgentAccountID, repo)` + index entry (W2, P2-GC-f). This is the **only caller of `Snapshot`** in P2 — + without it the verb is dead and the warm path never triggers. The whole + sub-unit dies if OQ-1b is ruled "descope snapshots," so it merges behind the + OQ-1 gate with the rest of W5. +- **Depends:** W1, W2, W3, W4; **merge-gated on OQ-1** (Option B would reorder + materialize-vs-launch; the clone-complete sub-unit is gated on OQ-1b). - **Test cycle:** integration — provision→session→teardown→reprovision - round-trip on the volume path; the parent's P2 cycle asserted end to end - (next task). + round-trip on the volume path, extended to the parent's full + provision→materialize→session→**release** round-trip (design.md:561-562); + W5 owns at merge: the volume survives teardown, the mount path is stable + across the reattach, and a suspend-stamped volume is `Expire`-ineligible. + W5 and W6 land together behind the OQ-1 gate; the five-probe acceptance + suite (the parent's end-to-end P2 cycle) is W6's. ### W6 — expiry reaper wiring + the P2 acceptance suite @@ -608,6 +676,9 @@ Each tagged **load-bearing** (blocks the gated tasks' merge; the caller asks Matt) or **non-load-bearing** (deferred with rationale). The record is drafted against each recommendation as a stated assumption. +> Namespace: an unprefixed **OQ-N** refers to *this* record's open questions; +> the parent record's are always written **parent OQ N** (space, no hyphen). + 1. **[load-bearing] Clone model + snapshot provenance.** Two coupled, load-bearing sub-decisions Matt ratifies as one package — A's cost/benefit leans on "A can amortize snapshots," so the posture cannot be ruled without @@ -641,9 +712,10 @@ drafted against each recommendation as a stated assumption. WIP into the next — a `git fetch` + checkout-delta restore does not remove it — against `workspace.go:1-8`'s isolation. A mechanism must be chosen: **(a, recommended)** snapshot a **provably-clean** post-clone tree — the - agent signals clone-complete to the Runner, the Runner verifies a - `git status`-clean tree at the ref, then snapshots; the snapshot store keys - a repo→snapshot index (W2) the provision path reads. Amortizes the **clone** + agent signals clone-complete to the Runner (owned by W5's clone-complete + sub-unit), the Runner verifies a `git status`-clean tree at the ref, then + snapshots; the snapshot store keys an `(account, repo)`→snapshot index + (W2, P2-GC-f) the provision path reads. Amortizes the **clone** safely with **no new credential**; build-prebuild (a warm `target/` shared across sessions) is deferred to the moment a prebuild *service* exists — exactly when B's host-side credential would earn its existence. From 1684f845fd3c3dbdf1e4dac676bc310b5e7edc32 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 17:22:01 -0400 Subject: [PATCH 3/7] =?UTF-8?q?docs(runtime):=20fold=20P2=20design=20revie?= =?UTF-8?q?w=20round=202=20=E2=80=94=20fold-introduced=20defects=20(RIG-23?= =?UTF-8?q?95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review of the P2 persistent-session-volume record (2 high, 5 medium, 2 low). All 8 fold-added source citations verified line-exact at main 17111cc0; 10 of 12 round-1 folds landed clean. The 9 findings are defects the round-1 fold itself introduced — no new design fork; OQ-1 remains the only fork, with Matt. - HIGH: restore the close-stamp invariant (c) a splice destroyed — all three W1 invariants (Attach-clears / per-volume-lock / close-vs-suspend intent) are labelled again. - HIGH: the clone-complete signal named `Control`, which is Runner→agent only (agent_gateway.proto:74-77). Re-spec it as a new agent→Runner unary RPC on the PostConversationFrame delivered-or-erred precedent (agent_gateway.proto: 68-73) — a dropped once-per-clone trigger must be an agent-retried error, not a silent warm-path loss on the loss-tolerable Publish spine; name the verify side (host-side git-status under the keep-id invariant). - MED: propagate the fold into the `## Tasks` checklist (add Lookup to W1; W5 deps W1,W2,W3,W4 + clone-complete sub-unit; account-scoped W2 index + its OQ-1 gate marker) — it still encoded the pre-fold API, DAG edge, and the un-account-scoped index HIGH-2 existed to fix. - MED: name the crash-reconciliation source concretely — hold unstamped volumes until the Server pushes the session set over the `Sessions` stream (runner.proto:61-70); RunnerService has no Runner→Server pull verb. - MED: reorder W5 teardown to container→release→stamp (the real reverse + W1's crash model), not release-while-mounted. - MED: make the descope-survivor list exhaustive over W1–W6 (add W3 minus its snapshot backend; state Snapshot joins Archive/Restore reserved-not- implemented when its sole caller drops). - LOW: route the Stickiness bullet through `Lookup` not `Attach`; two remaining repo→index namings → `(account, repo)`. Refs RIG-2395 Co-authored-by: Matt Wilkinson --- .../p2-persistent-session-volume.md | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md index b9fdb4ce..ec9bc4e1 100644 --- a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -133,8 +133,8 @@ Concretely: - **Stickiness.** While a volume lives, the session (and its C3 bursts) relaunch on the volume's box. In the OSS single-box Runner this is trivially true (there is one box); the invariant P2 encodes is *box-local*: - a Runner never attaches a volume it does not host, and `Attach` of an - absent volume is an error, never a silent recreate. Multi-box placement is + a Runner never attaches a volume it does not host, and `Lookup` of an + absent volume is a typed error, never a silent recreate. Multi-box placement is a control-plane concern outside this repo's scope (OQ-4). - **Derived state always persists** (Global Constraint 5): `target/`, `node_modules`, caches live on the volume because the *whole working @@ -152,7 +152,7 @@ Concretely: reflink/snapshot (btrfs/XFS/bcachefs reflink copy), by rsync-clone otherwise; a later session on the same repo restores the snapshot and starts warm on `git fetch` + checkout-delta instead of a full clone. *What* gets - snapshotted, *when* (the clean-tree stamp point), and the repo→snapshot + snapshotted, *when* (the clean-tree stamp point), and the `(account, repo)`→snapshot index the provision path reads are the load-bearing sub-fork OQ-1b — the amortization mechanism, not just the clone posture, is what Matt ratifies. Detection of the copy primitive is a runtime capability probe, not a config @@ -238,7 +238,7 @@ container-local dir onto the volume. hold, file contents in place of tokens). So A's amortization is sound only with a designed provenance mechanism: the recommended one snapshots a **provably-clean** post-clone tree (an agent→Runner clone-complete signal - plus a `git status`-clean check), keyed in a repo→snapshot index (W2) the + plus a `git status`-clean check), keyed in an `(account, repo)`→snapshot index (W2) the provision path reads. This is the load-bearing sub-fork **OQ-1b**; it rides A's ruling because "A amortizes fine" is load-bearing in A's own cost/benefit. @@ -399,9 +399,13 @@ against Option A + provenance-(a), they change little under B but must not merge a credential posture or snapshot mechanism Matt has not ruled. **Descope-survivors:** if OQ-1b is ruled "descope snapshot amortization," W1 (lifecycle + local-dir backend + close-stamp), W2's `cloner` volume-copy -primitive, W4 (`WorkspaceSource`), and the non-snapshot legs of W5/W6 still -ship; what drops is W2's snapshot-store/index, W5's clone-complete sub-unit, -and W6 probe 3. Hermetic unless noted. +primitive, W3's seam + checkout and customer-mount backends, W4 +(`WorkspaceSource`), and the non-snapshot legs of W5/W6 still ship; what drops +is W2's snapshot-store/index, W3's snapshot-source backend + the +`TreeSource.Snapshot` path, W5's clone-complete sub-unit, and W6 probe 3. +`Snapshot` then joins `Archive`/`Restore` as **reserved-not-implemented** on +the W1 interface (its sole caller drops), keeping the honest-sentinel +discipline. Hermetic unless noted. ### W1 — volume lifecycle API + local-dir backend (`go/internal/vfs`) @@ -461,19 +465,22 @@ The package skeleton mirrors `go/internal/compute`'s layering closed-but-unexpired session never carries a past-deadline stamp into its new life; (b) **`Expire` takes a per-volume lock and re-verifies eligibility under it**, so a volume is never reaped in the window between the reaper + reading its stamp and a concurrent `Attach`; (c) the stamp carries **close-vs-suspend intent** (a suspended session is *not* eligible however old — D4's suspend uses the same stop+remove teardown path, design.md:708-709, so the intent bit comes from the caller, not inferred from "container gone"). A crash between container-remove and stamp-write leaves an **unstamped** closed volume; the startup pass reconciles unstamped - volumes against the **Server's authoritative session registry** — the same - source that reconstructs `COMPASS_RESUME_SESSION_FILE` - (`agent_exec.go:40-42`), queried at Runner boot (the in-memory `sessions` - map, `host.go:77`, is rebuilt empty on restart and is not authoritative). - The reconciler reaps an unstamped volume whose session the Server reports - closed, clears the intent of one still live, and — if the Server is - **unreachable** at startup — leaves every unstamped volume untouched and - retries, never reaping on unknown. So a crash fails *safe* (eventually + volumes against the Server's authoritative session set: it holds every + unstamped volume until the Server has pushed that set over the `Sessions` + stream after `Enroll` (`runner.proto:61-70`, Server-push — the Runner has no + pull verb, and the in-memory `sessions` map, `host.go:77`, is rebuilt empty + on restart and is not authoritative; the Server is the same authority that + reconstructs `COMPASS_RESUME_SESSION_FILE`, `agent_exec.go:40-42`). An + unstamped volume absent from the pushed set is reaped, one the Server reports + live has its intent cleared, and until the set arrives (Server not yet + resynced) every unstamped volume is held, never reaped on unknown. So a crash + fails *safe* (eventually reaped) not *open* (never reaped) and never *wrong* (a live session's volume reaped). - **Depends:** nothing (first P2 code task). @@ -484,9 +491,9 @@ The package skeleton mirrors `go/internal/compute`'s layering uid** (the keep-id ownership invariant, Layout); **attach clears a past-deadline stamp**; `Expire` reaps only closed-past-deadline volumes (live, suspended, recently-closed, and reopened all survive); a - **crash-orphaned unstamped** volume is reconciled against a fake session - registry (reaped when reported closed, spared when reported live, spared - when the registry is unreachable); `Archive`/`Restore` return the honest + **crash-orphaned unstamped** volume is reconciled against a fake delivered + session set (reaped when absent, spared when present-live, held when the set + has not yet arrived); `Archive`/`Restore` return the honest sentinel. ### W2 — snapshot backends: reflink with rsync fallback @@ -593,9 +600,9 @@ The package skeleton mirrors `go/internal/compute`'s layering born). The provision path composes: resolve-or-create (`Lookup`, then `CreateVolume` iff `ErrVolumeNotFound`) → `Attach` (which clears any close-stamp) → `Materialize` → `Launch`. Teardown composes the reverse: - `Release` the materialized root (never deleting volume contents, P2-GC-c) → - write the close-stamp **carrying the caller's close-vs-suspend intent** (W1) - → `Teardown` the container (`agent.go:216-236`); a suspend teardown stamps + `Teardown` the container (`agent.go:216-236`) → `Release` the materialized + root (never deleting volume contents, P2-GC-c) → write the close-stamp + **carrying the caller's close-vs-suspend intent** (W1); a suspend teardown stamps *suspended* and stays ineligible for `Expire`. Reprovision of a closed-but-unexpired session re-attaches and re-materializes warm. Box loss: `Lookup`'s typed not-found routes to the cold path (create + cold @@ -603,10 +610,16 @@ The package skeleton mirrors `go/internal/compute`'s layering - **Clone-complete signal → snapshot (sub-unit, gated on OQ-1b).** Under provenance-(a) the snapshot is taken from a **provably-clean post-clone tree**, so the trigger is owned here, not left implicit: (i) the agent - (`packages/compass-agent`, TypeScript) emits a **clone-complete** control - message over the AgentGateway socket once its self-clone finishes; (ii) a - Runner-side handler verifies a `git status`-clean tree at the expected `Ref` - and calls `VolumeManager.Snapshot` (W1) → writes the `(AgentAccountID, repo)` + (`packages/compass-agent`, TypeScript) emits a **clone-complete** signal to + the Runner over a **new unary `AgentGateway` RPC** — delivered-or-erred, the + `PostConversationFrame` precedent (`agent_gateway.proto:68-73`), *not* the + loss-tolerable `Publish` spine: the trigger fires exactly once per clone and + cannot be reconstructed, so a drop on `Publish` would silently kill the warm + path for that `(account, repo)` with no retry, whereas a unary drop is an + agent-retried error; (ii) a Runner-side handler verifies a **host-side** + `git status`-clean tree at the volume root (viable under the keep-id + ownership invariant, Layout) at the expected `Ref`, then calls + `VolumeManager.Snapshot` (W1) → writes the `(AgentAccountID, repo)` index entry (W2, P2-GC-f). This is the **only caller of `Snapshot`** in P2 — without it the verb is dead and the warm path never triggers. The whole sub-unit dies if OQ-1b is ruled "descope snapshots," so it merges behind the @@ -653,20 +666,24 @@ The package skeleton mirrors `go/internal/compute`'s layering ## Tasks - [ ] **W1** — `go/internal/vfs` volume lifecycle API - (`CreateVolume`/`Attach`/`Snapshot`/`Archive`/`Restore`/`Expire`) + + (`CreateVolume`/`Lookup`/`Attach`/`Snapshot`/`Archive`/`Restore`/`Expire`) + local-dir backend; `Archive`/`Restore` reserved-not-implemented (OQ-2). Hermetic. - [ ] **W2** — snapshot backends: reflink probe + rsync fallback, snapshot - store + repo→snapshot index, one-current-snapshot-per-repo retention, - restore path (depends: W1; reflink CI leg FS-pinned). + store + `(AgentAccountID, repo)`→snapshot index (never cross-account, + P2-GC-f), one-current-snapshot-per-`(account, repo)` retention, restore + path (depends: W1; **snapshot-store/index leg merge-gated on OQ-1**; + reflink CI leg FS-pinned). - [ ] **W3** — `VirtualFS` seam + checkout/snapshot/customer-mount backends (depends: W1, W2; **merge-gated on OQ-1**). - [ ] **W4** — `WorkspaceSource` variant + `AgentSpec.Mounts` doc amendment + mount rendering (depends: W1; parallel with W3). - [ ] **W5** — provision wiring: resolve-or-create → attach (clears stamp) → materialize → launch; `SpecBuilder` owns the P2-GC-d path triple; - teardown close-stamp with close-vs-suspend intent; box-loss cold path - (depends: W1, W3, W4; **merge-gated on OQ-1**). + teardown (container → release → close-stamp with close-vs-suspend + intent); agent clone-complete signal → host-side git-status-clean verify + → `Snapshot` → `(account, repo)` index write (sub-unit gated on OQ-1b); + box-loss cold path (depends: W1, W2, W3, W4; **merge-gated on OQ-1**). - [ ] **W6** — expiry reaper driver + the five-probe P2 acceptance suite (rebuild-freshness + git-shape probes, not timers) (depends: W1–W5). From f27a6a3b6b2a2920c9545d5de9416d38e744a0de Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 18:00:25 -0400 Subject: [PATCH 4/7] =?UTF-8?q?docs(runtime):=20fold=20P2=20design=20revie?= =?UTF-8?q?w=20round=203=20=E2=80=94=20Runner-local=20crash=20reconcile=20?= =?UTF-8?q?(RIG-2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-3 review (K=3, final) of the P2 persistent-session-volume record: 1 high, 1 medium, both verified at main 17111cc0. Seven of the nine round-2 folds landed clean; two did not — both are round-2 fold-introduced defects, no new design fork (OQ-1 remains the only fork, with Matt). - HIGH: the round-2 crash-reconciliation re-spec was unbuildable. It waited for the Server to push a session SET over `Sessions`, but `SessionsResponse.command` is an exhaustive oneof with no session-set variant (runner.proto:233-284), and its premise ("Server's authoritative session set") inverts the real authority direction — the Runner is authoritative for live session truth (host.go:3-7, dispatch.go:10-11), and `Hub.enroll` clears the Server's bindings at every enroll (hub.go:933-935), so the map is empty exactly when a restart would read it. It failed OPEN: an unstamped orphan held forever = the unbounded leak the 14-day policy exists to bound. Re-spec Runner-local: stamp every unstamped volume closed AT DISCOVERY (deadline from discovery), invariant (a) clears it on a re-provisioning Attach. No Server query — fails safe, never open, never wrong. W1 test cycle retargeted (discovery-stamp + Attach-clears, no fake session set). - MED: W6's OQ-1 gate status contradicted itself across three sections — the round-2 fold's added sentence gated all of W6, but the Plan enumeration and the descope list gate only probe 3. Reconciled to gate W6's snapshot-materialized probe (probe 3) alone in all four places (Plan :395-400, W5 test :641, Tasks W6 line, consistent with descope :406). Review K=3 is exhausted; this fold is not itself review-verified. Surfaced to Matt on RIG-2395. Refs RIG-2395 Co-authored-by: Matt Wilkinson --- .../p2-persistent-session-volume.md | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md index ec9bc4e1..6ab984fa 100644 --- a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -393,10 +393,11 @@ session path stays green (8), version floors (9). P2-specific additions: ## Plan Six implementation tasks (W1–W6). W1 is foundational. **OQ-1 gating:** W3's -checkout-backend semantics, W5's wiring + clone-complete-signal sub-unit, and -W2's snapshot-store/index leg are **gated on the OQ-1 ruling** — drafted -against Option A + provenance-(a), they change little under B but must not -merge a credential posture or snapshot mechanism Matt has not ruled. +checkout-backend semantics, W5's wiring + clone-complete-signal sub-unit, +W2's snapshot-store/index leg, and W6's snapshot-materialized acceptance +probe (probe 3) are **gated on the OQ-1 ruling** — drafted against Option A + +provenance-(a), they change little under B but must not merge a credential +posture or snapshot mechanism Matt has not ruled. **Descope-survivors:** if OQ-1b is ruled "descope snapshot amortization," W1 (lifecycle + local-dir backend + close-stamp), W2's `cloner` volume-copy primitive, W3's seam + checkout and customer-mount backends, W4 @@ -470,19 +471,25 @@ The package skeleton mirrors `go/internal/compute`'s layering old — D4's suspend uses the same stop+remove teardown path, design.md:708-709, so the intent bit comes from the caller, not inferred from "container gone"). A crash between container-remove and stamp-write - leaves an **unstamped** closed volume; the startup pass reconciles unstamped - volumes against the Server's authoritative session set: it holds every - unstamped volume until the Server has pushed that set over the `Sessions` - stream after `Enroll` (`runner.proto:61-70`, Server-push — the Runner has no - pull verb, and the in-memory `sessions` map, `host.go:77`, is rebuilt empty - on restart and is not authoritative; the Server is the same authority that - reconstructs `COMPASS_RESUME_SESSION_FILE`, `agent_exec.go:40-42`). An - unstamped volume absent from the pushed set is reaped, one the Server reports - live has its intent cleared, and until the set arrives (Server not yet - resynced) every unstamped volume is held, never reaped on unknown. So a crash - fails *safe* (eventually - reaped) not *open* (never reaped) and never *wrong* (a live session's volume - reaped). + leaves an **unstamped** closed volume; the startup pass **stamps every + unstamped volume closed at discovery time** — a discovered-orphan stamp + whose deadline runs from discovery, not from the lost close — and invariant + (a) undoes it for free if that session is re-provisioned before the + deadline. This needs **no Server query**, and by design cannot want one: + RunnerService exposes no session-query verb and `SessionsResponse` carries + no session-set variant (its `command` oneof is exhaustive, + `runner.proto:233-284`), while the Runner — not the Server — is + authoritative for live session truth (OQ6, + `go/internal/runner/host.go:3-7`, `go/internal/runner/dispatch.go:10-11`) + and `Hub.enroll` clears the Server's session bindings at every enroll + (`hub.go:933-935`), so the Server's live-session map is *empty* exactly when + a restart would consult it. The in-memory `sessions` map (`host.go:77`) is + rebuilt empty on restart and is not authoritative either. So a crash fails + *safe* (reaped one full expiry window after discovery) not *open* (the + volume is always stamped, so `Expire` can always reach it) and never *wrong* + (a live or resuming session re-attaches and clears the stamp on its next + launch, and a suspended session that never crashed was stamped *suspended* + by its normal teardown, untouched by this pass). - **Depends:** nothing (first P2 code task). - **Test cycle:** hermetic over tempdirs — create/attach round-trip returns a stable path; `Lookup` round-trips the resolved volume and returns the @@ -491,10 +498,10 @@ The package skeleton mirrors `go/internal/compute`'s layering uid** (the keep-id ownership invariant, Layout); **attach clears a past-deadline stamp**; `Expire` reaps only closed-past-deadline volumes (live, suspended, recently-closed, and reopened all survive); a - **crash-orphaned unstamped** volume is reconciled against a fake delivered - session set (reaped when absent, spared when present-live, held when the set - has not yet arrived); `Archive`/`Restore` return the honest - sentinel. + **crash-orphaned unstamped** volume is stamped at discovery, survives to + its discovery-based deadline, is cleared by a subsequent `Attach`, and is + reaped only when the deadline passes with no re-attach; + `Archive`/`Restore` return the honest sentinel. ### W2 — snapshot backends: reflink with rsync fallback @@ -631,8 +638,9 @@ The package skeleton mirrors `go/internal/compute`'s layering provision→materialize→session→**release** round-trip (design.md:561-562); W5 owns at merge: the volume survives teardown, the mount path is stable across the reattach, and a suspend-stamped volume is `Expire`-ineligible. - W5 and W6 land together behind the OQ-1 gate; the five-probe acceptance - suite (the parent's end-to-end P2 cycle) is W6's. + W5 and W6's snapshot-materialized probe (probe 3) land together behind the + OQ-1 gate; the five-probe acceptance suite (the parent's end-to-end P2 + cycle) is W6's. ### W6 — expiry reaper wiring + the P2 acceptance suite @@ -685,7 +693,8 @@ The package skeleton mirrors `go/internal/compute`'s layering → `Snapshot` → `(account, repo)` index write (sub-unit gated on OQ-1b); box-loss cold path (depends: W1, W2, W3, W4; **merge-gated on OQ-1**). - [ ] **W6** — expiry reaper driver + the five-probe P2 acceptance suite - (rebuild-freshness + git-shape probes, not timers) (depends: W1–W5). + (rebuild-freshness + git-shape probes, not timers) (depends: W1–W5; + **probe 3 merge-gated on OQ-1**). ## Open Questions From f91941581f8d617522a1ee49dc615f5492b6f943 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 5 Sep 2026 10:54:25 -0400 Subject: [PATCH 5/7] =?UTF-8?q?docs(runtime):=20fold=20OQ-1=20ruling=20?= =?UTF-8?q?=E2=80=94=20agent=20self-clone=20+=20clone-only=20snapshot=20(R?= =?UTF-8?q?IG-2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matt ruled the P2 record's central clone/credential fork (OQ-1) on 2026-09-05: **Option A (agent self-clone) + clone-only snapshot (provenance-(a))** — the posture the record was already drafted against, so this fold settles the contract without reshaping any task. ### What changed - **OQ-1 section** rewritten from an open question to a ruled decision (`### OQ-1 … *(RULED 2026-09-05, Matt)*`), recording both sub-parts and Matt's revisit trigger: Runner-side box-global read-only clones of subscribed repos are a deferred follow-up optimization, and a host-side read credential (Option B/C) earns its existence only when a sessionless prebuild service materializes trees with no session. - **DL-326** added to `DECISIONS.md` (Storage), the credential-**location** decision beside DL-052 (which speaks only to the Server's write credential). - **Un-gated the tasks:** W2 (snapshot store/index), W3 (`VirtualFS` seam), W5 (provision wiring + clone-complete sub-unit), and W6 probe 3 now build to the ruled contract instead of carrying "gated on OQ-1 / drafts against" hedging. Global Constraint P2-GC-b, the Plan intro, the central-decision close, and the Status/Ledger-impact note all reframed to the ruled state. - OQ-2/3/4 promoted to headings for section consistency (they were a numbered list under a heading). The snapshot carries a provably-clean post-clone tree only — never cross-session build state — so a new session of a seen `(account, repo)` skips the cold clone but builds cold on its own volume: zero cross-session leak. Cross-session build prebuild is explicitly out of P2 scope, deferred to the same future prebuild-service moment as the 1a revisit. Spec-impact: none. Refs RIG-2395 Co-authored-by: Matt Wilkinson --- docs/designs/DECISIONS.md | 1 + .../p2-persistent-session-volume.md | 264 ++++++++---------- 2 files changed, 125 insertions(+), 140 deletions(-) diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index a3c954fe..fc8dbadc 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -112,6 +112,7 @@ check enforces the mechanical half. Full rationale: | DL-314 | Postgres remains the store of record and the sole durability source of truth for committed comms/routing/session-binding state (restating DL-019's surviving "Postgres is the store of record" clause verbatim): JetStream is an at-least-once delivery TRANSPORT whose consumer state is disposable, never a second truth store; every fabric recovery path terminates in a Postgres cursor or row. Supersedes DL-019's "JetStream is comms-only" clause only — its store-of-record and transcript-blob-seam clauses (DL-093) survive (RIG-2861 OQ-1) | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#q3--the-eventing-substrate-one-nats-eventfabric-a-standalone-stack-service-jetstream-as-the-delivery-transport) | | DL-315 | The cross-tenant background/system loops (delivery-cursor sweep, deliver-ack advance, reattach recovery, lag-resync) run under a narrowly-scoped `BYPASSRLS` system role granted ONLY to those named background workers and NEVER on the request path; every request-path query stays fail-closed under RLS (RIG-2861 OQ-4 = option 1) | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#resolved-decisions-freeze--matt-2026-08-31) | | DL-317 | Under multi-tenancy, user/system handle uniqueness is PER-ORGANIZATION not global (RIG-2921 = option A): `account_handles` gains a `tenant_id` and its partial-unique indexes become org-scoped, and the handle resolvers gain a tenant filter — two organizations may each hold `@matt`. The sole globally-unique identifier is the organization NAME, CASE-FOLDED for that uniqueness check (normalized index form, display casing preserved) — `Acme` and `acme` are the same organization (GitHub model). Refines DL-271's "user/system handles globally unique" clause to per-organization under multi-tenancy (single-tenant OSS stays degenerate: one org, so per-org == global); implemented on the RIG-2880 `account_handles` storage contract | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#resolved-decisions-freeze--matt-2026-08-31) | +| DL-326 | The P2 session-volume clone model is **agent self-clone (Option A)**: the Runner prepares/attaches the volume (empty or snapshot-restored) and the agent clones/fetches in-container with its existing `$HOME` machine-user token — no new host-side forge read credential (distinct from DL-052's Server-only write secret). Snapshot amortization is **clone-only (provenance-(a))**: the snapshot carries a provably-clean post-clone tree (agent clone-complete signal → Runner host-side `git status`-clean verify → snapshot, keyed by an `(account, repo)` index), never cross-session build state (`target/`, caches), so a new session of a seen `(account, repo)` skips the cold clone but builds cold on its own volume — zero cross-session leak. Runner-side read-only bare clones of subscribed repos are a deferred follow-up optimization, and a host-side read credential earns its existence only when a sessionless prebuild service materializes trees with no session; the `Materialize` signature already accommodates that flip. Resolves this record's OQ-1 (both sub-parts). | Active (Matt, 2026-09-05) | [P2 persistent session volume §OQ-1](infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md#oq-1--clone-model--snapshot-provenance-ruled-2026-09-05-matt) | > Note (2026-07-31, RIG-1570 R5): DL-065's retired internal `ResumeContext > resume = 12` envelope field is NOT silently reintroduced — the collapsed diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md index 6ab984fa..dbf04ca6 100644 --- a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -13,14 +13,14 @@ > citation is a path in the **`RigelBuild/compass`** monorepo at main > `17111cc0` (line numbers drift; resolve against that commit). -Status: PROPOSED — details P2 under the frozen parent + the active amendment. +Status: PROPOSED — details P2 under the frozen parent + the active amendment; +the central clone/credential fork (OQ-1) is ruled (Matt, 2026-09-05, DL-326). Tracking: RIG-2395 -Ledger impact: none intended. The clone/credential fork (OQ-1) is -load-bearing and, once ruled, may warrant a ledger row beside DL-052 (it is a -credential-**location** decision DL-052 does not speak to, -virtualfs-descope-amendment.md:62-67); that row lands with the ruling, not -with this draft. +Ledger impact: **DL-326** (the OQ-1 ruling: agent self-clone + clone-only +snapshot). It is a credential-**location** decision beside DL-052, which speaks +only to the Server's write credential (virtualfs-descope-amendment.md:62-67); +the row landed with Matt's ruling. ## Problem / Intent @@ -280,10 +280,10 @@ snapshots via OQ-1b's clean-stamp mechanism), and B's price is a standing new credential surface plus launch-path churn. A is reversible: if a prebuild *service* ever materializes trees with no session, that is the moment a host-side read credential earns its existence, and the `Materialize` signature -already accommodates it. The whole fork is **OQ-1, load-bearing** — its clone -posture (1a) and snapshot provenance (1b) are ratified together; the caller -takes it to Matt, the Plan below sequences the gated tasks after the ruling, -and this record drafts against A + provenance-(a). +already accommodates it. The whole fork was **OQ-1, load-bearing** — its clone +posture (1a) and snapshot provenance (1b), ratified together by Matt as +**Option A + clone-only** (DL-326, §OQ-1); this record now builds to that +ruling. ### `WorkspaceSource` and the provision wiring @@ -364,12 +364,12 @@ session path stays green (8), version floors (9). P2-specific additions: comment (`agent.go:43-44`), never the `Mount` shape (`podman.go:62-66`). The read-write agent-gateway-socket mount (`podman.go:100-103`) is the existing read-write-bind-mount precedent at this layer. -- **P2-GC-b — no new credential surface under the drafted-against ruling.** - Option A under provenance-(a) introduces no host-side forge credential, and - until Matt rules OQ-1 no task may introduce one. A ruling of OQ-1b(b) or of - Option B/C *does* introduce a host-side forge credential and re-opens this - constraint — the prohibition binds the drafted-against posture, not every - legal outcome of OQ-1. +- **P2-GC-b — no new credential surface.** The ruled Option A + clone-only + (DL-326) introduces no host-side forge credential — the agent self-clones + in-container with its existing `$HOME` token — and no P2 task may introduce + one. This binds the ruled posture; a future Option B/C or build-prebuild + service (the DL-326 revisit trigger) would re-open this constraint under its + own decision. - **P2-GC-c — volume destruction only via `Expire`.** `Release`, `Teardown`, eviction, crash, and failed launches never delete volume contents; the only reclaim path is the policy reaper (the parent's GC 5 made mechanical). @@ -392,21 +392,15 @@ session path stays green (8), version floors (9). P2-specific additions: ## Plan -Six implementation tasks (W1–W6). W1 is foundational. **OQ-1 gating:** W3's -checkout-backend semantics, W5's wiring + clone-complete-signal sub-unit, -W2's snapshot-store/index leg, and W6's snapshot-materialized acceptance -probe (probe 3) are **gated on the OQ-1 ruling** — drafted against Option A + -provenance-(a), they change little under B but must not merge a credential -posture or snapshot mechanism Matt has not ruled. -**Descope-survivors:** if OQ-1b is ruled "descope snapshot amortization," W1 -(lifecycle + local-dir backend + close-stamp), W2's `cloner` volume-copy -primitive, W3's seam + checkout and customer-mount backends, W4 -(`WorkspaceSource`), and the non-snapshot legs of W5/W6 still ship; what drops -is W2's snapshot-store/index, W3's snapshot-source backend + the -`TreeSource.Snapshot` path, W5's clone-complete sub-unit, and W6 probe 3. -`Snapshot` then joins `Archive`/`Restore` as **reserved-not-implemented** on -the W1 interface (its sole caller drops), keeping the honest-sentinel -discipline. Hermetic unless noted. +Six implementation tasks (W1–W6). W1 is foundational. **OQ-1 is ruled Option A +with a clone-only snapshot (DL-326, §OQ-1)**, so W3's checkout-backend +semantics, W5's wiring plus its clone-complete-signal sub-unit, W2's +snapshot-store/index leg, and W6's snapshot-materialized acceptance probe +(probe 3) all **build to that contract**: the agent self-clones in-container +(no host-side forge credential), and the snapshot carries a provably-clean +post-clone tree only (no cross-session build state). The record was drafted +against this posture, so the ruling settled the contract without reshaping any +task. Hermetic unless noted. ### W1 — volume lifecycle API + local-dir backend (`go/internal/vfs`) @@ -509,9 +503,9 @@ The package skeleton mirrors `go/internal/compute`'s layering implementations — reflink copy (`cp --reflink=always`-class, FS-supporting) and rsync-clone — chosen by a **runtime capability probe** (attempt a reflink of a probe file in the base dir at manager construction; cache the verdict; - no config knob, OQ-3). Useful under every OQ-1b ruling (it is also D4's + no config knob, OQ-3). Useful regardless (it is also D4's archive/restore copy path), so it is **not** gated. -- **Snapshot store + index (gated on OQ-1b).** A sibling subtree under the base +- **Snapshot store + index.** A sibling subtree under the base dir keyed by `VolumeSnapshotID`, plus an **`(AgentAccountID, repo)`→snapshot index** the provision path reads to set `TreeSource.Snapshot` for a new session of an already-seen `(account, repo)`. The account scope is @@ -523,9 +517,9 @@ The package skeleton mirrors `go/internal/compute`'s layering unbounded-multi-GB-liability argument, design.md:619-621, applies to the snapshot subtree exactly as to volumes); a superseded snapshot is unlinked only after the replacement commits. What counts as a snapshot-worthy (clean) - source, and when it is taken, is OQ-1b (the trigger is W5's clone-complete - sub-unit); this whole leg drops if OQ-1b is ruled "descope." -- **Depends:** W1; **snapshot-store/index leg merge-gated on OQ-1**. + source, and when it is taken, is the clone-only ruling (DL-326; the trigger + is W5's clone-complete sub-unit). +- **Depends:** W1. The snapshot-store/index leg builds (clone-only, DL-326). - **Test cycle:** hermetic on the rsync path (any FS); the reflink path needs a reflink-capable FS — CI job pinned to one, plus the probe's fallback asserted on a non-capable FS (tmpfs). Snapshot→restore @@ -535,7 +529,7 @@ The package skeleton mirrors `go/internal/compute`'s layering (P2-GC-f) or an unseen repo; taking a newer snapshot atomically supersedes the prior one (old key gone, one current key per `(account, repo)`). -### W3 — `VirtualFS` seam + backends *(gated on OQ-1)* +### W3 — `VirtualFS` seam + backends *(Option A, DL-326)* - **Interfaces:** @@ -560,19 +554,17 @@ The package skeleton mirrors `go/internal/compute`'s layering } ``` - Under Option A the checkout backend's `Materialize` prepares the - destination (snapshot-restore via W2, else an empty owned root) and - records the expected `Repo`/`Ref`; the agent writes the tree bytes - in-container. Under Option B it additionally performs the host-side - clone/fetch. The seam, `TreeSource`, and the tests' shape are identical - either way — only the checkout backend's body and W5's launch-order differ. - **`Materialize`'s post-condition varies with the OQ-1 ruling** — "destination - prepared, tree completed in-container by the agent" under A vs "tree present - at root" under B — so the interface doc-comment states the post-condition - explicitly against the ruling (an empty-dir return under A must not surprise - a future caller: C3 burst wiring, customer-VFS interop). -- **Depends:** W1, W2; **merge-gated on OQ-1** (the backend body encodes the - ruling). + Under the ruled Option A (DL-326) the checkout backend's `Materialize` + prepares the destination (snapshot-restore via W2, else an empty owned root) + and records the expected `Repo`/`Ref`; the agent writes the tree bytes + in-container. The seam, `TreeSource`, and the tests' shape were designed + decision-proof — Option B would only have swapped the checkout backend's body + (adding a host-side clone/fetch) and W5's launch-order, never the interface. + **`Materialize`'s post-condition under A is "destination prepared, tree + completed in-container by the agent"** — so the interface doc-comment states + that post-condition explicitly (an empty-dir return must not surprise a + future caller: C3 burst wiring, customer-VFS interop). +- **Depends:** W1, W2. The backend body encodes Option A (DL-326). - **Test cycle:** contract tests a fake and the real backend both pass (the S1 seam discipline, design.md:561-565); snapshot-source materialize restores the tree; customer-mount source validates and passes through; @@ -595,7 +587,7 @@ The package skeleton mirrors `go/internal/compute`'s layering in-container path; `Mount.ReadOnly=false` renders without `:ro` (`podman.go:846-850`). -### W5 — provision wiring + teardown/reprovision *(gated on OQ-1)* +### W5 — provision wiring + teardown/reprovision *(Option A, DL-326)* - **Interfaces:** `SpecBuilder` (`go/internal/runner/host.go:46-48`) derives `WorkspaceSource` + the volume mount from the provision request, and is the @@ -614,7 +606,7 @@ The package skeleton mirrors `go/internal/compute`'s layering closed-but-unexpired session re-attaches and re-materializes warm. Box loss: `Lookup`'s typed not-found routes to the cold path (create + cold materialize), logged as a capability event, never an error to the user. -- **Clone-complete signal → snapshot (sub-unit, gated on OQ-1b).** Under +- **Clone-complete signal → snapshot (sub-unit).** Under provenance-(a) the snapshot is taken from a **provably-clean post-clone tree**, so the trigger is owned here, not left implicit: (i) the agent (`packages/compass-agent`, TypeScript) emits a **clone-complete** signal to @@ -628,18 +620,18 @@ The package skeleton mirrors `go/internal/compute`'s layering ownership invariant, Layout) at the expected `Ref`, then calls `VolumeManager.Snapshot` (W1) → writes the `(AgentAccountID, repo)` index entry (W2, P2-GC-f). This is the **only caller of `Snapshot`** in P2 — - without it the verb is dead and the warm path never triggers. The whole - sub-unit dies if OQ-1b is ruled "descope snapshots," so it merges behind the - OQ-1 gate with the rest of W5. -- **Depends:** W1, W2, W3, W4; **merge-gated on OQ-1** (Option B would reorder - materialize-vs-launch; the clone-complete sub-unit is gated on OQ-1b). + without it the verb is dead and the warm path never triggers. This sub-unit + builds under the clone-only ruling (DL-326); it would have dropped only under + a "descope snapshots" ruling, which Matt did not take. +- **Depends:** W1, W2, W3, W4. Encodes Option A + clone-only (DL-326): + materialize-then-launch order, and the clone-complete sub-unit builds. - **Test cycle:** integration — provision→session→teardown→reprovision round-trip on the volume path, extended to the parent's full provision→materialize→session→**release** round-trip (design.md:561-562); W5 owns at merge: the volume survives teardown, the mount path is stable across the reattach, and a suspend-stamped volume is `Expire`-ineligible. - W5 and W6's snapshot-materialized probe (probe 3) land together behind the - OQ-1 gate; the five-probe acceptance suite (the parent's end-to-end P2 + W5 and W6's snapshot-materialized probe (probe 3) land together (Option A, + DL-326); the five-probe acceptance suite (the parent's end-to-end P2 cycle) is W6's. ### W6 — expiry reaper wiring + the P2 acceptance suite @@ -680,100 +672,92 @@ The package skeleton mirrors `go/internal/compute`'s layering - [ ] **W2** — snapshot backends: reflink probe + rsync fallback, snapshot store + `(AgentAccountID, repo)`→snapshot index (never cross-account, P2-GC-f), one-current-snapshot-per-`(account, repo)` retention, restore - path (depends: W1; **snapshot-store/index leg merge-gated on OQ-1**; - reflink CI leg FS-pinned). + path (depends: W1; snapshot-store/index leg builds the clone-only snapshot + per DL-326; reflink CI leg FS-pinned). - [ ] **W3** — `VirtualFS` seam + checkout/snapshot/customer-mount backends - (depends: W1, W2; **merge-gated on OQ-1**). + (depends: W1, W2; backend body encodes Option A per DL-326). - [ ] **W4** — `WorkspaceSource` variant + `AgentSpec.Mounts` doc amendment + mount rendering (depends: W1; parallel with W3). - [ ] **W5** — provision wiring: resolve-or-create → attach (clears stamp) → materialize → launch; `SpecBuilder` owns the P2-GC-d path triple; teardown (container → release → close-stamp with close-vs-suspend intent); agent clone-complete signal → host-side git-status-clean verify - → `Snapshot` → `(account, repo)` index write (sub-unit gated on OQ-1b); - box-loss cold path (depends: W1, W2, W3, W4; **merge-gated on OQ-1**). + → `Snapshot` → `(account, repo)` index write (clone-only snapshot, DL-326); + box-loss cold path (depends: W1, W2, W3, W4; encodes Option A per DL-326). - [ ] **W6** — expiry reaper driver + the five-probe P2 acceptance suite (rebuild-freshness + git-shape probes, not timers) (depends: W1–W5; - **probe 3 merge-gated on OQ-1**). + probe 3 asserts the clone-only warm path, DL-326). ## Open Questions -Each tagged **load-bearing** (blocks the gated tasks' merge; the caller asks -Matt) or **non-load-bearing** (deferred with rationale). The record is -drafted against each recommendation as a stated assumption. +Each tagged **load-bearing** (blocked the gated tasks' merge) or +**non-load-bearing** (deferred with rationale). OQ-1 (the one load-bearing +question) is now ruled (DL-326); the record builds to each recommendation. > Namespace: an unprefixed **OQ-N** refers to *this* record's open questions; > the parent record's are always written **parent OQ N** (space, no hyphen). -1. **[load-bearing] Clone model + snapshot provenance.** Two coupled, - load-bearing sub-decisions Matt ratifies as one package — A's cost/benefit - leans on "A can amortize snapshots," so the posture cannot be ruled without - the mechanism. - - **1a — Clone model + clone-read-credential location** (the amendment's - deferred fork, virtualfs-descope-amendment.md:57-72,123-129). - **(A)** keep the agent self-clone: Runner prepares/attaches the volume - (empty or snapshot-restored), agent clones/fetches in-container with its - existing `$HOME` machine-user token (`workspace.go:58-62`, - `agent.go:354-358`) — no new credential class, GC 8 trivially green; - **(B)** Runner-side host clone before container start — `VirtualFS` - literally materializes and sessionless prebuilds become possible, at the - price of a new host-side forge READ credential (distinct from DL-052's - Server-only write secret, `docs/designs/DECISIONS.md:83`) with - provisioning/rotation/scoping burden and a wider Runner-host blast radius, - plus launch-order churn; **(C)** a host-side read-only mirror + in-container - `file://` clone (§Alternatives) — collapses to "B with a smaller blast - radius": credential-free *for the agent*, but the mirror refresh needs the - same host-side forge read credential, merely fetch-scoped. - **Recommendation: A**, revisiting B only if a sessionless prebuild service - ever exists; the `Materialize` signature already accommodates the flip. - Ratifying A includes ratifying the stated reading of Global Constraint 2 - (seam owns destination + source-selection; the cold path's tree bytes are - written by the agent under the seam's contract). - - **1b — Snapshot-amortization provenance under A** (the parent mandates the - prebuild model, design.md:622-626). Under A the clone runs in-container - *after* `Materialize` returns, so there is no automatic clean snapshot - point, and a close-time snapshot leaks one session's untracked non-ignored - WIP into the next — a `git fetch` + checkout-delta restore does not remove - it — against `workspace.go:1-8`'s isolation. A mechanism must be chosen: - **(a, recommended)** snapshot a **provably-clean** post-clone tree — the - agent signals clone-complete to the Runner (owned by W5's clone-complete - sub-unit), the Runner verifies a `git status`-clean tree at the ref, then - snapshots; the snapshot store keys an `(account, repo)`→snapshot index - (W2, P2-GC-f) the provision path reads. Amortizes the **clone** - safely with **no new credential**; build-prebuild (a warm `target/` shared - across sessions) is deferred to the moment a prebuild *service* exists — - exactly when B's host-side credential would earn its existence. - **(b)** a Runner-driven one-shot first-clone (a scoped, single-use exception - to A) — a clean *and* built snapshot, but reintroduces a host-side read - credential for that path. **(c)** close-time snapshots restored through an - **ignore-aware** `git clean -fd` (no `-x`) + `git reset --hard` — removes - the untracked-non-ignored leak while keeping ignored caches (`target/`, - `node_modules`), but still shares any *ignored* file a session left (a - gitignored `.env`), so it does not fully close the leak. - **Recommendation: (a)**; the acceptable alternative ruling is to **descope - snapshot amortization from P2 entirely** (ship the per-session persistent - volume, defer cross-session snapshots to a follow-up), dropping W2's - snapshot leg and W6 probe 3 from P2 scope. The record is drafted against - (a). -2. **[non-load-bearing] `Archive`/`Restore` implementation timing.** The - parent puts the verbs on P2's API but their consumer is D4's cold-idle - (design.md:638-641,714-723). **Recommendation:** freeze the signatures in - W1 with honest not-implemented sentinels (the `Resize`/`ExecStreaming` - discipline, `podman.go:387-396`, `compute.go:26-29`); the object-store - backend and endpoint config land with D4, which owns the archive - thresholds anyway. No P2 executor is blocked. -3. **[non-load-bearing] Snapshot FS-capability detection.** **Recommendation:** - a runtime probe at `VolumeManager` construction (attempt a reflink copy of - a probe file in the base dir; cache the verdict; fall back to rsync) — no - operator knob until a deployment demonstrates the probe mis-detecting. - Pure mechanism; W2 owns it. -4. **[non-load-bearing — out of scope] Session→box stickiness vs the - scheduler.** Multi-box placement (which box a resuming session lands on, - collision handling) is a control-plane concern outside this repo; the - parent's D4 already designs the voluntary cold-migration relief valve - (design.md:759-770). P2 encodes only the box-local invariant: a Runner - attaches only volumes it hosts, and an absent volume is a typed error - routed to the cold path. Nothing in `go/internal/vfs` assumes or names - any particular placement layer. +### OQ-1 — Clone model + snapshot provenance *(RULED 2026-09-05, Matt)* + +**Ruling: Option A (agent self-clone) + provenance-(a) (clone-only snapshot).** +Both coupled sub-decisions were ratified together as recommended; the record +was drafted against this posture, so the ruling changes no mechanism — it +un-gates the tasks and fixes the drafted-against assumption as the contract. +Ledgered as **DL-326**. + +**1a — Clone model + clone-read-credential location: A.** The agent keeps its +self-clone: the Runner prepares/attaches the volume (empty or +snapshot-restored), the agent clones/fetches in-container with its existing +`$HOME` machine-user token (`workspace.go:58-62`, `agent.go:354-358`) — no new +credential class, GC 8 trivially green. Ratifying A also ratifies the record's +reading of Global Constraint 2 (design.md:423-428): the `VirtualFS` seam owns +the destination and source-selection, and the cold path's tree bytes are +written by the agent under the seam's contract — `Materialize` on the +fresh-clone path is preparation, not tree-writing. **Revisit trigger (Matt):** +direct forge integrations with box-global read-only clones of subscribed repos +on the Runner side are a deliberate follow-up optimization, not needed early; +that is the moment a host-side read credential (Option B/C) would earn its +existence, and the `Materialize` signature already accommodates the flip. A is +reversible by design. + +**1b — Snapshot-amortization provenance under A: clone-only (provenance-(a)).** +The snapshot carries a **provably-clean post-clone tree** and nothing else — +the agent signals clone-complete to the Runner (W5's clone-complete sub-unit), +the Runner verifies a `git status`-clean tree at the ref, then snapshots, +keyed in an `(account, repo)`→snapshot index (W2, P2-GC-f) the provision path +reads. A new session of a seen `(account, repo)` skips the cold **clone** (a +`git fetch` + checkout-delta restore) and then builds cold on its own volume. +This carries **zero cross-session leak**: a clean tree at a ref is org-public +content already (no WIP, no other session's build artifacts), and the account +scope in the index key holds the tenancy boundary (P2-GC-f). Cross-session +**build** prebuild (a warm `target/` shared across sessions) is explicitly +**out of P2 scope** — it is the part that carries the leak vector and needs a +provenance story clone-only does not, and each session already keeps its own +warm `target/` across suspend/resume via its persistent volume. Build-prebuild +rides the same future prebuild-service moment as the 1a revisit trigger. + +### OQ-2 — `Archive`/`Restore` implementation timing *(non-load-bearing)* + +The parent puts the verbs on P2's API but their consumer is D4's cold-idle +(design.md:638-641,714-723). **Recommendation:** freeze the signatures in +W1 with honest not-implemented sentinels (the `Resize`/`ExecStreaming` +discipline, `podman.go:387-396`, `compute.go:26-29`); the object-store +backend and endpoint config land with D4, which owns the archive +thresholds anyway. No P2 executor is blocked. + +### OQ-3 — Snapshot FS-capability detection *(non-load-bearing)* + +**Recommendation:** a runtime probe at `VolumeManager` construction (attempt a +reflink copy of a probe file in the base dir; cache the verdict; fall back to +rsync) — no operator knob until a deployment demonstrates the probe +mis-detecting. Pure mechanism; W2 owns it. + +### OQ-4 — Session→box stickiness vs the scheduler *(non-load-bearing — out of scope)* + +Multi-box placement (which box a resuming session lands on, collision +handling) is a control-plane concern outside this repo; the parent's D4 +already designs the voluntary cold-migration relief valve +(design.md:759-770). P2 encodes only the box-local invariant: a Runner +attaches only volumes it hosts, and an absent volume is a typed error +routed to the cold path. Nothing in `go/internal/vfs` assumes or names +any particular placement layer. From 97b2a38bfdef686c24949572f2d72c85621194e5 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 5 Sep 2026 11:18:35 -0400 Subject: [PATCH 6/7] =?UTF-8?q?docs(runtime):=20fold=20OQ-1-ruling=20revie?= =?UTF-8?q?w=20round=201=20=E2=80=94=20reconcile=20stranded=20prose=20(RIG?= =?UTF-8?q?-2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 1 of the review loop on the OQ-1-ruling fold returned 0 high, 3 medium, 5 low. All three mediums are reconciliation misses (the reviewer routed all to driver auto-fix, none to Matt); folded here with three clean lows. ### Mediums - **§VirtualFS-seam definitional prose** still read "under the *recommended* clone model … *if OQ-1 is ruled the other way*", telling a W3 executor the fork was still open. Reconciled to the ruled Option A + the counterfactual-past framing already used in the W3 task body. - **§The volume snapshot bullet** still read "… *is what Matt ratifies*" (future tense) for the OQ-1b mechanism. Reframed to the settled clone-only ruling (DL-326). - **OQ-1b rationale** introduced a "a clean tree at a ref is org-public content already" justification that directly contradicts the record's own **P2-GC-f** ("cleanliness is not authorization — a provably-clean tree of a private repo is still that repo's content"). Deleted the clause; the account-scoped index key now carries the tenancy-boundary claim alone, matching P2-GC-f. ### Lows folded - W2 "Volume-copy primitive" dropped the vestigial "(ungated)" label + "so it is not gated" clause (the only gate is gone; the label falsely implied its sibling bullet was still gated). - §Alternatives "recommends and sequences around it rather than deciding it" gained a closing clause noting Matt ruled it (DL-326); the Option C ruling-input note moved to past tense. - DL-326 ledger row: "read-only **bare** clones" → "**box-global** read-only clones", aligning to Matt's words and the record body ("bare" is a git concept Matt did not use; "box-global" is the property that distinguishes the follow-up). Two lows kept as reviewed dispositions: the §Approach Options A/B/C tense (preserved pre-ruling deliberation narrative) and DL-326's section placement (beside its cited sibling DL-052, per the reviewer's own note). Spec-impact: none. Refs RIG-2395 Co-authored-by: Matt Wilkinson --- docs/designs/DECISIONS.md | 2 +- .../p2-persistent-session-volume.md | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index fc8dbadc..8e636f35 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -112,7 +112,7 @@ check enforces the mechanical half. Full rationale: | DL-314 | Postgres remains the store of record and the sole durability source of truth for committed comms/routing/session-binding state (restating DL-019's surviving "Postgres is the store of record" clause verbatim): JetStream is an at-least-once delivery TRANSPORT whose consumer state is disposable, never a second truth store; every fabric recovery path terminates in a Postgres cursor or row. Supersedes DL-019's "JetStream is comms-only" clause only — its store-of-record and transcript-blob-seam clauses (DL-093) survive (RIG-2861 OQ-1) | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#q3--the-eventing-substrate-one-nats-eventfabric-a-standalone-stack-service-jetstream-as-the-delivery-transport) | | DL-315 | The cross-tenant background/system loops (delivery-cursor sweep, deliver-ack advance, reattach recovery, lag-resync) run under a narrowly-scoped `BYPASSRLS` system role granted ONLY to those named background workers and NEVER on the request path; every request-path query stays fail-closed under RLS (RIG-2861 OQ-4 = option 1) | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#resolved-decisions-freeze--matt-2026-08-31) | | DL-317 | Under multi-tenancy, user/system handle uniqueness is PER-ORGANIZATION not global (RIG-2921 = option A): `account_handles` gains a `tenant_id` and its partial-unique indexes become org-scoped, and the handle resolvers gain a tenant filter — two organizations may each hold `@matt`. The sole globally-unique identifier is the organization NAME, CASE-FOLDED for that uniqueness check (normalized index form, display casing preserved) — `Acme` and `acme` are the same organization (GitHub model). Refines DL-271's "user/system handles globally unique" clause to per-organization under multi-tenancy (single-tenant OSS stays degenerate: one org, so per-org == global); implemented on the RIG-2880 `account_handles` storage contract | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#resolved-decisions-freeze--matt-2026-08-31) | -| DL-326 | The P2 session-volume clone model is **agent self-clone (Option A)**: the Runner prepares/attaches the volume (empty or snapshot-restored) and the agent clones/fetches in-container with its existing `$HOME` machine-user token — no new host-side forge read credential (distinct from DL-052's Server-only write secret). Snapshot amortization is **clone-only (provenance-(a))**: the snapshot carries a provably-clean post-clone tree (agent clone-complete signal → Runner host-side `git status`-clean verify → snapshot, keyed by an `(account, repo)` index), never cross-session build state (`target/`, caches), so a new session of a seen `(account, repo)` skips the cold clone but builds cold on its own volume — zero cross-session leak. Runner-side read-only bare clones of subscribed repos are a deferred follow-up optimization, and a host-side read credential earns its existence only when a sessionless prebuild service materializes trees with no session; the `Materialize` signature already accommodates that flip. Resolves this record's OQ-1 (both sub-parts). | Active (Matt, 2026-09-05) | [P2 persistent session volume §OQ-1](infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md#oq-1--clone-model--snapshot-provenance-ruled-2026-09-05-matt) | +| DL-326 | The P2 session-volume clone model is **agent self-clone (Option A)**: the Runner prepares/attaches the volume (empty or snapshot-restored) and the agent clones/fetches in-container with its existing `$HOME` machine-user token — no new host-side forge read credential (distinct from DL-052's Server-only write secret). Snapshot amortization is **clone-only (provenance-(a))**: the snapshot carries a provably-clean post-clone tree (agent clone-complete signal → Runner host-side `git status`-clean verify → snapshot, keyed by an `(account, repo)` index), never cross-session build state (`target/`, caches), so a new session of a seen `(account, repo)` skips the cold clone but builds cold on its own volume — zero cross-session leak. Runner-side box-global read-only clones of subscribed repos are a deferred follow-up optimization, and a host-side read credential earns its existence only when a sessionless prebuild service materializes trees with no session; the `Materialize` signature already accommodates that flip. Resolves this record's OQ-1 (both sub-parts). | Active (Matt, 2026-09-05) | [P2 persistent session volume §OQ-1](infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md#oq-1--clone-model--snapshot-provenance-ruled-2026-09-05-matt) | > Note (2026-07-31, RIG-1570 R5): DL-065's retired internal `ResumeContext > resume = 12` envelope field is NOT silently reintroduced — the collapsed diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md index dbf04ca6..a0b9b8ba 100644 --- a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -153,8 +153,9 @@ Concretely: otherwise; a later session on the same repo restores the snapshot and starts warm on `git fetch` + checkout-delta instead of a full clone. *What* gets snapshotted, *when* (the clean-tree stamp point), and the `(account, repo)`→snapshot - index the provision path reads are the load-bearing sub-fork OQ-1b — the - amortization mechanism, not just the clone posture, is what Matt ratifies. + index the provision path reads are settled by the ruled OQ-1b (clone-only, + DL-326): a provably-clean post-clone tree, stamped at W5's clone-complete + signal, keyed in the W2 index. Detection of the copy primitive is a runtime capability probe, not a config knob (OQ-3). `VolumeSnapshotID` stays an **opaque string** (frozen so by the parent, design.md:536-537); its P2 production shape is the snapshot store's @@ -192,13 +193,13 @@ backends in siblings, session-scoped construction): - `Release` detaches/cleans the materialized root without destroying volume contents (volume destruction is `Expire`'s, never `Release`'s). -Under the recommended clone model (next section), the P2 checkout backend's +Under the ruled clone model (Option A, DL-326), the P2 checkout backend's `Materialize` **prepares** the destination — snapshot-restore when `TreeSource.Snapshot` is set, else an empty owned root — and the agent -completes the tree (clone or fetch-delta) in-container. If OQ-1 is ruled the -other way, the same signature holds and the backend performs the full -host-side clone; the seam shape is decision-proof, which is exactly why the -amendment let it wait for P2. +completes the tree (clone or fetch-delta) in-container. The signature was +designed decision-proof: Option B would have kept it while performing the full +host-side clone in the backend body — which is why the amendment let the seam +wait for P2. ### The clone/credential fork — the record's central decision (OQ-1) @@ -330,8 +331,9 @@ contract violation. snapshots here are dumb FS-level copies keyed by opaque id. - **Ruling the clone fork inside this record.** Rejected — it is exactly the decision the amendment marked load-bearing for Matt - (virtualfs-descope-amendment.md:123-129); this record recommends and - sequences around it (OQ-1) rather than deciding it. + (virtualfs-descope-amendment.md:123-129); this record recommended and + sequenced around it (OQ-1) rather than deciding it — Matt ruled it on + 2026-09-05 (DL-326, §OQ-1), which this record now records. - **Option C — a host-side read-only bare mirror + in-container `file://` clone.** The codebase names this pattern: `mountArg`'s doc calls the read-only mount "the shared bare-repo cache" (`podman.go:842-844`), @@ -347,8 +349,8 @@ contract violation. or B, and against `workspace.go:1-8`'s one-agent-one-clone isolation). C thus collapses to "B with a smaller blast radius." Recorded because a reviewer or executor will see the mirror pattern in the code and ask, and because the - fetch-scoped-vs-clone-scoped credential narrowing is a genuine input to - Matt's OQ-1 ruling. + fetch-scoped-vs-clone-scoped credential narrowing was a genuine input to + Matt's OQ-1 ruling (ruled Option A, DL-326). ## Global Constraints @@ -499,12 +501,12 @@ The package skeleton mirrors `go/internal/compute`'s layering ### W2 — snapshot backends: reflink with rsync fallback -- **Volume-copy primitive (ungated).** An unexported `cloner` seam with two +- **Volume-copy primitive.** An unexported `cloner` seam with two implementations — reflink copy (`cp --reflink=always`-class, FS-supporting) and rsync-clone — chosen by a **runtime capability probe** (attempt a reflink of a probe file in the base dir at manager construction; cache the verdict; no config knob, OQ-3). Useful regardless (it is also D4's - archive/restore copy path), so it is **not** gated. + archive/restore copy path). - **Snapshot store + index.** A sibling subtree under the base dir keyed by `VolumeSnapshotID`, plus an **`(AgentAccountID, repo)`→snapshot index** the provision path reads to set `TreeSource.Snapshot` for a new @@ -727,9 +729,10 @@ the Runner verifies a `git status`-clean tree at the ref, then snapshots, keyed in an `(account, repo)`→snapshot index (W2, P2-GC-f) the provision path reads. A new session of a seen `(account, repo)` skips the cold **clone** (a `git fetch` + checkout-delta restore) and then builds cold on its own volume. -This carries **zero cross-session leak**: a clean tree at a ref is org-public -content already (no WIP, no other session's build artifacts), and the account -scope in the index key holds the tenancy boundary (P2-GC-f). Cross-session +This carries **zero cross-session leak**: the snapshot holds no WIP and no +other session's build artifacts, and the account scope in the index key — not +the tree's cleanliness — holds the tenancy boundary (P2-GC-f: cleanliness is +not authorization). Cross-session **build** prebuild (a warm `target/` shared across sessions) is explicitly **out of P2 scope** — it is the part that carries the leak vector and needs a provenance story clone-only does not, and each session already keeps its own From 1610a30fa7909523608f109bfeb7677f96aeb87d Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 5 Sep 2026 11:52:51 -0400 Subject: [PATCH 7/7] =?UTF-8?q?docs(runtime):=20fold=20OQ-1-ruling=20revie?= =?UTF-8?q?w=20round=202=20=E2=80=94=20record=E2=86=94ledger=20revisit-tri?= =?UTF-8?q?gger=20reconcile=20(RIG-2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 of the fresh review loop on the OQ-1-ruling change to the P2 persistent-session-volume record returned 0 high / 2 medium / 4 low; round-1 closure verified clean (MED-1/2/3 + LOW-1/3/4 all landed, no new inconsistency, gates green). This folds the two floor findings plus the four mechanical lows. MED-1 (record↔ledger contract divergence): the round-1 fold's revisit-trigger sentence (§OQ-1 1a) collapsed DL-326's two distinct follow-ups into one, reading as though a host-side read credential earns its existence the moment box-global clones are built. DL-326 states they are separate: box-global read-only clones are a deferred optimization, and *separately* a host-side credential earns existence only at a sessionless prebuild service. Rewrote the record's clause to the ledger's two-clause shape (matching §Recommendation :281-283), and aligned P2-GC-b to name the prebuild-service moment as the sole reopening trigger. Lows folded: §Approach snapshot bullet "materialized"→"cloned" (ruled Materialize = preparation, not tree-writing); §Alternatives doubled "records"→"reflects"; §Open-Questions preamble split OQ-1-is-a-contract from OQ-2..4-are-recommendations; 1b block gained a provenance-(a) label gloss + the rejected close-time git-clean-fd defeater (still shares ignored files); 1b line reflow. Both gates green: markdownlint 0 errors; design-ledger-gate OK (293 rows, 123 headers), DL-326 anchor resolves. Refs RIG-2395 Co-authored-by: Matt Wilkinson --- .../p2-persistent-session-volume.md | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md index a0b9b8ba..f4858ae5 100644 --- a/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md +++ b/docs/designs/infra/runtime/compass-elastic-session-runtime/p2-persistent-session-volume.md @@ -147,7 +147,7 @@ Concretely: session is closed AND whose close-stamp is past the deadline; a live or suspended session's volume is never eligible. - **Snapshots for first-clone amortization** (the Codespaces prebuild model, - parent design.md:622-626): a freshly materialized, **provably-clean** tree + parent design.md:622-626): a freshly cloned, **provably-clean** tree is snapshotted at the FS layer where the box's filesystem supports reflink/snapshot (btrfs/XFS/bcachefs reflink copy), by rsync-clone otherwise; a later session on the same repo restores the snapshot and starts @@ -333,7 +333,7 @@ contract violation. decision the amendment marked load-bearing for Matt (virtualfs-descope-amendment.md:123-129); this record recommended and sequenced around it (OQ-1) rather than deciding it — Matt ruled it on - 2026-09-05 (DL-326, §OQ-1), which this record now records. + 2026-09-05 (DL-326, §OQ-1), which this record now reflects. - **Option C — a host-side read-only bare mirror + in-container `file://` clone.** The codebase names this pattern: `mountArg`'s doc calls the read-only mount "the shared bare-repo cache" (`podman.go:842-844`), @@ -369,9 +369,10 @@ session path stays green (8), version floors (9). P2-specific additions: - **P2-GC-b — no new credential surface.** The ruled Option A + clone-only (DL-326) introduces no host-side forge credential — the agent self-clones in-container with its existing `$HOME` token — and no P2 task may introduce - one. This binds the ruled posture; a future Option B/C or build-prebuild - service (the DL-326 revisit trigger) would re-open this constraint under its - own decision. + one. This binds the ruled posture; the DL-326 revisit trigger — a sessionless + prebuild *service* that materializes trees with no session — would re-open + this constraint under its own decision (Option B/C earns its host-side read + credential only there). - **P2-GC-c — volume destruction only via `Expire`.** `Release`, `Teardown`, eviction, crash, and failed launches never delete volume contents; the only reclaim path is the policy reaper (the parent's GC 5 made mechanical). @@ -694,7 +695,8 @@ The package skeleton mirrors `go/internal/compute`'s layering Each tagged **load-bearing** (blocked the gated tasks' merge) or **non-load-bearing** (deferred with rationale). OQ-1 (the one load-bearing -question) is now ruled (DL-326); the record builds to each recommendation. +question) is ruled (DL-326) and is a contract; the non-load-bearing OQ-2..OQ-4 +stay recommendations the record is drafted against as stated assumptions. > Namespace: an unprefixed **OQ-N** refers to *this* record's open questions; > the parent record's are always written **parent OQ N** (space, no hyphen). @@ -716,10 +718,11 @@ reading of Global Constraint 2 (design.md:423-428): the `VirtualFS` seam owns the destination and source-selection, and the cold path's tree bytes are written by the agent under the seam's contract — `Materialize` on the fresh-clone path is preparation, not tree-writing. **Revisit trigger (Matt):** -direct forge integrations with box-global read-only clones of subscribed repos -on the Runner side are a deliberate follow-up optimization, not needed early; -that is the moment a host-side read credential (Option B/C) would earn its -existence, and the `Materialize` signature already accommodates the flip. A is +two separate follow-ups, not one — box-global read-only clones of subscribed +repos on the Runner side are a deferred follow-up optimization (not needed +early), and *separately* a host-side read credential (Option B/C) earns its +existence only when a sessionless prebuild *service* materializes trees with no +session; the `Materialize` signature already accommodates that flip, so A is reversible by design. **1b — Snapshot-amortization provenance under A: clone-only (provenance-(a)).** @@ -732,13 +735,19 @@ reads. A new session of a seen `(account, repo)` skips the cold **clone** (a This carries **zero cross-session leak**: the snapshot holds no WIP and no other session's build artifacts, and the account scope in the index key — not the tree's cleanliness — holds the tenancy boundary (P2-GC-f: cleanliness is -not authorization). Cross-session -**build** prebuild (a warm `target/` shared across sessions) is explicitly +not authorization). Cross-session **build** prebuild (a warm `target/` shared +across sessions) is explicitly **out of P2 scope** — it is the part that carries the leak vector and needs a provenance story clone-only does not, and each session already keeps its own warm `target/` across suspend/resume via its persistent volume. Build-prebuild rides the same future prebuild-service moment as the 1a revisit trigger. +The parenthetical *(provenance-(a))* is the pre-ruling option label for this +clean-post-clone-tree snapshot; the alternative close-time approach — an +ignore-aware `git clean -fd` + `git reset --hard` — was rejected because it +still shares any *ignored* file (e.g. a gitignored `.env`) a session left +behind, so it does not close the leak. + ### OQ-2 — `Archive`/`Restore` implementation timing *(non-load-bearing)* The parent puts the verbs on P2's API but their consumer is D4's cold-idle