@microck/raft 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +30 -14
  2. package/deploy/deploy.py +112 -19
  3. package/deploy/install-incus.sh +10 -0
  4. package/deploy/raft-expire.py +109 -31
  5. package/deploy/verify-controls.py +34 -48
  6. package/deploy/verify-features.py +592 -3
  7. package/deploy/verify-fresh-host.py +64 -1
  8. package/deploy/verify-limits.py +327 -72
  9. package/deploy/verify-live.py +56 -31
  10. package/deploy/verify-self-update.py +372 -0
  11. package/docs/boat-capabilities.md +8 -7
  12. package/docs/boot-performance.mdx +1 -1
  13. package/docs/contract.md +33 -23
  14. package/docs/first-box.mdx +1 -1
  15. package/docs/guides/backup-recovery.mdx +2 -2
  16. package/docs/guides/config-policy.mdx +253 -0
  17. package/docs/guides/docker.mdx +9 -1
  18. package/docs/guides/long-running-services.mdx +10 -4
  19. package/docs/guides/meta.json +1 -0
  20. package/docs/guides/project-setup.mdx +4 -3
  21. package/docs/guides/scripting-ci.mdx +27 -10
  22. package/docs/guides/snapshots-forks.mdx +9 -3
  23. package/docs/guides/troubleshooting.mdx +37 -8
  24. package/docs/guides/upgrades.mdx +17 -5
  25. package/docs/index.mdx +1 -0
  26. package/docs/operations.mdx +16 -14
  27. package/docs/reference/capacity.mdx +198 -0
  28. package/docs/reference/cli.md +178 -35
  29. package/docs/reference/configuration.mdx +40 -2
  30. package/docs/reference/meta.json +1 -1
  31. package/docs/releases/0.5.0.md +19 -0
  32. package/docs/releases/0.6.0.md +16 -0
  33. package/docs/requirements.mdx +2 -2
  34. package/docs/verification.mdx +14 -8
  35. package/package.json +1 -1
  36. package/raft.py +1181 -128
  37. package/skills/raft-cli/SKILL.md +79 -13
  38. package/docs/reference/capacity-limits.mdx +0 -92
package/docs/contract.md CHANGED
@@ -17,14 +17,16 @@ Raft manages persistent unprivileged native-architecture Incus system containers
17
17
  - npm publishes `@microck/raft` with the same Python sources as the wheel and source archive. Its executable is `raft`; Linux and Python 3.11.8+ are required. Installation runs no scripts and downloads no workspace images.
18
18
  - `raft --version` reports the release version without reading host configuration. The Python package, npm metadata and release tag must agree before publication.
19
19
  - Releases include Python wheels, source archives, npm tarballs and both native images with checksums. The release workflow verifies installation outside the checkout and publishes npm with provenance before creating the GitHub release.
20
+ - **Self-update**: `raft self-update [--check | --version X.Y.Z]` takes the latest version from the GitHub releases API and compares versions numerically. It upgrades only npm global installs (through the registry, at the same prefix) and uv tools (a release wheel verified against the release `SHA256SUMS`), and refuses checkouts and other layouts. It never downgrades without `--version`, never changes hosts, and prints `raft doctor` as the next step. `--check` exits `100` when a newer release exists.
21
+ - **Release notice**: At most once per 24 hours a command checks the latest release in the background and, if it is newer, prints one stderr line after the command. It never installs, prompts, changes stdout or exit status, or waits more than 2 seconds; failures stay silent. `RAFT_NO_UPDATE_CHECK=1` disables it, and `self-update` and `--version` skip it.
20
22
 
21
23
  ## Infrastructure and deployment
22
24
 
23
25
  - **Strict infrastructure validation**: Deployment refuses conflicting storage or network names and incomplete infrastructure rather than overwriting or repairing state.
24
- - **Storage pool sizing**: Existing owned Btrfs pools must retain the configured 60 GiB size; drift is rejected without automatic resizing.
26
+ - **Storage pool sizing**: The shared Btrfs pool `raft-data` must retain its configured 60 GiB size. The own-disk pool `raft-disks` is opt-in: deployment creates it only when `--disk-pool SIZE` is given, on a fresh host or on any later redeploy, as a loop-backed LVM thin pool (`lvm.use_thinpool=true`) of SIZE whose file is allocated in full on the host disk and whose thin pool uses `--discards nopassdown`. A fresh host needs 80 GiB free, plus SIZE when `--disk-pool` is given; adding the pool later needs SIZE plus 20 GiB free. A host without `raft-disks` is healthy. An existing `raft-disks` keeps its size, a different explicit `--disk-pool` is refused, and a pool with another driver or thick volumes is rejected. Deployment never resizes either pool.
25
27
  - **Firewall precedence**: Redeployment reapplies firewall rules. Incus startup requires successful firewall application before the daemon starts, including socket activation.
26
28
  - **Guided onboarding**: `raft host add LOCATION --ssh TARGET` probes the target read-only and refuses unsupported hosts, including macOS, before any change. It prints the exact plan and applies it only after an interactive yes or `--yes`; `--dry-run` stops after the plan. It runs the same deployer as `deploy/deploy.py`, imports an image only after its SHA-256 matches (release `SHA256SUMS` or the local archive digest) and its architecture matches the host, then registers the location with the image fingerprint. It finishes with `doctor` and a disposable create/exec/destroy smoke test; if either fails, it removes the new location from the configuration and leaves host changes for a rerun. It uses existing key-based SSH and never collects passwords or changes SSH, other firewall rules or IP configuration. It requires the deployment files from the npm package or a source checkout.
27
- - **CLI coverage**: The single-operator workflow spans lifecycle, execution, files, snapshots, jobs, tunnels, backup, and capacity inspection; see the [command reference](./reference/cli.md).
29
+ - **CLI coverage**: The single-operator workflow spans lifecycle, execution, files, snapshots, jobs, tunnels, backup, and advisory capacity inspection; see the [command reference](./reference/cli.md).
28
30
 
29
31
  ## Configuration and lifecycle defaults
30
32
 
@@ -33,38 +35,45 @@ Raft manages persistent unprivileged native-architecture Incus system containers
33
35
  | Host identification | Configuration requires named SSH targets using letters, digits, underscores, or hyphens. |
34
36
  | Qualified handles | Every handle includes location and instance name (`<location>:<name>`). |
35
37
  | Image pinning | Workspace creation requires the immutable 64-hex image fingerprint, not an updating alias. |
