Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c580941
sandboxd: add read-only catalog volumes
CMGS Aug 11, 2026
d939c7c
review: keep type declarations contiguous with their method sets
CMGS Aug 11, 2026
66f4425
review: tighten volume comments
CMGS Aug 11, 2026
c2d3345
types: own the direct-I/O vocabulary and volume-name projection
CMGS Aug 11, 2026
141c733
server: align entry volume-validation bodies; pin the volume wire sur…
CMGS Aug 11, 2026
04c629e
sandboxd: complete warm read-only volume claims
CMGS Aug 11, 2026
842b233
review: reunite split declarations and helper placement
CMGS Aug 11, 2026
234475b
sdk: move shared RPC plumbing to utils.go
CMGS Aug 11, 2026
58b6470
review: drop restating comments
CMGS Aug 11, 2026
194d284
review: test request contexts and package-level imports
CMGS Aug 11, 2026
6266475
review: name repeated func types
CMGS Aug 11, 2026
a2868a5
pool: drop dead volume guards; single-pass applyVolumes
CMGS Aug 11, 2026
05cfb18
engine: share the silkd stream drain; tighten device-wait error
CMGS Aug 11, 2026
f39b476
server: hoist the volume-owner fallback; share closeWrite
CMGS Aug 11, 2026
c41934d
pool: return golden resolution as a struct
CMGS Aug 11, 2026
bf4f5b2
e2e: volumesmoke uses the harness claim prologue
CMGS Aug 11, 2026
ec3d7e0
review: move fsChunk beside its consumer; trim silkdStream doc
CMGS Aug 11, 2026
a5f53cb
store: share the remove-entries sweep
CMGS Aug 11, 2026
a380f75
peer: expire the redirect cache via maps.DeleteFunc
CMGS Aug 11, 2026
11447a4
pool: forkSource reuses sourceSnap
CMGS Aug 11, 2026
c65f4ef
silkdtest: drop the unused Fake.Serve
CMGS Aug 11, 2026
e7e2cf3
silkdtest: ListenHybrid reuses acceptLoop
CMGS Aug 11, 2026
2f9a9b7
python: share the close-on-exit context manager
CMGS Aug 11, 2026
7d22a90
review: tighten comments and group the smoke helpers
CMGS Aug 12, 2026
256b97c
silkd: end a watch on a failed write like an EOF disconnect
CMGS Aug 12, 2026
5b0cdfc
e2e: android and browser smoke use the harness claim prologue
CMGS Aug 12, 2026
80633ef
silkdtest: send DataResp as a pointer like its siblings
CMGS Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ performance) — source in
baked into the base image
- `sandboxd/` — per-node control plane (Go): warm pools refilled from golden
snapshot exports (online-retunable), claim/release/hibernate/fork/promote/
checkpoint HTTP API, signed preview URLs, the HTTP-upgrade byte relay to
silkd, usage + audit journals, /metrics, reap + restart reconcile,
memberlist mesh with redirect placement
checkpoint HTTP API, operator-catalog read-only dataset volumes, signed
preview URLs, the HTTP-upgrade byte relay to silkd, usage + audit journals,
/metrics, reap + restart reconcile, memberlist mesh with redirect placement
- `sdk/go/` — Go SDK (stdlib-only): `Connect/New/Lookup`, `Exec/Run`, files,
`Push/Pull`, sessions, `Find/Replace`, `Watch`, git verbs, `OpenPty`,
`Fork/Hibernate/Promote/Checkpoint`, `DialPort/ProxyPort/PreviewURL`,
Expand All @@ -54,8 +54,9 @@ performance) — source in
- `e2e/` — in-process full-stack tests (real pool/engine/relay/SDK, fake
cocoon+guest) plus bare-metal acceptance drivers under `cmd/`: `demo`,
`smoke`, `meshsmoke`, `crossnode`, `coldproof`, `egresssmoke`,
`interceptsmoke`, `lifecycle` (idle→hibernate→archive), `androidsmoke`,
`browsersmoke`, and the `pullbench`/`pushbench`/`rpcbench` perf drivers
`interceptsmoke`, `volumesmoke`, `lifecycle` (idle→hibernate→archive),
`androidsmoke`, `browsersmoke`, and the `pullbench`/`pushbench`/`rpcbench`
perf drivers
- `boot/kernel/` — kernel version pin (`VERSION` + matching tarball `SHA256`,
bump both together) + config fragment (amd64: over `x86_64_defconfig` +
`kvm_guest.config`; arm64: over `defconfig` + `sandbox-arm64.config`)
Expand Down Expand Up @@ -102,6 +103,8 @@ TEMPLATE=rt:24.04 scripts/sandboxd-e2e.sh
# `ip link add br0 type bridge` with no uplink is enough (NIC, not network).
# SANDBOXD_BIN/DEMO_BIN/SMOKE_BIN point at prebuilt binaries for nodes
# without a Go toolchain.
# VOLUME_IMAGE=/absolute/dataset.img enables the read-only sharing proof; the
# image contains volume-e2e.txt. Prebuilt runs also set VOLUME_SMOKE_BIN.
```

## CI
Expand Down
48 changes: 39 additions & 9 deletions docs/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

A cluster is a set of sandboxd nodes joined through a
[hashicorp/memberlist](https://github.com/hashicorp/memberlist) SWIM mesh.
Gossip carries only placement hints — per-pool warm counts and each node's
data-plane address. Per-sandbox state never leaves its owning node, so a
stale view costs at most one extra redirect, never correctness. A single
node with no seeds is a valid mesh of one.
Gossip carries only placement hints — per-pool warm counts, promoted-template
hashes, available volume names, and each node's data-plane address.
Per-sandbox state never leaves its owning node, so a stale view costs at most
one extra redirect, never correctness. A single node with no seeds is a valid
mesh of one.

## Joining

Expand Down Expand Up @@ -57,6 +58,32 @@ Node death is honest: a dead node's sandboxes die with it (memory state is
node-local by design). SWIM detects the death and peers stop redirecting to
it.

### Read-only volumes and placement

A volume name has one fleet-wide meaning and access list, while catalog
membership is node-local and deliberately excluded from the cluster config
digest. Nodes gossip only their currently available catalog names: host paths
and access lists never leave the node. After config load the set appears on the
next gossip tick; later image distribution or removal is detected the same way.
The node epoch bumps only when the advertised name set changes.

A volume claim may consume an ordinary warm VM because attach happens after the
pop and before finalization. Warm candidates retain their normal ranking, but a
candidate must advertise every requested volume. If the entry node cannot serve
all requested names, it redirects once to a peer advertising their intersection.
A promoted-template claim uses the intersection of template owners and volume
owners first. If that advertised intersection is empty, a volume holder gets one
chance to prove it can resolve the template from a shared store. The target
retries with `no_redirect` plus the carried `require_promoted` intent and
validates both resources before provisioning, so a node-local template still
fails without a second hop even while template gossip is one tick stale.

`GET /v1/volumes` and the SDK discovery calls return the gossiped union filtered
through the answering node's fleet-uniform access lists. `nodes` counts members
advertising each name, while `available` and `size_bytes` describe only the
answering node's image. No node address or dataset-to-host mapping is returned;
claim placement resolves the holder.

## Querying members

`GET /v1/info` (root `api_token`) reports this node's pools plus the peer
Expand Down Expand Up @@ -119,9 +146,9 @@ Gossip is eventually consistent: a template promoted a moment ago may be
invisible to name-based calls for about a gossip tick (the claim fails
cold — retry), and one deleted a moment ago may still redirect to a 404.
Correctness is never violated; only the name-based convenience lags. The
handle `Sandbox.Promote` returns is still owner-bound`template.New` and
`template.Delete` dial the owner directly, no gossip involved — and is the
race-free choice immediately after a promote.
handle `Sandbox.Promote` returns is still owner-bound. `template.Delete` and a
`template.New` without volumes dial the owner directly; a volume claim may use
one placement redirect to a node that can resolve both resources.

## Checkpoints on a cluster

Expand Down Expand Up @@ -179,10 +206,10 @@ fully from it:

| state | source of truth | survives restart |
|---|---|---|
| operator config (`tenants`, `secrets`, egress policies, `bridges`/`networks`, `mesh`, `preview_secret`, `egress_ca`) | `config.json` (human/deploy-tool owned) | re-read at boot |
| operator config (`tenants`, `volumes`, `secrets`, egress policies, `bridges`/`networks`, `mesh`, `preview_secret`, `egress_ca`) | `config.json` (human/deploy-tool owned) | re-read at boot |
| API-applied pool targets (`PUT /v1/pools`) | `<data_dir>/pools.json` (machine owned) | yes |
| claims | the claims journal + `Reconcile` | yes |
| placement hints (warm counts, template sets) | gossip | rebuilt |
| placement hints (warm counts, template and volume sets) | gossip | rebuilt |
| checkpoint ownership | a live per-request probe (no gossip); a healed replica is this node's own persisted copy, aged out by `checkpoint_ttl_hours` | yes |

Pools are managed API-first. The first time a node takes `PUT /v1/pools`, it
Expand Down Expand Up @@ -235,3 +262,6 @@ the mesh.
- `cluster_key` set if the gossip network is not otherwise trusted
- pool changes via `Client.SetPoolsCluster` (or per-node `SetPools`); the applied
set persists to `pools.json` and survives restart
- keep each volume name's dataset identity and access list identical across the
fleet, distribute its immutable image to every node meant to advertise it,
and verify the fleet view and holder count with `GET /v1/volumes`
Loading