@microck/raft 0.5.0 → 0.7.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 (63) hide show
  1. package/README.md +27 -15
  2. package/deploy/build-image.py +4 -2
  3. package/deploy/raft-expire.py +109 -31
  4. package/deploy/verify-controls.py +6 -6
  5. package/deploy/verify-features.py +329 -19
  6. package/deploy/verify-jobs.py +264 -0
  7. package/deploy/verify-limits.py +22 -3
  8. package/deploy/verify-live.py +31 -6
  9. package/deploy/verify-output.py +208 -0
  10. package/deploy/verify-release.py +33 -4
  11. package/deploy/verify-run.py +571 -0
  12. package/deploy/verify-self-update.py +9 -1
  13. package/deploy/verify-sync.py +122 -0
  14. package/docs/benchmarks/read-latency-2026-10-29.json +70 -0
  15. package/docs/boat-capabilities.md +60 -7
  16. package/docs/boat-parity-design.md +1 -0
  17. package/docs/boot-performance.mdx +21 -0
  18. package/docs/contract.md +88 -13
  19. package/docs/first-box.mdx +22 -19
  20. package/docs/guides/background-jobs.mdx +38 -31
  21. package/docs/guides/backup-recovery.mdx +4 -3
  22. package/docs/guides/ci-jobs.mdx +209 -0
  23. package/docs/guides/config-policy.mdx +109 -35
  24. package/docs/guides/files.mdx +2 -2
  25. package/docs/guides/index.mdx +31 -0
  26. package/docs/guides/long-running-services.mdx +9 -3
  27. package/docs/guides/meta.json +7 -2
  28. package/docs/guides/project-setup.mdx +1 -1
  29. package/docs/guides/scripting-ci.mdx +36 -14
  30. package/docs/guides/snapshots-forks.mdx +4 -2
  31. package/docs/guides/troubleshooting.mdx +19 -2
  32. package/docs/guides/upgrades.mdx +11 -9
  33. package/docs/index.mdx +26 -28
  34. package/docs/maintainers/index.mdx +18 -0
  35. package/docs/maintainers/meta.json +12 -0
  36. package/docs/meta.json +5 -9
  37. package/docs/operations.mdx +17 -10
  38. package/docs/publishing.md +14 -4
  39. package/docs/reference/capacity.mdx +2 -1
  40. package/docs/reference/cli.md +135 -43
  41. package/docs/reference/configuration.mdx +10 -6
  42. package/docs/reference/meta.json +3 -1
  43. package/docs/releases/0.2.0.md +1 -0
  44. package/docs/releases/0.2.1.md +1 -0
  45. package/docs/releases/0.3.0.md +1 -0
  46. package/docs/releases/0.4.0.md +1 -0
  47. package/docs/releases/0.5.0.md +1 -0
  48. package/docs/releases/0.6.0.md +17 -0
  49. package/docs/releases/0.7.0.md +23 -0
  50. package/docs/releases/index.mdx +21 -0
  51. package/docs/releases/meta.json +15 -0
  52. package/docs/requirements.mdx +1 -1
  53. package/docs/reviews/docs-audit-2026-10-24.md +92 -0
  54. package/docs/reviews/performance-audit-2026-10-29.md +90 -0
  55. package/docs/site-development.md +1 -0
  56. package/docs/verification.mdx +22 -4
  57. package/package.json +3 -1
  58. package/raft.py +526 -193
  59. package/raft_files.py +13 -1
  60. package/raft_jobs.py +390 -0
  61. package/raft_output.py +166 -0
  62. package/raft_run.py +330 -0
  63. package/skills/raft-cli/SKILL.md +78 -182
@@ -5,11 +5,11 @@ description: Run detached commands with systemd logging and process management.
5
5
 
6
6
  # Background jobs
7
7
 
8
- Raft runs detached background commands under the guest systemd init system using transient service units.
8
+ Raft runs detached commands under guest systemd and captures their results with a transient host supervisor.
9
9
 
