Skip to content

Latest commit

 

History

History
506 lines (452 loc) · 37.3 KB

cli.md

File metadata and controls

506 lines (452 loc) · 37.3 KB

The slates command

This guide describes the implemented command grammar (the A-9 review was 2026-09-05; mount/unmount, the merge verbs, --json on every verb, and mcp were added 2026-09-09). The unified design §4.12 describes the target interface; the gap ledger §8i tracks what is still missing.

The current CLI is a development surface. --locked records intent without establishing locked backing, and bounded/dynamic claims do not yet provide the design's host-capacity guarantee. A daemon restart under the same anchor keeps acknowledged content and its snapshots (acknowledged_content_and_its_snapshot_survive_a_daemon_restart_byte_for_byte); the anchor's own death loses RAM by design. attach returns an attachment record without creating a mounted path. A CLI attachment outlives the slates attach that made it, for later slates runs to name (write ID ATTACHMENT, detach ATTACHMENT, advance). It lasts one term, the daemon's failover SLO (10 s unless slates anchor is given another), and every verb that names it renews the term. It ends at the term's lapse with the write lease it held. A verb that names it after is refused AttachmentExpired, never NotFound: attach again. An SDK client's attachments end with the client instead. exec is Linux-only and currently expects an externally supplied SLATES_ROOT naming an established root mount. There is no implemented CLI/MCP virtio-fs or OCI attachment flow; the MCP server (slates mcp) and the Python and Node SDKs are implemented (§4.12). On macOS a volume can be mounted for real over the loopback NFS bridge (slates mount) with no privilege, kernel extension or Apple entitlement. These limitations are open correctness/integration work, not optional setup steps.

Running a daemon

slates anchor [--instance NAME] [--quick] [--shards N] [--fleet PATH --node NAME]

The anchor measures the machine profile (seconds; --quick for tests), creates the shared segment in RAM, publishes the profile into it, and supervises slates daemon as a child with the segment handed over in its environment. It restarts a daemon that exits, kills one whose heartbeat lapses, and gives up on a crash loop (the bound is derived from the recovery budget and the measured daemon start). SIGINT or SIGTERM stops both. This path is intended to avoid explicit disk writes; an anonymous shared segment alone does not establish locked residency or no swapping.

slates daemon run alone measures a profile and serves without an anchor (development).

Creating the first consensus group

After starting a new standalone daemon or the first node of a new fleet, run from another terminal:

slates --instance NAME bootstrap root

Recovering a lost consensus quorum

Warm daemon restarts recover both groups from anchor RAM automatically. Whole-anchor loss joins under a fresh member identity when each group still has a quorum.

If a group has lost its quorum, compare slates recovery-plan root --json (or region) on the surviving nodes. The plan identifies the retained committed position, application version, uncommitted tail and former voters. Choose the most complete retained copy of the same group. Fence the entire former group and prevent its clients from continuing before recovery. Unreachable copies may contain newer committed state; recovery cannot rule out that loss.

slates recovery-plan root --json
slates recover root --confirm PLAN --fenced --accept-loss --json

Before starting the anchor, operators on any platform can set SLATES_RECOVERY_KEY to a read-only file containing exactly 32 random bytes (not all zero), provisioned separately for that node. Set the same path in the recovery CLI's environment. A Kubernetes Secret mount, a VM secret mount or an operator-owned local file uses the same reader. Never share this key across nodes; it authorizes consensus recovery only. The daemon reads it at startup. An unreadable, zero or wrong-sized configured key refuses; restart with the new key to rotate it.

With no configured recovery key, approval requires the anchor's human capability (the named issuer surface on macOS/Windows; Linux operators should provision the recovery key). The second command refuses if the reviewed state or recovery authority has changed. Its reply names the replacement GROUP. On each other survivor, explicitly authorize joining that group:

slates recovery-plan root --join-group GROUP --json
slates recover root --join-group GROUP --confirm PLAN --fenced --accept-loss --json

A pending join stops voting and proposing in the old group but retains its state until the replacement validates. It survives a warm restart and refuses a replacement older than the node's known application version. Discovery cannot authorize recovery. Use region in the same commands when recovering a regional council.