36
- | Lifecycle defaults | Defaults to first configured location, 1 CPU, 2 GiB RAM, and 600-second lifetime. Supported choices are 1 or 2 CPUs, 1/2/4/8 GiB RAM, and 60 to 2,592,000 seconds (30 days) TTL. |
37
- | Admission limit | Hard ceiling of four saved boxes per host, including stopped boxes. |
38
+ | Lifecycle defaults | Built-in defaults are the first configured location, 1 CPU, 2 GiB RAM, the shared pool, a persistent box and 600-second lifetime. `~/.config/raft/config.json` can override them for `new` only; explicit flags win, and `--persistent` overrides a `disposable` default. Boxes from a snapshot (`new --from`) and forks keep the source's CPU, RAM and disk instead of any default, configured or built-in; each flag overrides only its own value. `--cpu` takes any positive whole number, `--memory` any positive whole GiB (`16GiB`), `--disk` a positive whole GiB or `shared`, and TTL 60 to 2,592,000 seconds (30 days). |
39
+ | Box count and size | Raft limits neither how many boxes a host holds nor their size. CPU or RAM limits larger than the whole host (CPU count, or `MemTotal` rounded up to whole GiB) are refused under the host lock before any change, because Incus would clip them silently. Other failures come from Incus, such as too little pool space. |
38
40
  | TTL calculation | Running TTL begins after launch completes, using the host clock. Cold image preparation does not consume TTL. |
39
41
  | Host locking | Creation, lifecycle transitions, and expiry are serialized by host file lock (`/run/lock/raft-incus.lock`). Noninteractive transactions close controller stdin so commands cannot wait for piped input while holding this lock. Exec, SSH, and file-transfer paths remain unchanged. Piped input to `raft exec` is unsupported. Native Incus admin calls bypass CLI policy. |
40
42
 
41
- ## Capacity and admission inspection
43
+ ## Locations, notes and capacity inspection
42
44
 
43
- `raft locations` lists configured locations as JSONL with `location`, `default` and `note` fields without contacting hosts. `raft note LOCATION TEXT` and `raft note LOCATION --clear` edit the optional `note` field in the controller configuration atomically, preserving location order and mode `0600`. Notes are advisory guidance; Raft shows them in `locations`, `limits` and on stderr from `new`, and never enforces them.
45
+ `raft locations` lists configured locations as JSONL with `location`, `default` and `note` fields without contacting hosts. `raft note LOCATION TEXT` and `raft note LOCATION --clear` edit the optional `note` field in the controller configuration atomically, preserving location order and mode `0600`. Notes are advisory guidance; Raft shows them in `locations`, `capacity` and on stderr from `new`, and never enforces them.
44
46
 
45
- Boxes accept an optional note with a fixed template: `purpose` (required), `owner` and `next`, all nonempty strings. `raft new --purpose TEXT [--owner TEXT] [--next TEXT]` and `raft note BOX ...` store it as JSON in the box's `user.raft.note` Incus key; `raft note BOX --clear` removes it. Each edit replaces the whole note. `raft list` reports it as `note`, or `null`. `new` always writes the key, so `new --from` never inherits a snapshot's note; forks and recovered backups keep it. Box notes are advisory and never change lifecycle policy.
47
+ Boxes accept an optional note with a fixed template: `purpose` (required), `owner` and `next`, all nonempty strings. `raft new --purpose TEXT [--owner TEXT] [--next TEXT]` and `raft note BOX ...` store it as JSON in the box's `user.raft.note` Incus key; `raft note BOX --clear` removes it. Each edit replaces the whole note. `raft list` reports it as `note`, or `null`, reports each box's `disk` as `"shared"` or its own disk size, and reports `deleteAt`, the Unix time retention deletes a stopped box, or `""`. `new` always writes the key, so `new --from` never inherits a snapshot's note. Forks and recovered backups keep it unless `fork` or `recover` gets note flags, which replace the whole note. Box notes never change lifecycle behavior.
46
48
 
47
- `raft limits [--location NAME] [--json]` inspects hosts without modifying state. It reports effective CPUs, total and available RAM, saved and active box counts, active resource limits, and disk space.
49
+ ## Controller defaults and host policies
48
50
 