10
10
  ## Target workspace handle
11
11
 
12
- Commands in this guide target a running workspace. Select one from `raft list` (see [First working box](../first-box.mdx)):
12
+ Launching a job requires a running workspace. Select one from `raft list` (see [First working box](../first-box.mdx)):
13
13
 
14
14
  ```sh title="CONTROLLER: Select running workspace"
15
15
  raft list
@@ -33,49 +33,56 @@ The command returns a 32-hex systemd unit name prefixed with `rfcmd-` (such as `
33
33
 
34
34
  ## Checking job status
35
35
 
36
- Use `raft status` to query guest systemd unit properties:
36
+ `raft status "$box" "$job"` returns a retained JSON record:
37
37
 
38
- ```sh title="CONTROLLER: Inspect background job state"
39
- raft status "$box" "$job"
38
+ ```json
39
+ {"job":"rfcmd-...","box":"lab:rf-...","state":"completed","exitCode":23,"startedAt":1793280000,"finishedAt":1793280010,"droppedBytes":0}
40
40
  ```
41
41
 
42
- Active jobs report running substate:
43
- ```text
44
- ActiveState=active
45
- SubState=running
46
- ExecMainStatus=0
47
- ```
48
-
49
- Because Raft runs detached commands with `RemainAfterExit=yes`, completed jobs retain their exit record:
50
- ```text
51
- ActiveState=active
52
- SubState=exited
53
- ExecMainStatus=0
54
- ```
55
-
56
- <Callout type="info" title="Exit status vs CLI return code">
57
- `ExecMainStatus` reports the command payload exit code inside the container. The `raft status` command returns exit code 0 when the query succeeds. It queries unit state and does not report process PIDs or start timestamps.
58
- </Callout>
59
-
60
- ## Viewing job logs
42
+ States are `starting`, `running`, `completed`, `cancelled`, `interrupted` and
43
+ `failed`. `completed` means the command finished, not that it succeeded; inspect
44
+ `exitCode`. An interrupted job has no confirmed payload exit code. A transport interruption can leave the guest command running;
45
+ inspect or cancel it before rerunning the payload. Timestamps
46
+ are host Unix seconds. A successful status query itself returns zero.
61
47
 
62
- Retrieve output recorded in the guest systemd journal:
48
+ ## Viewing retained output
63
49
 
64
- ```sh title="CONTROLLER: Retrieve job log output"
50
+ ```sh
65
51
  raft logs "$box" "$job"
66
52
  ```
67
53
 
68
- The command runs `journalctl --no-pager -o cat -u <job>` inside the container. It prints recorded logs without a pager or stream follow mode.
54
+ A host supervisor retains the first 1 MiB of combined stdout and stderr. Further
55
+ output is drained and counted in `droppedBytes` when the job ends. Logs and final
56
+ status remain readable after the workspace stops or is deleted; keep its exact
57
+ original handle. They are not copied by workspace snapshots or forks.
69
58
 
70
- ## Cancelling jobs
59
+ Results live in the root-private host directory `/var/lib/raft-jobs`. They expire
60
+ seven days after completion. Job commands prune expired records on access, so an
61
+ idle host may retain expired files until the next access. The store holds at most
62
+ 512 records and refuses new jobs when full instead of evicting unexpired results.
63
+ Unreadable records remain on the host for inspection and count toward the limit.
64
+ They do not block other jobs. A query for a damaged record reports its host path;
65
+ inspect that directory and its retained output before deciding how to recover it.
66
+ Output can contain secrets; this is host-local retention, not an off-host backup.
71
67
 
72
- Terminate a running detached command:
68
+ ## Cancelling jobs
73
69
 
74
- ```sh title="CONTROLLER: Terminate background job"
70
+ ```sh
75
71
  raft cancel "$box" "$job"