Run it once on one node of the first region. For each additional region, wait for the root to admit that region, then run slates bootstrap region once on one of its nodes. The authenticated local account may issue bootstrap; an enrolled consumer or forwarded fleet request cannot. The CLI binds the request to the current daemon member id, so automatic retries cannot bootstrap a replacement.

Startup alone serves discovery and status but refuses writes until initialized and admitted. A daemon restarted under its anchor keeps its Raft state and its member id: the anchor holds both groups and the voter identity, retained before any consensus reply escapes, so it rejoins without bootstrap. A start that lost the anchor's RAM (the anchor itself died, or the pod was replaced) has no Raft state and uses a fresh member id. It joins through a surviving voting quorum. A standalone node that lost its anchor loses the sole quorum, requiring explicit new-group creation; that does not restore a lost consensus history. See AUD-07 and §4.8 "Consensus lifetime". Never add bootstrap to a pod's recurring startup command or readiness probe.

Deploying a fleet

The discovery and admission protocol is the same for local processes, bare-metal hosts, VMs, and Kubernetes pods (R8). Use loopback addresses with distinct port pairs for a local fleet, or routable IPs/DNS names for separate hosts. There is no Kubernetes API call in the protocol.

Configured peers discover each other's current addresses and boot identities automatically. DNS names are resolved again on reconnect; authenticated replacements fetch the existing group's state and join through Raft. Only creating the first group requires bootstrap. Discovery currently uses the manifest's pinned identities. Finding and enrolling previously unlisted nodes requires a discovery provider and a trust-enrollment protocol; those are not implemented. A discovery response alone cannot grant voting authority or create a new group.

A fleet is several daemons on several machines that replicate each other's volume heads and content and take over for a dead member. Every node is started from one shared manifest with its own --node:

slates anchor --fleet /etc/slates/fleet.json --node a
{
  "name": "slates-fleet",
  "f": 1,
  "nodes": [
    { "node": "a", "address": "10.0.0.1:7000", "certificate": "a.crt.der", "key": "a.key.der" },
    { "node": "b", "address": "10.0.0.2:7000", "certificate": "b.crt.der", "key": "b.key.der" },
    { "node": "c", "address": "10.0.0.3:7000", "certificate": "c.crt.der", "key": "c.key.der" }
  ]
}
  • name is the TLS name every node's certificate carries (its subject alternative name); peers verify each other's sessions under it. Certificates and keys are DER files the operator provisions, named relative to the manifest; every certificate is read (peers pin them, and a node's stable anchor is derived from its certificate), only this node's key is read. Its live member id also includes a fresh boot nonce. Material the TLS stack cannot use (a key that does not match the certificate, a key shape it does not take) stops the boot naming the node.
  • f is the regional fault tolerance: a write commits once f + 1 nodes hold it. Regional consensus uses 2f + 1 voters; the separate root group must also retain a quorum (see "Creating the first consensus group" above). A manifest that could never commit (fewer than f + 1 nodes) is refused.
  • address is the IP or DNS name peers dial and the node's base port: it serves probes on that UDP port and records on the next, every peer on the same two sockets, so open those two ports. Every node computes the same map from the same file. A name (slates-0.slates.default.svc.cluster.local:7000 — the Kubernetes deployment of deploy.md names each node by its per-pod DNS name) is resolved by the daemon at every fresh dial through the nameservers of this host's /etc/resolv.conf, so a peer that came back under a new address (a rescheduled pod) is reached on the next re-dial; a name that does not resolve is counted under the fleet.resolve refusal in status and dialed again next period. A named node binds its own two sockets on every interface. A manifest that names a node on a host with no IPv4 nameserver in /etc/resolv.conf stops the boot naming the file. Both sockets are taken up before the daemon starts: an address already in use stops the boot with fleet serve: the probe serve socket could not be taken up at ADDRESS (or record). Under slates anchor --fleet, the anchor binds the two ports once and keeps them across daemon restarts, so no other process can take them while a daemon restarts. A supervisor of your own can do the same: pass the two bound sockets as inherited descriptors and name them in SLATES_ANCHOR_FLEET_SERVE=PROBE_FD,RECORD_FD. A socket bound anywhere but the node's planned address is refused by name.
  • domain and region are optional per-node non-negative integers, both unset by default and set only for a real topology. domain is the node's failure domain (a rack or zone id): placement forms each object's copyset across distinct domains, so nodes that share a domain are treated as co-located. region is the node's region: the root group across regions agrees on which regions exist and routes cross-region, and a node with no region is in the single default region (a fleet that declares none is one region). Neither is derived from the address — several nodes may share one host and IP in a test or dev fleet without sharing a domain or region.
  • durability is an optional fleet-level object, { "accepted_loss": <0..1>, "coincident_failures": <n> }, the operator's accepted probability of losing some object when coincident_failures hosts fail at once. It is unset by default (no check). When set, every configuration change is checked against it and a breach is surfaced as a health signal (never silently over-scattered): a breach is a recovery-vs-durability conflict to resolve by raising f, the re-replication bandwidth, or the failure-domain granularity. It is a policy, so it is stated, never derived.
  • mirrors is an optional fleet-level object mapping a region id to its mirror region id, e.g. { "0": 1, "1": 0 } (region 0's data is mirrored to region 1). A region with a mirror is not failed over automatically when its hosts are lost — a region that is merely partitioned would be failed over while still serving, creating a second owner — so it stays in the fleet until an operator deliberately promotes its mirror; a region with no mirror is simply retired when its hosts are all lost. Empty by default. The operator promotes a lost region's mirror with slates promote-region REGION, issued on any node: it commits PromoteRegion on the root group and every node re-homes the lost region's volumes to the mirror. A node that does not lead the root group forwards the command to the leader it knows, so the operator need not find the leader first; NotRootLeader is returned only when no leader is currently known (retry), and a region with no declared mirror is refused Unsupported.

slates status on any node shows its place in the fleet: fleet_host (its member id), fleet_f, fleet_host_epoch, fleet_members (the members it holds alive) and fleet_peers_probed (peers with a formed probe session), fleet_peers_in_contact (the members it keeps direct contact with: its holders and the voters, and every member while it votes) and fleet_peers_unmeshed (those of them not probed yet: its mesh is up at 0; two members that neither hold for each other nor vote keep no contact, so fleet_peers_probed need not reach the member count less one), fleet_learner: council_version=… fetched_version=… asked=… last_asked_ago_ms=… (as a learner: the council version it holds, the version after its last configuration fetch, how many members that fetch asked, and how long ago, never before any; a node held at an old version says here whether it is asking), fleet_held_records (the objects it holds a record copy of as a candidate holder), fleet_takeovers_pending (a retired owner's objects the council reassigned to it whose takeover has not finished: a takeover that stalls shows here) and fleet_configuration_version (the regional configuration it has installed; each admission, retirement, settlement and takeover confirmation advances it); then fleet_configuration_members (how many members that configuration holds: the council's committed membership, where fleet_members is the ones this node's detector holds alive), fleet_settled_generation and fleet_neighbourhood_generation (the versions its settled and its current neighbourhood were fixed at: they differ while its records are being moved onto a new neighbourhood), and one fleet_retirement: host=… version=… survivors=[…] confirmed=[…] unconfirmed=[…] line per retirement the council still keeps (the retired member, the version that retired it, the members its objects' successors are ranked among, those that have confirmed their share, and the takeovers it carried unconfirmed — a retirement whose survivors have not all confirmed is a takeover still running); then the two consensus groups as this node drives them — fleet_council_leads (whether this node is the regional configuration council's elected leader), fleet_council_base_periods and fleet_council_span_periods (the election timeout it derived, in coordinator periods: base ⌈10 × max(broadcast RTT tail, heartbeat) / heartbeat⌉, the span the same over the RTT variation), fleet_council_rtt_tail_ns and fleet_council_rtt_spread_ns (the measured tail and spread they came from; zero before any sample) and fleet_council_samples (the round trips behind them — a loopback fleet derives the ten-period floor from its samples, a WAN fleet a larger base); then its election state — fleet_council_term, fleet_council_priority_ns and fleet_council_priority_spread_ns (its measured quorum round trip, the election priority; zero while a needed path is unmeasured), fleet_council_rank (how many voters it holds alive outrank it: the timeouts it yields before campaigning), fleet_council_leader_lease (whether it still believes a leader alive, and so refuses pre-votes), fleet_council_pre_elections and fleet_council_elections (the campaigns it has begun since it started), fleet_council_pre_votes_granted and fleet_council_pre_votes_refused (the replies its pre-elections drew), and fleet_council_refused_role, _refused_leased, _refused_term and _refused_log (the pre-votes it refused, by reason: it leads, it holds a lease, the term is not ahead, the candidate's log is behind), and fleet_council_voters and fleet_council_joint (the voters seated in the configuration in force, and whether a joint change of them is in flight: members are admitted before they are caught up and promoted to voters, so right after formation a council can seat fewer voters than its f asks, and it tolerates only the losses its seated voters give) — and the same twenty-one fleet_root_* lines for the root group across regions. A single daemon shows the same lines, degenerate: f 0, itself the one member, leading both groups after explicit bootstrap, at the floor with no sample. Then one fleet_detector PEER: configured=… suspicions=… (allowance …/1000) condemnations=… (allowance …/1000) line per peer the failure detector holds (JSON: fleet.detector): the suspicions and condemnations this node's own probes made of that peer, each beside the number the detector's theory expects of a peer alive throughout, in thousandths (Chen, Toueg and Aguilera's Theorem 7, summed over every judged probe), and whether the pair's own estimator judges it yet. A count far above its allowance is a detector misjudging a live peer; a laptop shows none. Each line also says how the pair is judged now:

  • judged_by: own, pool, provisional or measuring;
  • expected_ns, margin_ns, samples and mistake;
  • pool_misfit: whether the member's pooled verdict does not fit the pair;
  • refused: why the pair's own estimator last refused to configure.

Then fleet_detector_pool: configured=… samples=… refused=… (JSON: fleet.detector_pool): the member's pooled verdict, which judges every pair without its own. A peer shown measuring with the pool unconfigured is judged by nothing until it has been silent past RFC 6298's timeout (at least 1 s).

Then fleet_detector_relays: due_ns=… stated=… (JSON: fleet.detector_relays): the relay stage as the detector bounds it. Each relay judges its own leg to the target by its own deadline and nacks when that passes unanswered; a stage ends once every relay asked has nacked, or when the slowest has its response due: the member's span for it plus the bound the relay stated on its acknowledgements. due_ns is that due for the slowest relay the member could ask (zero while no alive peer has stated a bound), and stated counts the peers that have stated one.

Then one block per shard: its counters (shard N: clients=… volumes=… served=…), its refusals by kind, its health signals (shard N catalog.volumes: 3 (age 0 ns) — a signal that is absent prints absent/unknown or absent/degraded, never a bare 0), and its telemetry. status drains each shard's bounded telemetry ring (the chokepoint spans recorded since the previous status, up to what one reply carries): shard N drain: spans=… window_ns=… horizon_ns=… shed_before=… dropped_total=… remaining=… missing_links=… — shed_before is the loss marker (spans the ring shed since the previous drain, before this batch), remaining what the bound left for the next status, missing_links spans whose cause was not carried across a boundary — followed by one line per chokepoint of the registry (shard N span shard.op: spans=3 latest_age_ns=812). A chokepoint whose newest span is older than the horizon (the failover SLO), or that has none, prints absent/unknown (…) with its last sighting as an age and whether any producer of it runs on this host, never a stale age as a live value.

Client verbs

The instance is --instance, else SLATES_ENDPOINT, else default.

slates volume create NAME (--bounded SIZE | --dynamic MAX) [--fold] [--locked] [--base DIR] [--json]
slates volume list [--json]
slates volume stat ID [--json]
slates volume snapshot ID [--json]
slates volume destroy-snapshot ID SNAPSHOT [--json]
slates volume clone ID SNAPSHOT NAME [--json]
slates volume resize ID (--bounded SIZE | --dynamic MAX) [--json]
slates volume destroy ID [--json]
slates volume placed ID [--snapshot N] [--mirror] [--json]
slates green NAME [--require-evidence] [--base VOLUME --snapshot N] [--json]
slates versions GREEN [--json]
slates changed-since GREEN VERSION [--json]
slates work GREEN NAME [--json]
slates edit WORK PATH AT DELETE TEXT [--json]
slates submit WORK [--evidence HEX] [--json]
slates rebase WORK [--json]
slates advance ATTACHMENT [VERSION] [--json]
slates read VOLUME PATH [--version N | --attachment A]
slates mount ID PATH [--read-only] [--subtree DIR]
slates unmount PATH
slates export ID [--read-only] [--subtree DIR] [--uid UID]
slates land ID TARGET [--snapshot N] [--include P] [--exclude P] [--grant N] [--json]
slates image build ID SNAPSHOT NAME --capacity SIZE [--json]
slates image merge IMAGE NAME [--json]
slates grants [--json]
slates grant LANDING MANIFEST [--session] [--term SECONDS] [--json]
slates audit [--since N] [--json]
slates enroll [--account UID] [--json]
slates revoke CONSUMER [--json]
slates share ID PRINCIPAL [--read] [--write] [--admin] [--json]
slates run [--keep] [--json] -- CMD [ARG ...]
slates exec --volume V --at PATH -- CMD [ARG ...]
slates shards ID [--subtree DIR] [--read-only] --fd N [--json] -- CMD [ARG ...]
slates vhost-user ID [--tag NAME] [--subtree DIR] [--read-only] [--dax-window SIZE] [--json] -- VMM [ARG ...]
slates attach ID [--read | --write] [--snapshot N] [--json]
slates detach ATTACHMENT [--json]
slates status [--json]
slates status ID [--drift] [--json]
slates promote-region REGION [--json]
slates base read ID PATH
slates base rewitness ID [PATH ...] [--json]
slates base pin ID [PATH ...] [--json]
slates profile [--quick] [--json]
slates mcp [--instance NAME] [--http PORT]

slates live-ship ID [--off] turns a volume's live shipping on (or off): in a fleet each of its publications is then shipped to its candidates, and a sync is answered once the owner and f of them hold it (design A-200). It needs admin on the volume, as share does.

Sizes take a binary unit (512MiB, 4GiB; B, KiB, MiB, GiB, TiB) or plain bytes. Ids are 32 hexadecimal characters as create prints them.

Output is plain and stable: one key: value per line (create, stat, status, attach, snapshot, clone, pin), one record per line (list: id, name, referenced=, unique=, overlay=, live_shipped=; status --drift and rewitness: one path per line), ok for the verbs that return nothing, and the raw bytes for base read.

--json emits a machine-readable form of every verb — the read and query verbs (status, status ID / volume stat, volume list, versions, changed-since, submit, rebase), the volume-lifecycle verbs (create, snapshot, clone, resize, destroy, destroy-snapshot, placed, attach, detach, pin, rewitness, grants, audit, land), and the merge create verbs (green, work, edit) — with the same fields as the text form and the same schema the MCP surface emits (one definition, two surfaces). A creating verb returns {"id": "<hex>"} (the same key across create, clone, green and work); an outcome-only verb returns {"ok": true}. The one exception is base read, which streams a file's raw bytes with or without --json. status --json carries every shard's block under shards (an array: the counters, refusals, signals with each signal's value — null when absent — and its absence meaning, and telemetry: the drain's markers, the chokepoints registry with each chokepoint's fresh/latest_age_ns/absence/ expected, and the spans with their request, trace, span and cause identities), exactly what the text form prints and the MCP slates.status tool returns. slates mount ID PATH mounts the volume at an existing user-owned directory over the loopback NFS bridge (FUSE on Linux); slates unmount PATH removes it. On Linux the FUSE mount goes through the system's fusermount3. Ubuntu 26.04 confines fusermount3 with an AppArmor profile (/etc/apparmor.d/fusermount3) that allows FUSE mounts and unmounts only under your home directory, /mnt, /media, /tmp and /run/user/UID, for every caller and inside containers too; a PATH anywhere else is refused Permission denied ("the mount helper returned no device descriptor … fusermount3: mount failed"). Mount under one of those directories, or have an administrator allow the path in /etc/apparmor.d/local/fusermount3 and reload the profile (apparmor_parser -r /etc/apparmor.d/fusermount3). --subtree DIR presents only that directory of the volume: it is the mount's root, and nothing outside it can be reached through the mount, not even by a handle made by hand. A subtree that is a file or does not exist is refused, and nothing is mounted. slates advance ATTACHMENT [SNAPSHOT] moves a snapshot's mount to another snapshot of the volume (the newest when none is given) and prints the paths that differ. slates mcp serves the MCP tools over stdio, or loopback Streamable HTTP with --http PORT (macOS and Linux). The HTTP edge serves one endpoint, http://127.0.0.1:PORT/mcp, and prints a bearer token on the terminal when it starts; the agent sends it as Authorization: Bearer TOKEN with Content-Type: application/json and Host: 127.0.0.1:PORT. A request from a browser page on another origin, under another host name, or without the token is refused before it reaches a tool. Oversized lines, headers and bodies are refused, and a slow or idle connection never holds up another.

slates export ID prints export: /<name>@<attachment>.<token>, the path a Kubernetes PersistentVolume names as its nfs path on a fleet node's network export (RPC-with-TLS on the node's base port, served when the fleet manifest names an operator authority). The export has its own attachment, ending with slates detach or the volume's destroy. --read-only takes no write lease, and --subtree DIR presents only that directory. --uid UID serves only that Unix identity: every other caller, root included, is shown the export's attributes and refused everything else, so a Linux client never serves one user what another read through the mount (a symbolic link's target above all; A-115).

The merge flow (§4.16): green NAME starts a green from scratch, or from a complete immutable base with --base VOLUME --snapshot N — a snapshot of a volume whose whole tree is in memory (base pin VOLUME first for an overlay; a snapshot still served from the host directory is refused ConsistentBaseUnavailable), and --require-evidence makes every submit carry an evidence reference (submit WORK --evidence HEX, refused EvidenceRequired without one). A green is written by nothing but its merge task: edit, a write attach, volume snapshot or resize on one refuse ReadOnlyVolume. attach GREEN --read pins the green's head version (the attachment's version); the attached view never moves until advance ATTACHMENT [VERSION], which prints the version now pinned and the paths it invalidated. read VOLUME PATH streams a file's raw bytes at the head, --version N at a green version, --attachment A at the version an attachment pins.

A volume reaches a VMM that shares no folder with its guest (Firecracker) as a disk image (A-160). slates image build ID SNAPSHOT NAME --capacity SIZE renders the snapshot as an ext2 file system of SIZE bytes in a new volume NAME, as one file, image.ext2. It prints the image volume's id, its file, and the image's image_bytes, blocks_used and inodes_used. Mount the image volume and point the VMM's block device at image.ext2, or its pmem device (Firecracker 1.14 and later): a guest that mounts the pmem device as ext4 with dax=always maps the file's pages with no page cache of its own (A-165). The image carries names, kinds, permissions, owners, nanosecond times, symbolic links, hard links, FIFOs and sockets; it does not carry extended attributes or holes. The image volume pins its snapshot as a clone does: volume destroy-snapshot is refused until the image volume is destroyed. slates image merge IMAGE NAME reads the image as it is when the merge starts (an image the guest has not cleanly unmounted is refused). It makes a new clone NAME of the snapshot the image was built from hold the image's tree, and prints the clone's id and what the merge did: kept, created, moved, linked, removed, names_removed, staged, bytes_compared, bytes_written and attributes_set. A file the guest renamed keeps its identity in the clone, and an unchanged chunk stays shared. A name the volume cannot hold (not UTF-8), a device, or an image that is not ext2 as the build writes it is refused ImageRefused, naming why, and no clone is made. After a guest's crash, run e2fsck -fy on image.ext2 through the image volume's mount: it checks the file system and marks it clean, and the merge then takes the guest's tree. e2fsck adds a lost+found (the build makes none), which the merge takes like any new directory. Both verbs run in slices on the volume's owner while the daemon serves, and wait for their result.

A fleet whose deployment manifest declares a durability policy (accepted_loss, coincident_failures) refuses a volume create, clone, snapshot or merge submit with DurabilityUnmet { coincident_loss, accepted_loss, coincident_failures } while its committed configuration's coincident-loss probability is above the accepted one — the resolution is the operator's (more copies, more re-replication bandwidth, tighter failure domains, or a policy that accepts the loss); reads, destroys, resizes and status continue, and status counts the refusals under durability_unmet.

Exit codes: 0 done; 1 the daemon refused (the refusal is named on stderr, e.g. AlreadyExists); 2 usage; 3 no daemon at the instance; 4 the command itself failed.

slates grant LANDING [--session] [--term SECONDS] [--json] issues the grant a presented landing needs (§4.13, §4.15 step 3). It is the human's surface: the command runs as the user who started the daemon's anchor, reads the grant-issuer secret the daemon minted into the anchor segment at start, and proves it with a keyed hash over the exact landing — its id, the manifest hash the human saw, the scope and the term — which the daemon recomputes before issuing. A proof that does not verify (a forged, replayed or modified-plan approval) is refused GrantIssuerUnverified and counted; the MCP server and the SDKs carry no proof by construction and are refused by kind. Passing --grant N to land then consumes the grant; the landing must present the same manifest. land --snapshot N lands that snapshot exactly as it was taken, whatever the volume has become since; the volume's later edits stay private, and a later land without --snapshot lands them over it. One landing at a time writes into a directory, whichever volume lands and however its path is spelled: another landing into it meanwhile is refused LandingLeaseHeld (exit 1) and can be retried once the first has replied. slates status counts each shard's landings_in_flight and target_leases. Neither a control-channel label nor the caller's uid is proof of human approval — only the secret is, and only the anchor's user maps it. slates anchor prints the variables to export for that (slates anchor: issuer surface: export SLATES_ANCHOR=… SLATES_ANCHOR_LEN=…) on macOS and Windows, where the segment is a named object of the user; on Linux it is a descriptor only the anchor's children hold.

slates run [--keep] [--json] -- CMD [ARG ...] is the harness verb of §4.13: it runs CMD as a consumer enrolled for the command's lifetime. The command's own slates client (the CLI, an SDK, the MCP server) finds the capability on an inherited descriptor named by SLATES_CONSUMER_FD and binds its channel to the consumer before any verb — the capability is never in an argument, never in the environment, never printed — so what it creates is the consumer's, and the account (or another consumer) is refused on it until share says otherwise. run announces the consumer first (consumer: N, or {"consumer": N} under --json) so a human can share volumes with it while it runs, then hands the command the terminal and exits as the command exited; the enrollment is revoked when the command ends unless --keep. slates enroll [--account UID] enrolls a consumer and shows its capability once — for a harness that delivers it by its own means (Delivery in slates-ipc; the SDKs' pass_fds/stdio/handle_list spawns); slates revoke CONSUMER ends an enrollment (every later verb from its channels refuses ConsumerRevoked); slates share ID PRINCIPAL [--read] [--write] [--admin] sets a principal's rights on a volume (uid:N, consumer:N under your account, or consumer:ACCOUNT/N; no switch removes the entry). enroll, revoke and run are the anchor user's surface like grant and need its variables; share is the volume owner's.

slates shards ID [--subtree DIR] [--read-only] --fd N [--json] -- CMD [ARG ...] hands a program a Shards microVM's filesystem device: it attaches the volume (or only DIR of it; for writing unless --read-only, as mount attaches), announces the attachment (attachment: N, or {"attachment": N} under --json), and replaces itself with CMD, one end of a socket pair at descriptor N — the end the daemon serves that attachment on, in Shards' framing, so a VMM given it (shards run … --slates-fs-fd N) mounts the volume as shards0. N is 3 or more. The end is the attachment's capability: whoever holds it reaches that attachment and nothing else. Only the copy at N goes into the command, so the attachment lasts exactly as long as the command's end: it ends when the command, and anything the command handed the end to, closes it (or on detach, a revocation, the volume's destroy). The process you started is the command — its pid, its signals, its exit code — so ending it ends the attachment; a command that cannot run is a failure (exit 4), its attachment ended with the end it never got. Linux sends the end over the client's control socket; macOS claims it on the daemon's socket route, an encrypted exchange keyed by a single-use secret. Windows refuses the form by kind (AttachmentUnsupported, DescriptorRouteNotBuilt) until a route hands a client a handle.

slates vhost-user ID [--tag NAME] [--subtree DIR] [--read-only] [--dax-window SIZE] [--json] -- VMM [ARG ...] runs a VM whose virtio-fs device is slates' own (Linux). It attaches the volume (or only DIR of it; for writing unless --read-only), announces the attachment like shards, and replaces itself with VMM, the device's socket end inherited at the descriptor it arrived at. Each {socket} in the VMM's arguments becomes /proc/self/fd/N, that descriptor, and each {fd} becomes N. So for crosvm:

slates vhost-user ID --dax-window 8MiB -- crosvm run --vhost-user type=fs,socket={socket} ... vmlinuz

and for QEMU 11.1 or later, whose generic vhost-user device gives the device its DAX window (guest memory must be shared, as vhost-user needs):

slates vhost-user ID --dax-window 8MiB -- qemu-system-x86_64 -machine q35,memory-backend=mem \
  -object memory-backend-memfd,id=mem,size=512M,share=on -m 512M \
  -chardev socket,id=slates,fd={fd} \
  -device vhost-user-test-device-pci,chardev=slates,virtio-id=26,num_vqs=2,config_size=40 ...

and the guest mounts mount -t virtiofs slates /mnt -o dax=always (--tag names another tag). No socket file exists anywhere. A VMM with neither {socket} nor {fd} in its arguments is usage (exit 2): the device is reached through that end alone. QEMU's vhost-user-fs-pci has no DAX window, so use the generic device for DAX; QEMU before 11.1 offers no window on either. --dax-window SIZE offers the guest a DAX window of that size (a power of two of at least 2MiB); through it the guest maps a copy of each file slates keeps equal to the volume, so every change by any path shows in its map, and its own stores reach the volume when it syncs, closes, or the VM ends. The attachment lasts as long as the VMM's end: it ends when the VM exits, never started, or closes it (or on detach, a revocation, the volume's destroy), and a VMM that cannot run is a failure (exit 4). The device lives in the daemon, so the daemon's death ends it; crosvm then ends the VM, and QEMU keeps it with the device gone: what the guest synced or closed is in the volume, and its unsynced stores are lost with the daemon. A VM that dies on its own leaves its unsynced stores in the volume (a store past a file's end never reaches it, as POSIX says of a map). While the VM runs, slates status counts what its window does: maps sent to the VMM (virtiofs.dax_maps_sent), runs of the guest's stores written into the volume (virtiofs.dax_runs_ingested), and the rest of the virtiofs.dax_ counters. macOS and Windows refuse the form by kind (AttachmentUnsupported, BindingNotBuilt).

Planned interface corrections

The following are requirements, not additional runnable commands:

  • One typed operation definition supplies CLI help, MCP schemas, SDK calls and refusals.
  • Scoped names as well as ids; consistent --help, structured --json, pagination and actionable errors. --json now covers every verb (above) with the MCP schema; pagination (cursors) and scoped names are still open.
  • Discover the instance/root from enrolled context; report success only when the requested path or guest tag is ready, including consumer rights and supported attachment capabilities.
  • Distinguish a live overlay's retained base from a complete immutable capture. Cloning shares the base plus changed entries; a remote clone must not lose untouched files.
  • Show actual quota, reserved capacity, used bytes, retention, placement and freshness; report provisioning, capture and mount/device setup costs separately.
  • Supply a protected human preview/approval surface; keep issuance absent from MCP and SDKs.

The acceptance cases are AC-5.9–AC-5.11 in the unified design. No new command was implemented or exercised for this documentation correction.