49
- - **Reserve calculations**: Recommends running boxes after reserving 1 CPU and the larger of 2 GiB or 10% host RAM.
50
- - **Total capacity**: Assumes an empty dedicated host, capped at four boxes.
51
- - **Additional running**: Subtracts active configured limits and evaluates available RAM minus host reserves. Every non-`Stopped` state (including `Frozen`) counts as active.
52
- - **New box capacity**: Bounded by remaining saved-box slots: `min(more running, max(0, 4 - saved boxes))`.
53
- - **Advisory estimates**: CPU and memory calculations are advisory planning estimates, not admission guarantees. Disk space below 5 GiB triggers diagnostic warnings. The four-saved-box admission cap is strictly enforced.
51
+ - **Controller defaults**: `~/.config/raft/config.json` holds `{"defaults": {...}}` with optional `location`, `ttl`, `cpu`, `memory`, `disk` and `disposable` for `new`. Precedence is explicit flag, then configured default, then built-in default. `cpu`, `memory` and `disk` apply only to boxes from the image: `new --from` keeps the snapshot's location, CPU, RAM and disk unless a flag overrides them, and `--disk shared` is refused there. `ttl` and `disposable` apply to `new --from` as well, because a snapshot carries no lifecycle; `new --disposable` and `--persistent` are mutually exclusive overrides. `fork` takes no defaults and is persistent without `--disposable`. A `disk` default on a host without `raft-disks`, or larger than its unreserved space, fails like `--disk` and names `defaults.disk` as the source. Other commands take no defaults. A value is valid exactly when the same text is valid for the flag. Unknown keys, bad values and malformed JSON fail `new` with the file path and key, before any host access. `raft config set/unset` validate first, write atomically under a controller lock, keep mode `0600`, print the change and can repair the key they edit. The file is separate from `incus.json`, so location names never collide with setting names.
52
+ - **Policy storage**: `raft policy LOCATION [--require FIELDS|none] [--delete-stopped-after HOURS|never]` sets either host policy or both; each option changes only its own. Required box note fields, which must include `purpose`, live in the Incus project key `user.raft.require`, and `--require none` empties it. Stopped-box retention lives in `user.raft.delete-stopped-after` as a positive whole number of hours, with the time it was set in `user.raft.delete-stopped-after.since`; `never` empties both. Both values are validated before any host change. Each change runs under the host lock, prints the old and new value, records UTC time, the SSH account (`SUDO_USER`) and the self-reported controller user and hostname in the policy's `.changed` key, and logs to the host journal with tag `raft`. Redeployment leaves these keys unchanged.
53
+ - **Note policy enforcement**: `new` (including `--from`), `fork`, `recover` and `raft note BOX` check the note the box will carry. The controller reads the policy first and fails with the missing flags and a complete corrected command before changing anything. The host repeats the check inside the transaction that writes the note, under the host lock, so a concurrent policy change cannot be missed. That transaction also holds the oversize and own-disk reservation checks; all of them run before Incus creates anything. `fork` checks the given note or, without one, the source note read under the lock. `recover` checks a given note before upload; an archived note is checked after import, and a failing import is deleted in the same transaction. `note --clear` and replacement notes that drop a required field are rejected.
54
+ - **Retention enforcement**: The host expiry worker deletes a stopped Raft box, with its snapshots, once the hours have passed since the later of its `user.raft.stopped-at` and the policy's `.since` time, so setting, shortening or renewing a policy never deletes a box sooner than its hours after the change. It runs on the 15-second timer and on `raft gc`, under the host lock, and writes each deletion to the host journal with tag `raft`. Running boxes are never touched. A stopped Raft box without `user.raft.stopped-at`, from a guest poweroff, a native stop, a failed start or `raft recover`, gets the worker's current time as its stop time, never an earlier guess, whether or not a policy is set. A malformed retention value makes the worker delete nothing by retention, still enforce deadlines, and exit nonzero; the CLI reports it with the fix. Setting retention requires the current worker protocol; clearing it does not.
55
+ - **Policy scope**: Policies are guardrails for agents, not access controls. Anyone with host SSH and sudo can change them or create boxes with native Incus commands, and controllers without note policy support do not check it. A native `incus start` leaves a stale stop time that retention trusts. `raft config` reports every location's policies and last changes.
56
+
57
+ `raft capacity [--location NAME] [--json]` inspects hosts without modifying state. Text and JSON (`notice`) lead with: "Advisory estimate only. Raft enforces no box count or size budget; create the boxes your task needs." It reports effective CPUs, total and available RAM, saved and active box counts, active resource limits, headroom, free space in the host disk and the shared pool, and for `raft-disks` its data space, reserved and used bytes and metadata use (`boxDiskPool`, `null` when own box disks are not enabled).
58
+
59
+ - **Reserve calculations**: Estimates leave out 1 CPU and the larger of 2 GiB or 10% host RAM.
60
+ - **Example shapes**: `estimates` lists shapes doubling from 1 CPU / 2 GiB at 2 GiB per CPU while they fit an empty host, with `fitEmptyHost` and `fitNow` counts. No count is capped.
61
+ - **Headroom**: Subtracts active configured limits and keeps available RAM above the reserve. Every non-`Stopped` state (including `Frozen`) counts as active.
62
+ - **Advisory only**: Nothing in the report blocks creation, resume or fork. Disk space below 5 GiB and `raft-disks` thin pool metadata above 80% trigger warnings; a missing `raft-disks` does not.
54
63
 
55
64
  ## Container lifecycle and execution
56
65
 
57
66
  - **Stop and resume**: Stop terminates guest processes and preserves disk contents; stopped boxes retain disk but do not reserve guest RAM. Resume starts the container anew with an explicit TTL without restoring process memory.
58
67
  - **Lifetime extension**: `raft extend --ttl` resets a running box's deadline from current host time without restarting; stopped boxes require `raft resume`.
59
- - **Host worker contract**: `doctor`, disposable creation/fork, and retention cleanup require the current worker protocol. `doctor` checks it without running expiry or changing boxes, and reports the failing location. A mismatch fails before executing deletion policy or creating a disposable box; deploy the current checkout explicitly. Installing a new CLI does not update host workers.
60
- - **Disposable lifecycle**: `new --disposable` opts a box into deletion on stop or TTL expiry, including its snapshots. Clones and recovered boxes default to persistent; fork accepts an explicit `--disposable`.
61
- - **Retention cleanup**: `prune --older-than DAYS` previews persistent stopped boxes with a recorded stop timestamp. `--yes` deletes matching boxes under the host lifecycle lock. Running boxes and boxes without a recorded stop time are excluded. No automatic persistent-box deletion is enabled.
62
- - **Resizing**: Resume and fork accept optional supported CPU/RAM overrides, preserving omitted values. Resume validates stopped state under the lock before changing sizing.
63
- - **Expiration cleanup**: The host timer checks expirations every 15 seconds, stopping expired persistent containers and deleting expired disposable containers. Expiry may be delayed by graceful shutdown, long locked transactions, and host outage until startup.
64
- - **Destroy**: Removes the container instance and its snapshots, releasing storage. Destroy confirms absence; SSH failure is not proof of deletion. Disposable boxes created with `new --disposable` are deleted on explicit stop or expiry. Persistent boxes still require explicit destroy for deletion.
68
+ - **Host worker contract**: `doctor`, disposable creation/fork, prune and setting a retention policy require the current worker protocol (2 since 0.6.0, which added retention). `doctor` checks it without running expiry or changing boxes, and reports the failing location. A mismatch fails before executing deletion policy or creating a disposable box; deploy the current checkout explicitly. Installing a new CLI does not update host workers.
69
+ - **Disposable lifecycle**: `new --disposable`, or a configured `disposable` default without `--persistent`, opts a box into deletion on stop or TTL expiry, including its snapshots. Clones and recovered boxes default to persistent whatever the default; fork accepts an explicit `--disposable`.
70
+ - **Manual cleanup**: `prune --older-than DAYS` previews persistent stopped boxes with a recorded stop timestamp. `--yes` deletes matching boxes under the host lifecycle lock. Running boxes and boxes without a recorded stop time are excluded. Automatic deletion of persistent stopped boxes happens only under a host retention policy.
71
+ - **Resizing**: Resume and fork accept optional CPU/RAM overrides, preserving omitted values. Resume validates stopped state under the lock before changing sizing. Neither changes a box's disk.
72
+ - **Expiration cleanup**: The host timer checks expirations every 15 seconds, stopping expired persistent containers, deleting expired disposable containers and applying stopped-box retention. Expiry may be delayed by graceful shutdown, long locked transactions, and host outage until startup.
73
+ - **Destroy**: Removes the container instance and its snapshots, releasing storage. Destroy confirms absence; SSH failure is not proof of deletion. Disposable boxes created with `new --disposable` are deleted on explicit stop or expiry. Persistent boxes still require explicit destroy, prune or a retention policy for deletion.
65
74
  - **Execution transport**: Incus exec and file APIs travel over authenticated host SSH without a public Incus listener or injected credentials. Interactive SSH runs a PTY over host SSH and Incus exec.