76
72
  ```
77
73
 
78
- `raft cancel` executes `systemctl stop <job>`, terminating processes in the job's systemd control group.
74
+ Cancellation stops the guest systemd control group. Retained output remains
75
+ available. If a job is still starting and its guest unit is not yet registered,
76
+ cancellation reports an error; inspect its state before retrying. Unknown job
77
+ IDs and IDs belonging to another workspace are rejected. Cancellation waits up to
78
+ 120 seconds. If it times out, the cancellation request remains recorded; inspect
79
+ the job and retry cancellation.
80
+
81
+ Host reboot or guest shutdown interrupts active jobs. The next inspection marks
82
+ an absent supervisor as interrupted rather than assuming success. Completed
83
+ results survive host reboot. This development implementation has no fallback to
84
+ old guest-only journals; inspect old jobs with their controller version before
85
+ upgrading.
79
86
 
80
87
  ## Extending workspace lifetime
81
88
 
@@ -9,7 +9,8 @@ Raft exports portable workspace archives and restores them onto compatible hosts
9
9
 
10
10
  ## Target workspace handle
11
11
 
12
- Commands in this guide require a stopped workspace:
12
+ Use a **persistent** workspace. Stop it before backup; stopping a disposable box
13
+ deletes it and its snapshots, so copy files out instead.
13
14
 
14
15
  ```sh title="CONTROLLER: Select and stop target workspace"
15
16
  raft list
@@ -35,7 +36,7 @@ Backup archives contain the entire guest filesystem, including credentials, SSH
35
36
 
36
37
  Raft enforces safeguards during export:
37
38
 
38
- - **Private permissions**: Destination mode must be `0600`. Group and world permissions must be zero before export begins.
39
+ - **Private permissions**: Raft creates the staged archive with mode `0600` and publishes it privately. Choose a new destination path; do not pre-create the file.
39
40
  - **Atomic link publication**: After export and `fsync`, Raft publishes the archive using `os.link(archive.name, destination)`. Hard-link support is checked at final publication rather than preflight; export can succeed while publication fails if the filesystem lacks hard-link support.
40
41
  - **No overwriting**: The destination file must not already exist.
41
42
  - **Lifecycle lock**: Export holds `/run/lock/raft-incus.lock`. Expiry checks for other containers on the host are delayed while the lock is held.
@@ -53,7 +54,7 @@ echo "Recovered workspace: $recovered"
53
54
 
54
55
  - **Architecture compatibility**: Target host must match the native CPU architecture (ARM64 to ARM64, AMD64 to AMD64). Cross-architecture recovery is not supported.
55
56
  - **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
+ - **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
58
  - **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
59
  - **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
60
 