66
75
  - **State inspection and tunnels**: `raft list` and `raft info` read native metadata without guest process counters during shutdown; tunnels use the managed NIC.
67
- - **Resource reporting**: `raft usage` reports running cgroup memory, CPU microseconds, CPU affinity, and shared pool space. It does not provide per-box disk accounting or billing.
76
+ - **Resource reporting**: `raft usage` reports running cgroup memory, CPU microseconds, CPU affinity, and filesystem space with `filesystemScope`: the shared pool, or `own SIZE box disk`. It provides no billing.
68
77
  - **Recursive transfers**: `upload/download --recursive` require a running workspace and Python 3.11.8+ in the guest. They stream a tar archive and publish a new destination directory atomically without overwriting or merging. Relative links contained within the tree, regular files, directories and internal hardlinks are supported; escaping paths/links and special files are rejected. Source entries are pinned with no-follow descriptors before metadata inspection, file reads or directory descent, so pathname replacement cannot redirect access outside the selected tree. Failed transfers leave existing destinations unchanged.
69
78
  - **Directory transfer completion**: The receiver verifies the sender's exact archive byte count before publication. A truncated stream, including one ending between complete entries, is rejected without publishing a partial tree.
70
79
  - **File transfer failures**: Controller write errors return an error before replacing a download destination or publishing a backup archive.
@@ -79,10 +88,11 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
79
88
 
80
89
  ## Storage, snapshots, and cloning
81
90
 