@@ -0,0 +1,209 @@
1
+ ---
2
+ title: Disposable CI jobs
3
+ description: Run trusted builds in temporary Raft boxes, collect results, and clean up with a bounded lifetime.
4
+ ---
5
+
6
+ # Disposable CI jobs
7
+
8
+ `raft run` runs one foreground command in a fresh disposable box. It creates the
9
+ box, waits for command readiness, optionally uploads source, streams command
10
+ output, collects requested result directories, and attempts ownership-checked
11
+ cleanup even after failure or cancellation. It does not register a GitHub
12
+ self-hosted runner or replace your CI scheduler.
13
+
14
+ <Callout type="warn" title="Unreleased: requires the 0.7.0 development code">
15
+ The published release is 0.6.0, which does not include `raft run`. As checked on
16
+ 2026-10-24, no 0.7.0 npm package or GitHub release assets are available. Use the
17
+ [stable scripting workflow](./scripting-ci.mdx) unless you intend to test a reviewed
18
+ source revision. Do not install a nonexistent 0.7.0 package.
19
+ </Callout>
20
+
21
+ For development, run `python3 /path/to/raft.py run` from the reviewed checkout.
22
+ The CI example below pins the full source commit instead of an unpublished tag.
23
+
24
+ ## Trust boundary
25
+
26
+ Use dedicated hosts and trusted jobs only. The controller's SSH identity has
27
+ passwordless sudo and controls the host; it is not a scoped, per-job CI token.
28
+ Boxes are unprivileged Incus containers sharing the host kernel, not separate
29
+ kernel VMs. Do not expose this identity to unreviewed pull requests, fork jobs,
30
+ or a `pull_request_target` workflow that checks out attacker-controlled code.
31
+
32
+ Raft does not install or register a runner, configure repository secrets, forward
33
+ your SSH agent, copy controller environment variables, or buy cloud capacity.
34
+ The operator must approve and configure existing host access separately. Never
35
+ copy a host private key or a GitHub token into the guest.
36
+
37
+ ## One command
38
+
39
+ ```sh title="CONTROLLER: Git working tree"
40
+ RAFT_NO_UPDATE_CHECK=1 raft run \
41
+ --location lab --timeout 600 --cpu 2 --memory 4GiB \
42
+ --purpose "Project tests" --owner "Local CI" --next "Delete after collection" \
43
+ --push . --pull /workspace/project/reports ./ci-reports \
44
+ -- bash -lc 'set -euo pipefail; mkdir -p reports; npm ci; npm test 2>&1 | tee reports/test.log'
45
+ ```
46
+
47
+ The controller needs Linux, Python 3.11.8+, OpenSSH, Git for `--push`, a configured
48
+ Raft location, verified SSH host keys, batch authentication, and the current host
49
+ expiry worker. Ordinary image/host sizing and note policies still apply. `run`
50
+ always forces disposable behavior, regardless of controller lifetime defaults.
51
+ It never retries the payload command.
52
+
53
+ - stdout/stderr stream the payload; lifecycle messages and elapsed time go to
54
+ stderr. There is no cost estimate or JSON result protocol.
55
+ - `--timeout` is one total deadline for Git filtering, creation, readiness, upload
56
+ and execution, not a fresh timeout for each step. Default: 600 seconds; range:
57
+ 1 to 2,591,880 seconds.
58
+ - `--push` includes tracked and unignored untracked files from the chosen Git
59
+ working tree, including working-tree edits. It excludes `.git` at every level
60
+ and refuses submodules. Tracked files remain included even if ignored later:
61
+ this is not a secret scanner. Deleted tracked files are absent.
62
+ - `--push-all` explicitly copies the complete directory, including hidden and
63
+ ignored files and Git metadata. Inspect it for credentials first. This is the
64
+ opt-in alternative for non-Git or explicitly staged trees; it is not a silent
65
+ fallback when Git fails.
66
+ - Uploads default to `/workspace/project`; without an upload, commands default
67
+ to `/workspace`. `--cwd` overrides this. An upload destination must not exist,
68
+ and its parent must already exist. Raft never merges into a stale checkout.
69
+ - Repeat `--pull GUEST_DIRECTORY NEW_LOCAL_DIRECTORY` to collect directories,
70
+ including after a failing payload. Local parents must exist; destinations
71
+ must be new, including no existing symlinks. Files can be placed in a result
72
+ directory. Collection happens before deletion, with a shared 30-second grace.
73
+ A failed pull does not stop later pulls unless that grace has expired.
74
+ - Exit status preserves a failing payload, including signal-style statuses.
75
+ Job timeout returns 124, Ctrl-C 130, SIGTERM 143. Setup/collection/cleanup
76
+ failure returns 1 when there is no earlier failing status. A successful
77
+ payload is not reported successful when cleanup cannot be confirmed.
78
+
79
+ The host receives a unique ownership marker and a fixed expiry of timeout + 120
80
+ seconds as part of creation, before startup. Cleanup checks ownership under the
81
+ host lifecycle lock and has a separate 30-second grace. Repeated cancellation is
82
+ ignored during these bounded collection/cleanup attempts. No lease renewal or
83
+ `--keep` mode is implemented.
84
+
85
+ If creation was not acknowledged, cleanup first records that run's cancellation
86
+ under the same host lifecycle lock. Creation checks that record before allocating,
87
+ so a queued SSH request cannot start the box after cleanup has confirmed absence.
88
+ These empty per-run records live in the root-only `/run/raft-ci-cancelled`
89
+ directory, use no-follow operations, and remain until host reboot, when queued
90
+ processes also disappear. Ordinary acknowledged runs leave no cancellation file.
91
+
92
+ If the controller is killed before cleanup, SSH is lost, or cleanup fails, inspect
93
+ the exact handle printed on stderr. A disconnected create may still finish when
94
+ cancellation could not be recorded. The disposable expiry worker is a backstop,
95
+ not a guarantee of instantaneous deletion: an unreachable host, stopped worker or
96
+ held lifecycle lock can delay it. Do not blindly rerun an uncertain job or assume
97
+ its side effects were rolled back.
98
+
99
+ ## Prepared templates instead of shared mutable caches
100
+
101
+ Prepare dependencies and tool versions in a persistent base box, stop it, then
102
+ create a named snapshot. See [project setup](./project-setup.mdx) and
103
+ [snapshots](./snapshots-forks.mdx). Keep the source box and snapshot available:
104
+
105
+ ```sh title="CONTROLLER: Use an existing trusted snapshot"
106
+ raft run --from lab:rf-REPLACE_WITH_BASE_ID/ci-base --timeout 900 \
107
+ --purpose "Cached build" --owner "CI" --next "Delete after tests" \
108
+ --push . -- bash -lc 'npm ci --prefer-offline && npm test'
109
+ ```
110
+
111
+ The template stays on its source host and retains its size unless explicitly
112
+ overridden. Put reusable caches outside the upload destination, which must be
113
+ new. Each job gets an independent copy; its mutations do not update the template
114
+ or another job's cache. Refresh templates deliberately when dependencies change.
115
+
116
+ Raft snapshots retain filesystem contents, including credentials and Docker
117
+ storage. They do not save process memory. Inspect and sanitize the base before
118
+ sharing it with jobs. Do not assume Boat's selective snapshot/cache rules apply
119
+ to Raft's whole-filesystem copies.
120
+
121
+ ## GitHub Actions orchestration example
122
+
123
+ The following is an example for a separate trusted project, not a change to
124
+ Raft's own runner fleet. An ordinary `ubuntu-latest` job invokes Raft over SSH.
125
+ Before using it, an operator must approve storing a dedicated CI host key as
126
+ `RAFT_SSH_KEY`, verified host-key lines as `RAFT_KNOWN_HOSTS`, and the location
127
+ inventory JSON as `RAFT_INVENTORY`. The inventory's SSH target must use that key
128
+ and its image fingerprint must be pinned. Never generate trust with an unchecked
129
+ `ssh-keyscan` or disable host-key checking. Restrict access to these secrets.
130
+
131
+ The example pins development commit `7d678bb079a9b2dcde4e2eb9e4cd0468d00f50f3` in a
132
+ separate Python environment. Review that code and its unreleased status
133
+ before use; do not silently replace it with a moving branch.
134
+
135
+ ```yaml title="EXAMPLE: .github/workflows/trusted-build.yml"
136
+ name: Trusted Raft build
137
+ on:
138
+ push:
139
+ branches: [main]
140
+ workflow_dispatch:
141
+ permissions:
142
+ contents: read
143
+ jobs:
144
+ test:
145
+ runs-on: ubuntu-latest
146
+ timeout-minutes: 20
147
+ env:
148
+ RAFT_NO_UPDATE_CHECK: '1'
149
+ steps:
150
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
151
+ with:
152
+ persist-credentials: false
153
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
154
+ with:
155
+ python-version: '3.14'
156
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
157
+ with:
158
+ node-version: '24'
159
+ - name: Install reviewed development controller
160
+ run: |
161
+ python -m venv "$RUNNER_TEMP/raft-cli"
162
+ "$RUNNER_TEMP/raft-cli/bin/pip" install "git+https://github.com/Microck/raft.git@7d678bb079a9b2dcde4e2eb9e4cd0468d00f50f3"
163
+ echo "$RUNNER_TEMP/raft-cli/bin" >> "$GITHUB_PATH"
164
+ - name: Configure approved existing access
165
+ env:
166
+ SSH_KEY: ${{ secrets.RAFT_SSH_KEY }}
167
+ KNOWN_HOSTS: ${{ secrets.RAFT_KNOWN_HOSTS }}
168
+ INVENTORY: ${{ secrets.RAFT_INVENTORY }}
169
+ run: |
170
+ umask 077
171
+ mkdir -p ~/.ssh ~/.config/raft
172
+ printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
173
+ printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
174
+ printf '%s\n' "$INVENTORY" > ~/.config/raft/incus.json
175
+ printf 'Host *\n BatchMode yes\n StrictHostKeyChecking yes\n IdentitiesOnly yes\n' > ~/.ssh/config
176
+ - name: Run remote tests and collect logs
177
+ run: |
178
+ raft run --location ci --timeout 900 --cpu 2 --memory 4GiB \
179
+ --purpose "Trusted project tests" --owner "GitHub Actions" --next "Delete after tests" \
180
+ --push . --pull /workspace/project/reports ./ci-reports \
181
+ -- bash -lc 'set -euo pipefail; mkdir -p reports; npm ci; npm test 2>&1 | tee reports/test.log'
182
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
183
+ if: always()
184
+ with:
185
+ name: test-reports
186
+ path: ci-reports/
187
+ if-no-files-found: warn
188
+ ```
189
+
190
+ No checkout credential is needed in the guest. The secret files are outside the
191
+ uploaded tree, and `.git` is excluded. Set a CI job limit longer than Raft's
192
+ command deadline plus collection and cleanup grace. A forcibly terminated CI
193
+ runner cannot perform final cleanup; check the host expiry service and inventory.
194
+
195
+ ## Comparison and cost
196
+
197
+ [Boat's CI guide](https://docs.boat.dev/ci) uses the same broad pattern: an existing
198
+ CI job orchestrates a disposable remote workspace. Boat also provides a managed
199
+ `run` command, scoped API keys, renewable leases and a different VM/snapshot
200
+ model. Raft implements a fixed lifetime on operator-owned Incus hosts and uses
201
+ host-level SSH access; these are meaningful differences, not interchangeable
202
+ security or billing guarantees.
203
+
204
+ There is no universal “10x cheaper” claim for Raft. Compare the complete host,
205
+ storage, idle capacity, operations and orchestration-runner cost. Standard
206
+ GitHub-hosted runners are already free for public repositories; remote capacity
207
+ can still help with specific hardware, prepared environments or isolation from
208
+ other trusted jobs. See [Boat pricing](https://docs.boat.dev/pricing) and
209
+ [GitHub Actions billing](https://docs.github.com/en/billing/concepts/product-billing/github-actions).
@@ -1,19 +1,20 @@
1
1
  ---
2
- title: Defaults and note policies
3
- description: Set controller defaults for new boxes and require box note fields on a host.
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
4
  ---
5
5
 
6
- # Defaults and note policies
6
+ # Defaults and host policies
7
7
 
8
8
  Raft has two kinds of settings:
9
9
 
10
10
  | Setting | Stored in | Applies to | Change with |
11
11
  | --- | --- | --- | --- |
12
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` |
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` |
14
15
 
15
- `raft config` prints both: the effective defaults with their source, and each
16
- location's policy.
16
+ `raft config` prints both kinds: the effective defaults with their source, and
17
+ each location's policies.
17
18
 
18
19
  ```sh title="CONTROLLER: Inspect defaults and policies"
19
20
  raft config
@@ -42,6 +43,10 @@ raft config
42
43
  "disk": {
43
44
  "value": "shared",
44
45
  "source": "built-in"
46
+ },
47
+ "disposable": {
48
+ "value": true,
49
+ "source": "config"
45
50
  }
46
51
  },
47
52
  "policy": {
@@ -50,7 +55,10 @@ raft config
50
55
  "purpose",
51
56
  "owner"
52
57
  ],
53
- "changed": "2026-10-21T09:12:44Z by ubuntu from me@laptop"
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"
54
62
  }
55
63
  }
56
64
  }
@@ -61,33 +69,42 @@ names the location when a host is unreachable.
61
69
 
62
70
  ## Defaults for new
63
71
 
64
- Set a default for any of `location`, `ttl`, `cpu`, `memory` and `disk`:
72
+ Set a default for any of `location`, `ttl`, `cpu`, `memory`, `disk` and
73
+ `disposable`:
65
74
 
66
- ```sh title="CONTROLLER: Prefer bigger, longer boxes on paris"
75
+ ```sh title="CONTROLLER: Prefer bigger, longer, temporary boxes on paris"
67
76
  raft config set location paris
68
77
  raft config set ttl 3600
69
78
  raft config set cpu 4
70
79
  raft config set memory 16GiB
71
80
  raft config set disk 20GiB
81
+ raft config set disposable true
72
82
  raft config unset cpu
73
83
  ```
74
84
 
75
85
  Each command prints the old and new value. `raft new` takes an explicit flag
76
86
  first, then the configured default, then the built-in default (first configured
77
- location, 600 seconds, 1 CPU, 2 GiB, the shared pool). `resume`, `fork`,
78
- `extend` and `recover` take no defaults: their TTL and target are deliberate
79
- choices, and omitted sizes on resume and fork keep the source limits.
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.
80
97
 
81
98
  Size defaults (`cpu`, `memory`, `disk`) apply only where the built-in sizes do:
82
99
  to boxes created from the image. A box from `new --from` keeps the snapshot's
83
100
  location, CPU, RAM and disk, and a fork keeps its source's, whatever
84
101
  `config.json` says. Only a flag changes them:
85
102
 
86
- | Command | CPU and RAM | Disk |
87
- | --- | --- | --- |
88
- | `new` | Flag, then config, then 1 CPU / 2 GiB | Flag, then config, then the shared pool |
89
- | `new --from` | Flag, then the snapshot's | `--disk SIZE`, then the snapshot's |
90
- | `fork` | Flag, then the source's | The source's |
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 |
91
108
 
92
109
  A `disk` default needs own box disks on the target host. On a location without
93
110
  the `raft-disks` pool, or with too little unreserved space, `raft new` fails with
@@ -107,9 +124,9 @@ path and the key; Raft never ignores part of the file. Repair a bad key with
107
124
  `raft config set KEY VALUE` or `raft config unset KEY`. Writes are atomic, keep
108
125
  mode `0600` and remove the file when no default remains.
109
126
 
110
- Scripts and CI jobs that need stable sizing should pass `--location`, `--ttl`,
111
- `--cpu`, `--memory` and `--disk` explicitly, because defaults differ between
112
- controllers.
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.
113
130
 
114
131
  ## Required box note fields
115
132
 
@@ -118,7 +135,7 @@ fields. `purpose` is always part of a note, so a policy must include it:
118
135
 
119
136
  ```sh title="CONTROLLER: Require an owner on lab"
120
137
  raft policy lab --require purpose,owner
121
- raft policy lab --clear
138
+ raft policy lab --require none
122
139
  ```
123
140
 
124
141
  With that policy, a command that would leave a box without an owner fails before
@@ -151,29 +168,86 @@ so a policy change cannot race a creation. That transaction checks the
151
168
  requested size against the host and reserves own-disk space first; any refusal
152
169
  comes before Incus creates the box.
153
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
+
154
219
  ## Audit trail
155
220
 
156
- `raft policy` prints the change, for example
157
- `lab: required note fields none -> purpose,owner`. The host records the last
158
- change in the project key `user.raft.require.changed` (UTC time, the SSH account
159
- and the controller's self-reported user and hostname) and writes a `raft` entry
160
- to the host journal:
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:
161
228
 
162
- ```sh title="HOST: Review policy changes"
229
+ ```sh title="HOST: Review policy changes and retention deletions"
163
230
  sudo incus project get raft user.raft.require
164
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
165
234
  sudo journalctl -t raft
166
235
  ```
167
236
 
168
237
  ## Limits
169
238
 
170
- Note policies are a guardrail for agents, not a security boundary:
171
-
172
- - Anyone with SSH and sudo on the host can change or remove the policy, or create
173
- boxes with native `incus` commands that skip it.
174
- - Controllers older than the release that added policies do not check them.
175
- Upgrade every controller that uses a host before relying on its policy.
176
- - The controller identity in `user.raft.require.changed` is reported by the
177
- controller and is not verified. The SSH account comes from `sudo`.
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`.
178
252
  - Raft checks that required fields are present and nonempty, not that their text
179
253
  is accurate.
@@ -5,11 +5,11 @@ description: Transfer files between your controller machine and running Raft wor
5
5
 
6
6
  # File transfer
7
7
 
8
- Upload and download individual files between your local controller and running workspaces over verified host SSH and Incus instance APIs.
8
+ Upload and download individual files between your controller and a box over verified host SSH and Incus instance APIs.
9
9
 
10
10
  ## Target workspace handle
11
11
 
12
- Commands in this guide target a running workspace. Select one from `raft list` (see [First working box](../first-box.mdx)):
12
+ Use a running box for all examples below. Individual-file transfers also work while stopped; recursive directory transfers require a running box. Select one from `raft list` (see [First working box](../first-box.mdx)):
13
13
 
14
14
  ```sh title="CONTROLLER: Select running workspace"
15
15
  raft list
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Task guides
3
+ description: Pick a workflow after setting up your first Raft host.
4
+ ---
5
+
6
+ # Task guides
7
+
8
+ Start with [your first box](../first-box.mdx) if you have not run a command yet.
9
+ Commands run on the controller unless a block is labeled **HOST**.
10
+
11
+ ## Develop
12
+
13
+ - [Project setup and templates](./project-setup.mdx): upload a project and reuse dependencies.
14
+ - [Docker](./docker.mdx): build images and manage guest disk use.
15
+ - [File transfer](./files.mdx): copy individual files or directory trees.
16
+ - [Desktop and private services](./desktop-services.mdx): open a browser, desktop or local tunnel.
17
+
18
+ ## Automate
19
+
20
+ - [Background jobs](./background-jobs.mdx): detach, inspect logs and cancel.
21
+ - [Lifetime and persistent services](./long-running-services.mdx): set deadlines and restart services after resume.
22
+ - [Scripting and CI](./scripting-ci.mdx): parse output and handle failures.
23
+ - [Disposable CI jobs — unreleased 0.7.0](./ci-jobs.mdx): run and clean up one temporary build.
24
+
25
+ ## Operate
26
+
27
+ - [Snapshots and forks](./snapshots-forks.mdx): save disk state or make a same-host copy.
28
+ - [Backup and recovery](./backup-recovery.mdx): move a whole box to a compatible host.
29
+ - [Defaults and policies](./config-policy.mdx): set creation defaults and retention rules.
30
+ - [Upgrades](./upgrades.mdx): update the CLI, host worker and image separately.
31
+ - [Troubleshooting](./troubleshooting.mdx): start from a symptom.
@@ -11,7 +11,7 @@ terminal, and detached jobs do not extend that deadline automatically.
11
11
  ## Choose a lifetime and retention policy
12
12
 
13
13
  ```sh title="CONTROLLER: Create a persistent box"
14
- box=$(raft new --location lab --ttl 3600)
14
+ box=$(raft new --location lab --ttl 3600 --persistent)
15
15
  ```
16
16
 
17
17
  TTL is in seconds: 60 to 2,592,000 (30 days), with a creation default of 600.
@@ -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
33
  Persistent stopped boxes still occupy storage. Raft does not cap how many boxes
32
34
  a host keeps. `raft prune --older-than 7` previews old stopped boxes;
33
- `--yes` applies deletion. There is no automatic persistent-box retention cleanup.
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