82
- - **Storage pool**: A dedicated 60 GiB Btrfs pool per host supports snapshots and clones. Nested container subvolumes prevent strict per-box quota enforcement. The native compressed image cache resides outside the shared pool, so host disk space still matters.
91
+ - **Shared pool**: Boxes default to a dedicated 60 GiB Btrfs pool per host with instant snapshots and clones. Btrfs quotas cannot bound a box there, because nested subvolumes (Docker's, for example) escape them. The native compressed image cache resides outside both pools, so host disk space still matters.
92
+ - **Own box disks**: `new --disk SIZE` puts the box's root on an LVM thin volume of SIZE in `raft-disks`, formatted ext4. All guest writes, including Docker data, land in that filesystem, so writes fail with ENOSPC at SIZE. New own-disk boxes are thin snapshots of the cached image volume, and forks, templates and snapshots of own-disk boxes are thin snapshots of their source. The pool is never overcommitted: under the host lock, before `new`, `new --from`, `fork`, `snapshot` or `recover` adds thin volumes to `raft-disks`, Raft sums the sizes of every thin volume in the pool (from `lvs`) plus the new ones and refuses, before creating anything, if the total exceeds the thin pool's data space. A new volume counts as the larger of its size and its source's; the first own-disk box on a host also counts the image volume (`volume.size`, or 10 GiB); a recovered backup counts its size once per snapshot plus once. `restore` reserves nothing. On a host without `raft-disks`, `new --disk` and recovery of an own-disk backup fail before creating anything and name the deploy command that enables the pool. `fork` and `new --from` keep the source disk; `new --from ... --disk SIZE` copies onto an own disk of SIZE. A disk too small for its contents fails creation.
83
93
  - **Stopped-state requirement**: Snapshots, restores and forks require a stopped source for consistent disk contents. Operations run under the host lifecycle lock.
84
94
  - **Snapshot restore**: `raft restore` rolls back disk blocks via `--diskonly`, retaining container configuration, limits, and MAC address.
85
- - **Snapshot management**: `snapshot-delete <box> <name>` removes one named snapshot under the lock. `new --from LOCATION:BOX/SNAPSHOT` creates a same-host box from that snapshot, with normal creation defaults and a fresh network identity. The source may run; the named snapshot is immutable. Templates retain filesystem secrets.
95
+ - **Snapshot management**: `snapshot-delete <box> <name>` removes one named snapshot under the lock. `new --from LOCATION:BOX/SNAPSHOT` creates a same-host box from that snapshot, with the snapshot's CPU, RAM and disk unless `--cpu`, `--memory` or `--disk` overrides them, and a fresh network identity. The source may run; the named snapshot is immutable. Templates retain filesystem secrets.
86
96
  - **Workspace forks**: `raft fork` copies filesystem data and secrets to a new container with an independent Incus name, fresh MAC address, and renewed TTL. Source snapshots are not inherited.
87
97
 
88
98
  ## Background jobs and desktop
@@ -94,5 +104,5 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
94
104
 
95
105
  - **Export safeguards**: `raft backup` exports stopped containers and snapshots to a local archive. Destination path must not exist, requires mode `0600`, and uses atomic hard-link publication.
96
106
  - **Host locking**: Export holds the host lifecycle lock, temporarily delaying expiry checks on the source host.
97
- - **Recovery admission**: `raft recover` imports archives into a stopped container with a fresh name, new MAC address, and cleared TTL, while enforcing the four-box admission limit. Recovery never overwrites existing boxes.
107
+ - **Recovery**: `raft recover` imports archives into a stopped container with a fresh name, new MAC address, and cleared TTL. It reads the archive's recorded pool from `backup/index.yaml`: own-disk backups return to `raft-disks` with their size after the reservation check, failing on a host without the pool, and other archives land in `raft-data`. The host note policy applies after the reservation check. Recovery never overwrites existing boxes.
98
108
  - **Archive security**: Archives contain full container filesystems, including credentials. Native archive configuration is not sanitized; backups do not imply scheduled off-host uploads. Store archives privately and import only trusted exports.
@@ -83,4 +83,4 @@ raft destroy "$box"
83
83
  - [Troubleshooting](./guides/troubleshooting.mdx): Diagnose common errors and inspect host services.
84
84
  - [Upgrades and image changes](./guides/upgrades.mdx): Update the controller, hosts, and image pins.
85
85
 
86
- For temporary work, create with `--disposable`. Stopping or expiry then deletes the box and its snapshots. For persistent boxes, `raft prune --older-than 7` previews old stopped workspaces; add `--yes` only after reviewing the candidates.
86
+ For temporary work, create with `--disposable`. Stopping or expiry then deletes the box and its snapshots. For persistent boxes, `raft prune --older-than 7` previews old stopped workspaces; add `--yes` only after reviewing the candidates. A host can also delete boxes left stopped automatically; `raft config` shows each location's `deleteStoppedAfterHours`.
@@ -53,8 +53,8 @@ echo "Recovered workspace: $recovered"
53
53
 
54
54
  - **Architecture compatibility**: Target host must match the native CPU architecture (ARM64 to ARM64, AMD64 to AMD64). Cross-architecture recovery is not supported.
55
55
  - **Fresh network identity**: The container receives a new name in project `raft`, a new MAC address on `rfbr0`, and no inherited TTL deadline.
56
- - **Stopped initial state**: Recovered workspaces remain stopped until started with `raft resume "$recovered" --ttl <SECONDS>`.
57
- - **Enforced admission limit**: The destination location must hold fewer than four boxes.
56
+ - **Stopped initial state**: Recovered workspaces remain stopped until started with `raft resume "$recovered" --ttl <SECONDS>`. Under a host [retention policy](./config-policy.mdx#delete-boxes-left-stopped), the recovered box's clock starts when the host worker first sees it, within about 15 seconds; resume it before the policy's hours pass.
57
+ - **Disk placement**: A backup of a box with its own disk returns to an own disk of the same size in `raft-disks`. The destination needs own box disks enabled and enough unreserved pool space for the box and each of its snapshots; otherwise recovery fails before importing. Other backups land in the shared pool.
58
58
  - **Interrupted recovery**: Follow [Host crash during recovery](./troubleshooting.mdx#host-crash-during-recovery). Inspect the reported handle and container state before deleting any staging file.
59
59
 
60
60
  Start and verify the recovered workspace:
@@ -0,0 +1,253 @@
1
+ ---
2
+ title: Defaults and host policies
3
+ description: Set controller defaults for new boxes, require box note fields on a host and delete boxes left stopped.
4
+ ---
5
+
6
+ # Defaults and host policies
7
+
8
+ Raft has two kinds of settings:
9
+
10
+ | Setting | Stored in | Applies to | Change with |
11
+ | --- | --- | --- | --- |
12
+ | Defaults for `new` | `~/.config/raft/config.json` on the controller | That controller only | `raft config set` and `raft config unset` |
13
+ | Required box note fields | Incus project `raft` on each host | Every controller that uses the host | `raft policy --require` |
14
+ | Stopped-box retention | Incus project `raft` on each host | Every box on the host | `raft policy --delete-stopped-after` |
15
+
16
+ `raft config` prints both kinds: the effective defaults with their source, and
17
+ each location's policies.
18
+
19
+ ```sh title="CONTROLLER: Inspect defaults and policies"
20
+ raft config
21
+ ```
22
+
23
+ ```json title="Example output"
24
+ {
25
+ "file": "/root/.config/raft/config.json",
26
+ "defaults": {
27
+ "location": {
28
+ "value": "lab",
29
+ "source": "config"
30
+ },
31
+ "ttl": {
32
+ "value": 3600,
33
+ "source": "config"
34
+ },
35
+ "cpu": {
36
+ "value": 1,
37
+ "source": "built-in"
38
+ },
39
+ "memory": {
40
+ "value": "2GiB",
41
+ "source": "built-in"
42
+ },
43
+ "disk": {
44
+ "value": "shared",
45
+ "source": "built-in"
46
+ },
47
+ "disposable": {
48
+ "value": true,
49
+ "source": "config"
50
+ }
51
+ },
52
+ "policy": {
53
+ "lab": {
54
+ "require": [
55
+ "purpose",
56
+ "owner"
57
+ ],
58
+ "requireChanged": "2026-10-21T09:12:44Z by ubuntu from me@laptop",
59
+ "deleteStoppedAfterHours": 48,
60
+ "deleteStoppedAfterSince": 1792573964,
61
+ "deleteStoppedAfterChanged": "2026-10-21T09:12:44Z by ubuntu from me@laptop"
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ `raft config` contacts every configured host to read its policy. It fails and
68
+ names the location when a host is unreachable.
69
+
70
+ ## Defaults for new
71
+
72
+ Set a default for any of `location`, `ttl`, `cpu`, `memory`, `disk` and
73
+ `disposable`:
74
+
75
+ ```sh title="CONTROLLER: Prefer bigger, longer, temporary boxes on paris"
76
+ raft config set location paris
77
+ raft config set ttl 3600
78
+ raft config set cpu 4
79
+ raft config set memory 16GiB
80
+ raft config set disk 20GiB
81
+ raft config set disposable true
82
+ raft config unset cpu
83
+ ```
84
+
85
+ Each command prints the old and new value. `raft new` takes an explicit flag
86
+ first, then the configured default, then the built-in default (first configured
87
+ location, 600 seconds, 1 CPU, 2 GiB, the shared pool, persistent). `resume`,
88
+ `fork`, `extend` and `recover` take no defaults: their TTL and target are
89
+ deliberate choices, and omitted sizes on resume and fork keep the source limits.
90
+
91
+ With `disposable true`, every `raft new` box is deleted, with its snapshots, when
92
+ it stops or expires. `raft new --persistent` keeps one box anyway, and
93
+ `--disposable` overrides `disposable false` the same way. The default applies to
94
+ `new --from` too: a snapshot has no lifecycle of its own. A fork stays
95
+ persistent unless it gets `--disposable`, whatever the default says, like every
96
+ other fork setting.
97
+
98
+ Size defaults (`cpu`, `memory`, `disk`) apply only where the built-in sizes do:
99
+ to boxes created from the image. A box from `new --from` keeps the snapshot's
100
+ location, CPU, RAM and disk, and a fork keeps its source's, whatever
101
+ `config.json` says. Only a flag changes them:
102
+
103
+ | Command | CPU and RAM | Disk | Disposable |
104
+ | --- | --- | --- | --- |
105
+ | `new` | Flag, then config, then 1 CPU / 2 GiB | Flag, then config, then the shared pool | Flag, then config, then persistent |
106
+ | `new --from` | Flag, then the snapshot's | `--disk SIZE`, then the snapshot's | Flag, then config, then persistent |
107
+ | `fork` | Flag, then the source's | The source's | `--disposable`, else persistent |
108
+
109
+ A `disk` default needs own box disks on the target host. On a location without
110
+ the `raft-disks` pool, or with too little unreserved space, `raft new` fails with
111
+ the same error as `--disk` and names the configured value as the cause:
112
+
113
+ ```text
114
+ $ raft new --purpose "Run the tests"
115
+ raft: lab has no raft-disks pool, so own box disks are not enabled there; nothing was created. Enable them with python3 deploy/deploy.py --location lab --disk-pool SIZE. The 20GiB own disk comes from defaults.disk in /root/.config/raft/config.json; give --disk shared or --disk SIZE, or run raft config unset disk
116
+ ```
117
+
118
+ `--disk shared` puts one box on the shared pool despite a `disk` default. It is
119
+ refused with `--from`, which keeps the snapshot's disk.
120
+
121
+ The file accepts a value exactly when the same text is accepted as the flag.
122
+ An unknown key, a bad value or malformed JSON makes `raft new` fail with the file
123
+ path and the key; Raft never ignores part of the file. Repair a bad key with
124
+ `raft config set KEY VALUE` or `raft config unset KEY`. Writes are atomic, keep
125
+ mode `0600` and remove the file when no default remains.
126
+
127
+ Scripts and CI jobs that need stable boxes should pass `--location`, `--ttl`,
128
+ `--cpu`, `--memory`, `--disk` and `--disposable` or `--persistent` explicitly,
129
+ because defaults differ between controllers.
130
+
131
+ ## Required box note fields
132
+
133
+ A host policy makes every box on that host carry the listed [box note](../reference/cli.md#note)
134
+ fields. `purpose` is always part of a note, so a policy must include it:
135
+
136
+ ```sh title="CONTROLLER: Require an owner on lab"
137
+ raft policy lab --require purpose,owner
138
+ raft policy lab --require none
139
+ ```
140
+
141
+ With that policy, a command that would leave a box without an owner fails before
142
+ it changes anything and says how to fix it:
143
+
144
+ ```text
145
+ $ raft new --location lab --purpose "Run the tests"
146
+ raft: location lab requires --owner (host policy). Add it: raft new --purpose "..." --owner "..." [--next "..."]
147
+ ```
148
+
149
+ The policy covers every command that creates a box or changes its note:
150
+
151
+ | Command | Note checked |
152
+ | --- | --- |
153
+ | `new`, including `new --from` | The note given to `new`; a snapshot's note is never inherited |
154
+ | `fork` | The note given to `fork`, otherwise the source box's note |
155
+ | `recover` | The note given to `recover`, otherwise the note in the archive |
156
+ | `note BOX` | The new note; `--clear` is rejected while a policy requires fields |
157
+
158
+ On `fork` and `recover`, `--purpose`, `--owner` and `--next` replace the whole
159
+ note, like `raft note`. Give all the fields the copy needs; omitted fields are
160
+ not taken from the source. `recover` without note flags can only check the
161
+ archived note after import. When it fails, Raft deletes the imported box in the
162
+ same locked step and reports the error. Pass the note to `recover` to fail before
163
+ the upload instead.
164
+
165
+ The controller checks the policy first, so the error arrives before any work.
166
+ The host checks it again in the same locked transaction that writes the note,
167
+ so a policy change cannot race a creation. That transaction checks the
168
+ requested size against the host and reserves own-disk space first; any refusal
169
+ comes before Incus creates the box.
170
+
171
+ ## Delete boxes left stopped
172
+
173
+ A retention policy makes a host delete boxes that stay stopped too long, with
174
+ their snapshots. Use it when agents create boxes and forget them:
175
+
176
+ ```sh title="CONTROLLER: Delete boxes stopped for 48 hours on lab"
177
+ raft policy lab --delete-stopped-after 48
178
+ raft policy lab --delete-stopped-after never
179
+ ```
180
+
181
+ The host expiry worker applies it every 15 seconds, and on `raft gc`, even when
182
+ no controller is online. It deletes a stopped box once the given hours have
183
+ passed since the later of:
184
+
185
+ - the box's stop, recorded in `user.raft.stopped-at` by `raft stop` and expiry;
186
+ - the policy change, recorded in `user.raft.delete-stopped-after.since`.
187
+
188
+ So a new or shorter policy never deletes a box sooner than its hours after the
189
+ change, including boxes that were stopped long before. Lengthening it or setting
190
+ it again restarts the grace period the same way.
191
+
192
+ | Box | What retention does |
193
+ | --- | --- |
194
+ | Running, including one whose TTL has not expired | Nothing |
195
+ | Stopped with `raft stop` or expiry | Deleted HOURS after the later of the stop and the policy change |
196
+ | Stopped another way: a poweroff inside the box, a native `incus stop`, a failed start or `raft recover` | The worker records the time it first sees the box stopped, and the clock starts then |
197
+ | Resumed before its deadline | Nothing; `resume` clears the stop time, and the next stop starts a new clock |
198
+
199
+ `raft list` shows when each stopped box will go, as `deleteAt`:
200
+
201
+ ```sh title="CONTROLLER: Stopped boxes and their deletion times"
202
+ raft list --location lab | jq -r 'select(.deleteAt != "") | "\(.box) \(.deleteAt | tonumber | todate)"'
203
+ ```
204
+
205
+ Retention deletes the whole box. Keep anything you need with
206
+ [`raft backup`](./backup-recovery.mdx) before the deadline, or resume the box to
207
+ reset its clock. Snapshots live inside the box and are deleted with it.
208
+
209
+ Retention needs the current host worker. On a host deployed by an older release,
210
+ `--delete-stopped-after HOURS` fails with
211
+ `Host expiry worker differs from this CLI; deploy the current Raft checkout first`
212
+ and changes nothing; redeploy the host, see
213
+ [Upgrades and image changes](./upgrades.mdx). Clearing it with `never` works on
214
+ any worker.
215
+
216
+ `raft prune --older-than DAYS` remains a manual, previewed cleanup that works
217
+ with or without a retention policy.
218
+
219
+ ## Audit trail
220
+
221
+ `raft policy` prints each change, for example
222
+ `lab: required note fields none -> purpose,owner` or
223
+ `lab: delete stopped boxes after never -> 48 hours`. The host records the last
224
+ change of each policy in a `.changed` project key (UTC time, the SSH account and
225
+ the controller's self-reported user and hostname) and writes a `raft` entry to
226
+ the host journal. The worker adds a journal entry for every box retention
227
+ deletes:
228
+
229
+ ```sh title="HOST: Review policy changes and retention deletions"
230
+ sudo incus project get raft user.raft.require
231
+ sudo incus project get raft user.raft.require.changed
232
+ sudo incus project get raft user.raft.delete-stopped-after
233
+ sudo incus project get raft user.raft.delete-stopped-after.changed
234
+ sudo journalctl -t raft
235
+ ```
236
+
237
+ ## Limits
238
+
239
+ Host policies are a guardrail for agents, not a security boundary:
240
+
241
+ - Anyone with SSH and sudo on the host can change or remove a policy, or create
242
+ boxes with native `incus` commands that skip the note policy.
243
+ - Retention trusts `user.raft.stopped-at`. The worker clears it when it sees a
244
+ box running, but a native `incus start` and stop between two worker runs (15
245
+ seconds apart) keeps the old stop time, so the box can be deleted on the next
246
+ run. Use `raft resume`, which clears the stop time.
247
+ - Controllers older than the release that added note policies do not check
248
+ them. Upgrade every controller that uses a host before relying on one. The
249
+ host worker enforces retention whatever the controller version.
250
+ - The controller identity in the `.changed` keys is reported by the controller
251
+ and is not verified. The SSH account comes from `sudo`.
252
+ - Raft checks that required fields are present and nonempty, not that their text
253
+ is accurate.
@@ -64,7 +64,15 @@ docker run --rm my-app
64
64
 
65
65
  ## Managing disk space
66
66
 
67
- Nested container layers and volumes reside on the shared 60 GiB `raft-data` Btrfs storage pool.
67
+ Nested container layers and volumes live in the box's filesystem. By default that is the shared 60 GiB `raft-data` Btrfs pool, where one box's Docker data can fill the pool for every box.
68
+
69
+ On hosts with own box disks enabled, give Docker-heavy boxes their own disk. Docker data stays inside it, and builds fail with "No space left on device" at the cap instead of filling the host:
70
+
71
+ ```sh title="CONTROLLER: Create a box with its own 40 GiB disk"
72
+ box=$(raft new --disk 40GiB --ttl 3600 --purpose "Docker image builds")
73
+ ```
74
+
75
+ See [Own box disks](../reference/capacity.mdx#own-box-disks).
68
76
 
69
77
  Prune dangling images without deleting active container data:
70
78
 
@@ -24,13 +24,19 @@ Raft has no option to disable automatic expiry.
24
24
  | `resume --ttl SECONDS` | Starts the saved box with a new deadline | Only possible while the box still exists |
25
25
  | `destroy` | Deletes the box and snapshots | Deletes the box and snapshots |
26
26
 
27
- Use `raft new --disposable` for temporary work. A fork is persistent unless
27
+ Use `raft new --disposable` for temporary work, or
28
+ `raft config set disposable true` to make it the default on this controller;
29
+ `raft new --persistent` then keeps one box. A fork is persistent unless
28
30
  `--disposable` is explicitly supplied; recovered boxes are persistent.
29
31
  Back up work before stopping a disposable box.
30
32
 
31
- Persistent stopped boxes still occupy a saved slot and storage. Each host allows
32
- four saved boxes. `raft prune --older-than 7` previews old stopped boxes;
33
- `--yes` applies deletion. There is no automatic persistent-box retention cleanup.
33
+ Persistent stopped boxes still occupy storage. Raft does not cap how many boxes
34
+ a host keeps. `raft prune --older-than 7` previews old stopped boxes;
35
+ `--yes` applies deletion. To delete them automatically, set a host retention
36
+ policy: with `raft policy lab --delete-stopped-after 48`, the host deletes any
37
+ box left stopped for 48 hours, and `raft list` shows each one's `deleteAt`.
38
+ Resuming a box resets its clock. See
39
+ [Defaults and host policies](./config-policy.mdx#delete-boxes-left-stopped).
34
40
 
35
41
  ## Read and reset the deadline
36
42
 
@@ -9,6 +9,7 @@
9
9
  "background-jobs",
10
10
  "long-running-services",
11
11
  "scripting-ci",
12
+ "config-policy",
12
13
  "desktop-services",
13
14
  "snapshots-forks",
14
15
  "backup-recovery",
@@ -94,9 +94,10 @@ raft exec "$copy" -- ls -la /workspace/my-project
94
94
  ```
95
95
 
96
96
  The template stays on its source host. Its source box can be running when you
97
- copy the saved snapshot. `new --from` uses normal creation defaults when sizing
98
- is omitted: 1 CPU and 2 GiB RAM. The new box has its own handle, MAC address,
99
- deadline, and saved slot.
97
+ copy the saved snapshot. `new --from` keeps the snapshot's CPU, RAM and disk
98
+ unless `--cpu`, `--memory` or `--disk SIZE` overrides them; `--disk` gives it an
99
+ own disk of a new size. The new box has its own handle, MAC address
100
+ and deadline.
100
101
 
101
102
  The snapshot includes packages, project files, credentials, and enabled services.
102
103
  It does not include process memory. Changing the original after snapshot creation
@@ -19,11 +19,13 @@ Raft does not have a global `--json` option. Parse each command's actual format:
19
19
  | --- | --- |
20
20
  | `new`, `fork`, `recover` | One location-qualified handle |
21
21
  | `exec --detach` | One `rfcmd-` job ID |
22
- | `list` | JSONL: one object per box, with `box`, `state`, `stopAt`, `note` |
22
+ | `list` | JSONL: one object per box, with `box`, `state`, `stopAt`, `deleteAt`, `disk`, `note` |
23
23
  | `info` | One pretty-printed native Incus metadata JSON object |
24
24
  | `usage` | One JSON object containing running resource counters |
25
- | `limits --json` | One JSON object per location (JSONL across multiple hosts) |
25
+ | `capacity --json` | One JSON object per location (JSONL across multiple hosts) |
26
26
  | `locations` | One JSON object per configured location (JSONL) |
27
+ | `config` | One pretty-printed JSON object with `file`, `defaults`, `policy` |
28
+ | `config set/unset`, `policy` | One line describing the change |
27
29
  | `prune` | JSONL candidates or deleted records with `box`, `action` |
28
30
  | `status` | systemd `ActiveState`, `SubState`, `ExecMainStatus` property lines |
29
31
  | `logs` | Guest journal text |
@@ -31,24 +33,38 @@ Raft does not have a global `--json` option. Parse each command's actual format:
31
33
  | Other inspection/maintenance commands | Human-readable text or no output |
32
34
 
33
35
  `list` emits nothing when there are no boxes. `stopAt` is the host Unix deadline
34
- as a string, or an empty string when absent. `note` is the box note object with
36
+ as a string, or an empty string when absent. `deleteAt` is the Unix time the
37
+ location's retention policy deletes a stopped box, in the same form, or an empty
38
+ string. `disk` is `"shared"` or the box's
39
+ own disk size, such as `"20GiB"`. `note` is the box note object with
35
40
  `purpose` and optional `owner` and `next`, or `null`. `info` uses native Incus keys rather
36
41
  than a normalized Raft schema. `usage` includes `memoryBytes`, `memoryLimit`,
37
42
  `cpuTime`, `effectiveCpus`, `filesystemUsedBytes`, `filesystemAvailableBytes`, and
38
- `filesystemScope`; disk values describe the shared pool, not an individual box.
39
- See [Capacity limits](../reference/capacity-limits.mdx) for capacity calculations.
43
+ `filesystemScope`; disk values describe the shared pool, or the box's own disk
44
+ when it has one. See [Box sizing and capacity](../reference/capacity.mdx) for the
45
+ advisory capacity fields.
40
46
 
41
47
  These examples require `jq` on the controller:
42
48
 
43
49
  ```sh title="CONTROLLER: Parse records without mixing stderr"
44
50
  raft list --location lab | jq -r 'select(.state == "Running") | .box'
45
- raft limits --location lab --json | jq '{location, savedSlots, warnings}'
51
+ raft capacity --location lab --json | jq '{location, headroom, warnings}'
46
52
  raft prune --location lab --older-than 7 | jq -r '.box'
47
53
  ```
48
54
 
55
+ `raft new` fills omitted options from the controller's
56
+ `~/.config/raft/config.json`, so the same script can create different boxes on
57
+ different controllers. Pass `--location`, `--ttl`, `--cpu`, `--memory`,
58
+ `--disk` and `--disposable` or `--persistent` in CI.
59
+ Hosts can also require box note fields; give `--purpose`, `--owner` and `--next`
60
+ so a policy cannot fail the job. A host retention policy can delete a box left
61
+ stopped between jobs; see [Defaults and host policies](./config-policy.mdx).
62
+
49
63
  Do not redirect `2>&1` into machine-readable output. Recovery reports its allocated
50
64
  handle on stderr before import finishes; only a successful completion prints the
51
- final handle on stdout.
65
+ final handle on stdout. Once a day Raft may also print a release notice on
66
+ stderr. Set `RAFT_NO_UPDATE_CHECK=1` in jobs that pin a CLI version or compare
67
+ stderr.
52
68
 
53
69
  ## Exit codes and job results
54
70
 
@@ -58,6 +74,7 @@ final handle on stdout.
58
74
  | Synchronous `exec` | Transport/command return code; preserves payload exit status when transport succeeds |
59
75
  | Recognized runtime/configuration failure | `1`, with `raft: ...` on stderr |
60
76
  | Argument parsing failure | `2`, with argparse diagnostics on stderr |
77
+ | `self-update --check` finds a newer release | `100` |
61
78
  | Caught keyboard interruption | `130` |
62
79
 
63
80
  SSH transport failures may return SSH's status rather than the payload's result.
@@ -73,7 +90,7 @@ running job's initial `ExecMainStatus=0` as completion. See
73
90
  ready. Probe a harmless command before running setup. This Bash example waits up
74
91
  to about a minute plus transport time, executes an uploaded project check, and
75
92
  attempts to stop the persistent box on exit. It assumes `./my-project/check.sh`
76
- exists and the configured host has a free saved slot:
93
+ exists:
77
94
 
78
95
  ```bash title="CONTROLLER or trusted Linux CI runner"
79
96
  set -euo pipefail
@@ -110,10 +127,10 @@ raft upload "$box" ./my-project /workspace/my-project --recursive
110
127
  raft exec "$box" -- bash -lc 'cd /workspace/my-project && bash ./check.sh'
111
128
  ```
112
129
 
113
- The stopped box remains available for inspection and still consumes a saved slot.
130
+ The stopped box remains available for inspection and still uses disk space.
114
131
  Destroy it after collecting needed files, or preview old stopped boxes with prune.
115
132
  For disposable CI boxes, deletion on stop/expiry removes artifacts and snapshots;
116
- download results before cleanup. A new box or a retry still consumes a saved slot.
133
+ download results before cleanup. A new box or a retry uses its own disk space.
117
134
 
118
135
  Command readiness does not mean Docker or a web application is ready. Probe the
119
136
  specific dependency (`docker info`, an HTTP health endpoint, or systemd status)
@@ -75,7 +75,7 @@ echo "Created fork: $copy"
75
75
  - **Instance-only copy**: Uses Incus `--instance-only`. Filesystem data, installed packages, and `/workspace` files are copied; source snapshots are not inherited.
76
76
  - **Immediate start**: `raft fork` launches the new container immediately with the specified TTL while the source remains stopped.
77
77
  - **Fresh network identity**: The fork receives a new container name, a fresh MAC address on `rfbr0`, and an independent IP address.
78
- - **Capacity accounting**: The fork counts toward the location's four-saved-box limit and uses storage from the shared 60 GiB Btrfs pool.
78
+ - **Disk**: The fork keeps the source disk. Both kinds fork in seconds as copy-on-write snapshots: a shared-pool source within the shared 60 GiB Btrfs pool, an own-disk source as a thin snapshot that reserves its full size in `raft-disks`.
79
79
 
80
80
  Destroy the clone when finished to release capacity:
81
81
 
@@ -89,7 +89,13 @@ raft destroy "$copy"
89
89
  copy=$(raft new --from "$box/pre-upgrade" --ttl 600)
90
90
  ```
91
91
 
92
- The source box can be running because Raft copies the saved snapshot. The new box stays on the same host, uses normal creation sizing defaults and has a fresh network identity. Filesystem secrets are copied too.
92
+ The source box can be running because Raft copies the saved snapshot. The new box stays on the same host, keeps the snapshot's CPU, RAM and disk unless `--cpu`, `--memory` or `--disk` overrides them, and has a fresh network identity. Copies are copy-on-write snapshots, so starting many boxes from one prepared snapshot is fast; on own disks each copy still reserves its full size. Filesystem secrets are copied too.
93
+
94
+ Add `--disk SIZE` to copy the snapshot onto an own disk of a new size, for example to give a full box more room:
95
+
96
+ ```sh
97
+ bigger=$(raft new --from "$box/pre-upgrade" --disk 40GiB --ttl 600)
98
+ ```
93
99
 
94
100
  Remove a snapshot you no longer need:
95
101
 
@@ -104,4 +110,4 @@ copy=$(raft fork "$box" --ttl 600 --cpu 2 --memory 4GiB)
104
110
  raft resume "$box" --ttl 600 --memory 2GiB
105
111
  ```
106
112
 
107
- Omitted sizing values keep the source limits. Forks are persistent by default; add `--disposable` to delete a clone when it stops or expires.
113
+ Omitted CPU and RAM values keep the source limits; the disk never changes on fork or resume. Forks are persistent by default, even when `raft config` sets `disposable true`; add `--disposable` to delete a clone when it stops or expires.