@microck/raft 0.0.0-stage → 0.2.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 (62) hide show
  1. package/.github/assets/raft-logo.png +0 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -2
  4. package/deploy/benchmark-boot.py +150 -0
  5. package/deploy/build-image.py +95 -0
  6. package/deploy/deploy.py +176 -0
  7. package/deploy/incus-network.conf +4 -0
  8. package/deploy/install-incus.sh +22 -0
  9. package/deploy/raft-expire.py +62 -0
  10. package/deploy/raft-expire.service +8 -0
  11. package/deploy/raft-expire.timer +8 -0
  12. package/deploy/raft-network.service +12 -0
  13. package/deploy/raft-network.sh +40 -0
  14. package/deploy/verify-controls.py +241 -0
  15. package/deploy/verify-features.py +461 -0
  16. package/deploy/verify-fresh-host.py +301 -0
  17. package/deploy/verify-image-tests.py +139 -0
  18. package/deploy/verify-image.py +95 -0
  19. package/deploy/verify-limits.py +110 -0
  20. package/deploy/verify-live.py +903 -0
  21. package/deploy/verify-release.py +132 -0
  22. package/deploy/verify-tools.py +105 -0
  23. package/docs/benchmarks/amd64-native-ci.json +137 -0
  24. package/docs/benchmarks/arm64-baseline-a.json +108 -0
  25. package/docs/benchmarks/arm64-baseline-b.json +108 -0
  26. package/docs/benchmarks/arm64-candidate-a.json +109 -0
  27. package/docs/benchmarks/arm64-desktop-local.json +22 -0
  28. package/docs/benchmarks/arm64-native-ci.json +137 -0
  29. package/docs/boat-capabilities.md +64 -0
  30. package/docs/boat-parity-design.md +26 -0
  31. package/docs/boot-performance.mdx +102 -0
  32. package/docs/contract.md +93 -0
  33. package/docs/first-box.mdx +82 -0
  34. package/docs/guides/background-jobs.mdx +92 -0
  35. package/docs/guides/backup-recovery.mdx +111 -0
  36. package/docs/guides/desktop-services.mdx +87 -0
  37. package/docs/guides/docker.mdx +84 -0
  38. package/docs/guides/files.mdx +91 -0
  39. package/docs/guides/meta.json +14 -0
  40. package/docs/guides/snapshots-forks.mdx +107 -0
  41. package/docs/guides/troubleshooting.mdx +127 -0
  42. package/docs/incus.example.json +3 -0
  43. package/docs/index.mdx +36 -0
  44. package/docs/meta.json +21 -0
  45. package/docs/operations.mdx +224 -0
  46. package/docs/publishing.md +48 -0
  47. package/docs/reference/capacity-limits.mdx +83 -0
  48. package/docs/reference/cli.md +279 -0
  49. package/docs/reference/configuration.mdx +77 -0
  50. package/docs/reference/meta.json +10 -0
  51. package/docs/releases/0.2.0.md +22 -0
  52. package/docs/requirements.mdx +47 -0
  53. package/docs/reviews/code-review-2026-10-06.md +168 -0
  54. package/docs/site-development.md +80 -0
  55. package/docs/verification.mdx +231 -0
  56. package/images/incus/build.sh +57 -0
  57. package/images/incus/desktop.sh +21 -0
  58. package/images/incus/raft-desktop.service +7 -0
  59. package/package.json +46 -4
  60. package/raft.py +955 -0
  61. package/raft_files.py +134 -0
  62. package/skills/raft-cli/SKILL.md +117 -0
@@ -0,0 +1,279 @@
1
+ ---
2
+ title: CLI command reference
3
+ description: Complete command-line reference for the Raft controller CLI.
4
+ ---
5
+
6
+ # CLI command reference
7
+
8
+ Command-line reference for the `raft` controller tool.
9
+
10
+ Syntax:
11
+ `raft <subcommand> [options] [arguments]`
12
+
13
+ Workspaces are referenced by location-qualified handles formatted as `<location>:<instance-name>` (for example `lab:rf-a1b2c3d4e5f60718`).
14
+
15
+ ## Command index
16
+
17
+ | Task | Commands |
18
+ | --- | --- |
19
+ | Workspace lifecycle | [`new`](#new), [`stop`](#stop), [`resume`](#resume), [`extend`](#extend), [`destroy`](#destroy) |
20
+ | Execution and inspection | [`exec`](#exec), [`ssh`](#ssh), [`info`](#info), [`usage`](#usage) |
21
+ | Background job | [`status`](#status), [`logs`](#logs), [`cancel`](#cancel) |
22
+ | File transfer | [`upload`](#upload), [`download`](#download) |
23
+ | Snapshots and cloning | [`snapshot`](#snapshot), [`snapshots`](#snapshots), [`restore`](#restore), [`snapshot-delete`](#snapshot-delete), [`fork`](#fork) |
24
+ | Port forwarding and desktop | [`forward`](#forward), [`desktop`](#desktop) |
25
+ | Backup and recovery | [`backup`](#backup), [`recover`](#recover) |
26
+ | Host inspection and maintenance | [`limits`](#limits), [`doctor`](#doctor), [`list`](#list), [`gc`](#gc), [`prune`](#prune) |
27
+
28
+ ## Workspace lifecycle commands
29
+
30
+ ### `new`
31
+
32
+ Create and launch a new workspace container.
33
+
34
+ `raft new [--location LOCATION] [--ttl TTL] [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB}]`
35
+
36
+ - `--location LOCATION`: Target host location in `~/.config/raft/incus.json`. Defaults to first configured location.
37
+ - `--ttl TTL`: Lifetime in seconds before automatic stop (`60` to `2592000`). Default: `600`.
38
+ - `--cpu {1,2}`: CPU core count managed by Incus. Default: `1`.
39
+ - `--memory {1GiB,2GiB,4GiB}`: Memory limit. Default: `2GiB`.
40
+
41
+ - `--disposable`: Delete the box and snapshots on stop or expiry. Off by default.
42
+ - `--from LOCATION:BOX/SNAPSHOT`: Copy a named snapshot on its source host instead of the configured image. Normal creation sizing applies; the source can be running. Cannot target a different location.
43
+
44
+ Returns the qualified workspace handle.
45
+
46
+ ### `stop`
47
+
48
+ Stop a running workspace.
49
+
50
+ `raft stop <box>`
51
+
52
+ Terminates guest processes and stops the container. Persistent boxes retain filesystem contents, packages and snapshots. Disposable boxes are deleted, including their snapshots.
53
+
54
+ ### `resume`
55
+
56
+ Start a stopped workspace with a renewed lifetime deadline.
57
+
58
+ `raft resume <box> --ttl TTL [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB}]`
59
+
60
+ - `--ttl TTL`: Lifetime in seconds from resumption (`60` to `2592000`). Required.
61
+
62
+ ### `extend`
63
+
64
+ Reset the expiration deadline for a running workspace without restarting it.
65
+
66
+ `raft extend <box> --ttl TTL`
67
+
68
+ - `--ttl TTL`: Lifetime in seconds from the current host timestamp. Required.
69
+
70
+ ### `destroy`
71
+
72
+ Permanently delete a workspace container and its snapshots.
73
+
74
+ `raft destroy <box>`
75
+
76
+ Stops the container if running, removes all associated Btrfs snapshots, and releases host storage.
77
+
78
+ CPU/RAM overrides on resume apply before startup. Omitted values stay unchanged. A running box is rejected before changing its limits.
79
+
80
+ ## Execution and inspection commands
81
+
82
+ ### `exec`
83
+
84
+ Execute a command inside a workspace.
85
+
86
+ `raft exec <box> [--detach] [--] <command ...>`
87
+
88
+ - `--detach`: Run in background as a guest systemd unit. Returns a job ID immediately.
89
+ - `<command ...>`: Command and arguments passed to `execve`. For shell features (pipes, redirects, variables), wrap with `bash -lc '...'`.
90
+
91
+ Example:
92
+ ```sh
93
+ job=$(raft exec "$box" --detach -- make test)
94
+ ```
95
+
96
+ ### `ssh`
97
+
98
+ Open an interactive root terminal session inside the workspace over host SSH and Incus exec.
99
+
100
+ `raft ssh <box>`
101
+
102
+ ### `info`
103
+
104
+ Display container metadata from Incus in JSON format, including status, IP addresses, resource limits, and timestamps.
105
+
106
+ `raft info <box>`
107
+
108
+ ### `usage`
109
+
110
+ Display active resource consumption for a running workspace: cgroup memory, CPU microseconds, CPU affinity, and shared-pool disk space.
111
+
112
+ `raft usage <box>`
113
+
114
+ ## Background job commands
115
+
116
+ `status`, `logs` and `cancel` require a job ID returned by `exec --detach` in the selected running workspace. Unknown jobs return an error.
117
+
118
+ Cancelled jobs remain inspectable while their guest journal records exist. Rotating old logs does not prevent inspection of a live job.
119
+
120
+ ### `status`
121
+
122
+ Check the execution state of a detached background job.
123
+
124
+ `raft status <box> <job>`
125
+
126
+ Queries `systemctl show` inside the container and outputs `ActiveState`, `SubState`, and `ExecMainStatus`.
127
+
128
+ ### `logs`
129
+
130
+ Retrieve journal output from a detached background job using `journalctl --no-pager -o cat -u <job>`.
131
+
132
+ `raft logs <box> <job>`
133
+
134
+ ### `cancel`
135
+
136
+ Terminate a running detached background job using `systemctl stop <job>`.
137
+
138
+ `raft cancel <box> <job>`
139
+
140
+ ## File transfer commands
141
+
142
+ ### `upload`
143
+
144
+ Transfer a local file from the controller to an absolute path inside the workspace.
145
+
146
+ `raft upload <box> <source> <destination> [--recursive]`
147
+
148
+ - `<source>`: Path to local file on controller.
149
+ - `<destination>`: Target absolute path inside container.
150
+
151
+ ### `download`
152
+
153
+ Transfer a file from an absolute path inside the workspace to the local controller.
154
+
155
+ `raft download <box> <source> <destination> [--recursive]`
156
+
157
+ - `<source>`: Absolute path to file inside container.
158
+ - `<destination>`: Local file destination path on controller.
159
+
160
+ `--recursive` transfers a directory tree into a new destination directory. It requires a running workspace and Python 3.11.8+ in the guest. Parent directories must exist. Existing destinations are rejected, including empty directories and symlinks. Internal relative links, hardlinks, executable files and empty directories are supported; escaping links and special files are rejected. Archives use temporary controller disk space. Single-file transfers still work on stopped boxes.
161
+
162
+ ## Snapshots and cloning commands
163
+
164
+ ### `snapshot`
165
+
166
+ Create a named point-in-time snapshot of a stopped workspace. Names use alphanumeric characters, underscores, or hyphens up to 64 characters.
167
+
168
+ `raft snapshot <box> <name>`
169
+
170
+ ### `snapshots`
171
+
172
+ List existing snapshots and creation timestamps for a workspace.
173
+
174
+ `raft snapshots <box>`
175
+
176
+ ### `snapshot-delete`
177
+
178
+ Delete one named snapshot without deleting the workspace. The box can be running.
179
+
180
+ `raft snapshot-delete <box> <name>`
181
+
182
+ ### `restore`
183
+
184
+ Roll back a stopped workspace filesystem to a named snapshot using `--diskonly`, preserving container configuration and MAC address.
185
+
186
+ `raft restore <box> <name>`
187
+
188
+ ### `fork`
189
+
190
+ Clone a stopped workspace into an independent container on the same host and start it.
191
+
192
+ `raft fork <box> --ttl TTL [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB}] [--disposable]`
193
+
194
+ - `--ttl TTL`: Lifetime in seconds for the clone. Required.
195
+
196
+ Omitted CPU/RAM values inherit the source limits. The clone is persistent unless `--disposable` is supplied, regardless of the source policy.
197
+
198
+ Returns the qualified handle of the clone.
199
+
200
+ ## Port forwarding and desktop commands
201
+
202
+ ### `forward`
203
+
204
+ Open a private SSH tunnel between a guest port and controller loopback (`127.0.0.1`).
205
+
206
+ `raft forward <box> --remote REMOTE --local LOCAL`
207
+
208
+ - `--remote REMOTE`: Port number inside the workspace (bound to `0.0.0.0` or container IP).
209
+ - `--local LOCAL`: Port number on controller loopback (`127.0.0.1`).
210
+
211
+ Runs in foreground; press `Ctrl+C` to close.
212
+
213
+ ### `desktop`
214
+
215
+ Start `raft-desktop.service` inside the container and open a private tunnel for noVNC.
216
+
217
+ `raft desktop <box> [--local LOCAL]`
218
+
219
+ - `--local LOCAL`: Controller port to bind (default: `6080`). Access at `http://127.0.0.1:<LOCAL>/vnc.html`. Press `Ctrl+C` to close.
220
+
221
+ ## Backup and recovery commands
222
+
223
+ ### `backup`
224
+
225
+ Export a stopped workspace and its snapshots to a compressed archive on the controller.
226
+
227
+ `raft backup <box> <destination>`
228
+
229
+ - `<destination>`: Output archive path (e.g. `./workspace.tar.gz`). Must not exist. Created with mode `0600`.
230
+
231
+ ### `recover`
232
+
233
+ Import a portable backup archive onto a host as a fresh stopped workspace.
234
+
235
+ `raft recover <source> --location LOCATION`
236
+
237
+ - `<source>`: Path to archive file on controller.
238
+ - `--location LOCATION`: Target host location. Required.
239
+
240
+ Returns the qualified handle of the imported stopped workspace.
241
+
242
+ ## Host inspection and maintenance commands
243
+
244
+ ### `limits`
245
+
246
+ Inspect host resource capacity, active allocations, and recommended workspace limits.
247
+
248
+ `raft limits [--location LOCATION] [--json]`
249
+
250
+ - `--location LOCATION`: Filter report to a specific host.
251
+ - `--json`: Output report as JSON.
252
+
253
+ ### `doctor`
254
+
255
+ Inspect host connectivity, Incus version, active systemd units (`incus`, `raft-expire.timer`, `raft-network.service`), root filesystem usage, storage pool status, and saved box count across configured hosts.
256
+
257
+ `raft doctor`
258
+
259
+ ### `list`
260
+
261
+ List existing workspace containers across configured hosts.
262
+
263
+ `raft list [--location LOCATION]`
264
+
265
+ ### `gc`
266
+
267
+ Enforce deadlines immediately. Persistent boxes stop; disposable boxes are deleted.
268
+
269
+ `raft gc [--location LOCATION]`
270
+
271
+ ### `prune`
272
+
273
+ Preview deletion of persistent stopped boxes older than the selected number of days.
274
+
275
+ `raft prune --older-than DAYS [--location LOCATION] [--yes]`
276
+
277
+ Days must be at least one. Without `--yes`, this only prints JSONL candidates. With `--yes`, it deletes matching boxes and their snapshots under the host lock. Running boxes and boxes without a recorded stop time are excluded. Stop timestamps come from Raft stop/expiry, not box creation. No automatic retention policy is enabled.
278
+
279
+ Disposable creation/fork and prune require the current expiry worker installed through the deployer. A mismatch fails with deployment guidance; the CLI does not install or adapt an older worker.
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Configuration reference
3
+ description: Schema, fields, and security requirements for controller configuration.
4
+ ---
5
+
6
+ # Configuration reference
7
+
8
+ The Raft controller CLI reads host inventory and image mappings from `~/.config/raft/incus.json`.
9
+
10
+ ## File location and security
11
+
12
+ ```text
13
+ ~/.config/raft/incus.json
14
+ ```
15
+
16
+ Ensure the configuration file is accessible only by your user account:
17
+
18
+ ```sh title="CONTROLLER: Set strict file permissions"
19
+ chmod 600 ~/.config/raft/incus.json
20
+ ```
21
+
22
+ ## Schema format
23
+
24
+ The configuration file maps location names to host definition objects:
25
+
26
+ ```json title="Example ~/.config/raft/incus.json"
27
+ {
28
+ "lab": {
29
+ "ssh": "ubuntu@192.0.2.10",
30
+ "image": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
31
+ },
32
+ "remote-builder": {
33
+ "ssh": "builder.example.com",
34
+ "image": ""
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Location keys
40
+
41
+ - **Naming format**: Matches `^[a-zA-Z0-9][a-zA-Z0-9_-]*$`. Starts with an alphanumeric character and contains ASCII letters, digits, underscores, or hyphens.
42
+ - **Default location**: The first location listed is the default for `raft new`. `raft list` and `raft limits` accept `--location` filters; `raft doctor` and `raft gc` check all configured hosts.
43
+
44
+ ### Host entry fields
45
+
46
+ | Field | Type | Required | Description |
47
+ | :--- | :--- | :--- | :--- |
48
+ | `ssh` | `string` | Yes | Non-empty SSH connection string (IP, `user@host`, or SSH host alias from `~/.ssh/config`). |
49
+ | `image` | `string` | For `raft new` only | Full 64-character hexadecimal SHA-256 fingerprint (`^[a-f0-9]{64}$`). An empty string `""` is allowed during initial host provisioning. |
50
+
51
+ <Callout type="warn" title="Replace placeholder image fingerprint">
52
+ The `image` field requires the full 64-character hash for `raft new`; aliases such as `raft-dev` are rejected. Replace the example hash with the actual imported image fingerprint for that host.
53
+
54
+ ARM64 and AMD64 images produce different fingerprints; do not reuse an ARM64 hash on an AMD64 host.
55
+ </Callout>
56
+
57
+ ## SSH configuration and connection behavior
58
+
59
+ Primary remote commands (`raft ssh`, `upload`, `download`, and control commands) run OpenSSH with `BatchMode=yes` and `-o ConnectTimeout=10` to fail fast on unreachable hosts. The timeout applies to initial connection establishment, not session or transfer duration.
60
+
61
+ Other operations, recovery routines, and tunnels use standard SSH defaults without an explicit timeout flag.
62
+
63
+ Configure host connections in `~/.ssh/config` for batch authentication:
64
+
65
+ ```text title="~/.ssh/config"
66
+ Host lab-host
67
+ HostName 192.0.2.10
68
+ User ubuntu
69
+ IdentityFile ~/.ssh/id_ed25519
70
+ IdentitiesOnly yes
71
+ ```
72
+
73
+ Verify passwordless batch SSH before running Raft commands:
74
+
75
+ ```sh title="CONTROLLER: Verify passwordless batch connection"
76
+ ssh -o BatchMode=yes lab-host "echo SSH connection verified"
77
+ ```
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "Reference",
3
+ "collapsible": true,
4
+ "defaultOpen": false,
5
+ "pages": [
6
+ "cli",
7
+ "configuration",
8
+ "capacity-limits"
9
+ ]
10
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: Raft 0.2.0
3
+ ---
4
+
5
+ # Raft 0.2.0
6
+
7
+ Install the CLI with `npm install -g @microck/raft` on Linux with Python 3.11.8+, or use the Python wheel/source archive attached to this release. The command remains `raft`.
8
+
9
+ - Disposable boxes delete themselves on stop or expiry. Persistent boxes keep their files by default.
10
+ - `prune --older-than DAYS` previews stopped boxes eligible for deletion. Add `--yes` to apply it.
11
+ - Create boxes from named snapshots and delete snapshots directly.
12
+ - Upload and download directories, with no-overwrite publication and rejection of unsafe links, special files, incomplete streams and source-replacement races.
13
+ - Resize CPU/RAM on resume or fork. Omitted limits are preserved.
14
+ - `raft --version` reports the installed release.
15
+
16
+ Redeploy the host worker from this release before using disposable creation or retention cleanup. npm installation does not configure hosts or install Python; follow the [setup guide](https://raft.micr.dev/docs/operations).
17
+
18
+ ARM64 and AMD64 development images, manifests, package inventories and startup reports accompany this release. Verify `SHA256SUMS` before importing the archive for your host architecture.
19
+
20
+ Both architectures pass native lifecycle, desktop, development-tool, capacity, directory-transfer and full-image recovery checks. Fresh provisioning, image import, firewall ordering and reboot pass on a disposable AMD64 host.
21
+
22
+ Raft is an independent open-source alternative to boat.dev. Containers share the host kernel. Physical storage capacity, ARM64 host reboot and cross-architecture recovery are outside the verified scope.
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: System requirements
3
+ description: Hardware, operating system, network, and tool requirements for running Raft.
4
+ ---
5
+
6
+ # System requirements
7
+
8
+ Raft coordinates unprivileged Linux containers across two environments: dedicated Incus workspace hosts, and a controller running the Raft CLI.
9
+
10
+ ## Workspace host requirements
11
+
12
+ Each workspace host must be a dedicated physical machine or virtual instance:
13
+
14
+ | Component | Requirement | Notes |
15
+ | :--- | :--- | :--- |
16
+ | **Architecture** | ARM64 or AMD64 | Workspaces run native binaries matching the host CPU architecture. |
17
+ | **Operating system** | Ubuntu 22.04 LTS or 24.04 LTS | The deployer installs Incus 6.0 LTS and nftables bridge isolation rules. |
18
+ | **CPU** | At least 2 CPU cores | Capacity calculations reserve 1 CPU core for the host OS and services (an advisory estimate, not an enforced host reservation). |
19
+ | **RAM** | At least 8 GiB | Sizing calculations reserve the larger of 2 GiB or 10% of host RAM (an advisory estimate, not an enforced host reservation). |
20
+ | **Free disk** | At least 80 GiB on host disk | The deployer provisions a 60 GiB dedicated Btrfs pool (`raft-data`). |
21
+
22
+ Host CPU and RAM reserves are advisory estimates for `raft limits`, not enforced cgroup locks.
23
+
24
+ <Callout type="warn" title="Dedicated host requirement">
25
+ Do not deploy Raft on hosts with existing Incus storage pools or shared virtualization workloads. Deployment configures dedicated nftables rules, storage pools, and system services.
26
+ </Callout>
27
+
28
+ ### Host access and permissions
29
+
30
+ - **SSH access**: SSH access with passwordless `sudo` (`sudo -n true`).
31
+ - **Security model**: Single operator with root access. Deploy only on trusted hardware and networks.
32
+ - **Network connectivity**: Outbound internet for Debian packages, Incus releases, and container images. Inbound SSH from the controller. No public Incus listener or cloud account needed.
33
+
34
+ ## Controller machine requirements
35
+
36
+ The controller is your local workstation, developer machine, or CI runner:
37
+
38
+ | Component | Requirement | Notes |
39
+ | :--- | :--- | :--- |
40
+ | **Operating system** | Linux | Controller CLI commands invoke standard Linux utilities. |
41
+ | **Python** | Python 3.11.8 or newer | Required to run the `raft` CLI tool. |
42
+ | **Toolchain** | `git`, `uv`, OpenSSH client (`ssh`), `scp` | `uv` manages the Python environment; OpenSSH provides the transport. |
43
+ | **Network** | Network route to host SSH targets | Batch SSH commands must connect without interactive password prompts. |
44
+
45
+ ## Next step
46
+
47
+ Proceed to [Installation and setup](./operations.mdx) to install the CLI, configure inventory, provision Incus, and acquire a workspace image.
@@ -0,0 +1,168 @@
1
+ # Full-code review: 2026-10-06
2
+
3
+ Reviewed snapshot: `198bb1dfad60630b47a8fe40f4cd69e7d5c60162`.
4
+ Scope: the full CLI, deployment scripts, image build, verification tools, services,
5
+ workflows, packaging and agent skill. Two independent read-only reviews used
6
+ `docs/contract.md` as the specification and the applicable coding instructions as
7
+ standards. Findings below refer to this pinned snapshot, before documentation-site changes.
8
+
9
+ This is a code review, not a claim that every runtime path passed a new E2E run.
10
+ No host configuration changed during the original review.
11
+ The six distinct findings have since been fixed; see the follow-up verification below.
12
+
13
+ ## Standards
14
+
15
+ ### P1: stopped-state checks happen outside the lifecycle lock
16
+
17
+ Locations: [raft.py](../../raft.py), snapshot line 661, restore line 669,
18
+ fork line 675 in the reviewed snapshot.
19
+
20
+ Each command checks the previously fetched box status before acquiring the host
21
+ lifecycle lock. A concurrent resume can start the source before snapshot, restore
22
+ or copy runs. This violates the documented stopped-source requirement and the
23
+ correctness rule that validation and enforcement must agree.
24
+
25
+ Check the current state inside the same locked transaction as the operation.
26
+ The backup path already follows this approach. Evidence: code inspection of the
27
+ status read, branch and lock boundary; the race was not reproduced on a live host.
28
+
29
+ ### P1: repeat deployment does not apply changed firewall rules
30
+
31
+ Locations: [deploy/deploy.py](../../deploy/deploy.py), lines 145-146;
32
+ [raft-network.service](../../deploy/raft-network.service), line 9.
33
+
34
+ Deployment overwrites the firewall script, then calls `systemctl enable --now`.
35
+ The oneshot service has `RemainAfterExit=yes`. Starting an already active service
36
+ does not rerun its script, so updated rules remain unapplied while deployment
37
+ reports that the private bridge is ready. Apply and verify the installed rules
38
+ on redeployment. Evidence: code and unit inspection; no host service was restarted.
39
+
40
+ ### P1: firewall ordering does not gate guest autostart
41
+
42
+ Location: [raft-network.service](../../deploy/raft-network.service), line 3.
43
+
44
+ The firewall service runs after Incus. Incus can restore running guests before
45
+ filtering is installed. No dependency gates guest startup on the firewall.
46
+ The existing reboot checks wait for the firewall to become active before probing,
47
+ so they cannot detect exposure during startup. Install persistent filtering before
48
+ guest autostart or explicitly gate startup on it. This is a structural risk found
49
+ by inspection, not a demonstrated boot-time exploit.
50
+
51
+ ### P2: native E2E workflow misses deployment and expiry changes
52
+
53
+ Location: [images.yml](../../.github/workflows/images.yml), push path filter.
54
+
55
+ Changes to `deploy/deploy.py`, `deploy/raft-expire.py` and the expiry service/timer
56
+ do not trigger native fresh-host, reboot and TTL checks. Package checks do not
57
+ exercise those host behaviors. Include these sources in the native workflow's
58
+ trigger list. Evidence: comparison of source ownership with the workflow paths.
59
+
60
+ No separate heuristic refactor was warranted by the review.
61
+
62
+ ## Spec
63
+
64
+ ### P1: stopped-source requirement can be bypassed by concurrent resume
65
+
66
+ Locations: [raft.py](../../raft.py), snapshot, restore and fork branches.
67
+
68
+ Contract: “Snapshots, restores and forks require a stopped source for consistent disk contents.”
69
+
70
+ The stopped check is outside the lifecycle lock. A concurrent resume can invalidate
71
+ it before disk operations begin. Validate stopped state inside the locked operation.
72
+ Evidence: code inspection; no live race experiment.
73
+
74
+ ### P1: redeployment can leave the network contract unenforced
75
+
76
+ Locations: [deploy/deploy.py](../../deploy/deploy.py) and
77
+ [raft-network.service](../../deploy/raft-network.service).
78
+
79
+ Contract: “firewall rules reject guest access to host services, cloud metadata, private address ranges.”
80
+
81
+ `enable --now` does not reapply changed firewall rules when the persistent oneshot
82
+ is already active. A redeployed host can retain obsolete filtering. Reapply and
83
+ verify rules before reporting success. Evidence: code and service inspection.
84
+
85
+ ### P2: clean-image inspection misses non-root SSH credentials
86
+
87
+ Location: [verify-image.py](../../deploy/verify-image.py), credential check near line 35.
88
+
89
+ Contract: “The development image omits ... account credentials.”
90
+
91
+ The inspector rejects `rootfs/root/.ssh/` but accepts a private-key path such as
92
+ a private key beneath a non-root user’s `.ssh` directory. It also skips nonregular archive entries
93
+ before inspecting credential paths. A real temporary tar fixture containing the
94
+ non-root private-key path passed verification and printed `Clean image verified`.
95
+ This proves a gap in the inspector, not that a published image contains credentials.
96
+ Inspect SSH credential paths across user homes and relevant archive entry types.
97
+
98
+ ### P2: an oversized existing owned pool passes deployment validation
99
+
100
+ Location: [deploy/deploy.py](../../deploy/deploy.py), pool validation near line 108.
101
+
102
+ Contract: “Btrfs storage is bounded to a 60 GiB pool per host.”
103
+
104
+ Validation checks the Btrfs driver but not the configured pool size. An existing
105
+ owned pool configured above 60 GiB passes the shown checks. Reject contract drift
106
+ with explicit recovery instructions rather than silently altering storage.
107
+ Evidence: code inspection, not a destructive live pool-resize test.
108
+
109
+ ## Counts
110
+
111
+ Standards: four findings, with P1 lifecycle consistency and firewall enforcement
112
+ as the highest severity. Spec: four findings, with P1 lifecycle consistency and
113
+ firewall enforcement as the highest severity. The two axes intentionally retain
114
+ overlapping findings rather than combining their counts.
115
+
116
+ ## Documentation-site follow-up
117
+
118
+ The new site received separate standards and spec follow-up reviews. Three site
119
+ issues were corrected before delivery: bare Markdown links opening raw text,
120
+ a repository workflow link resolving to an absent local route, and missing
121
+ `Vary: Accept` on HTML responses. Canonical document links now begin with `./`,
122
+ source-file links use GitHub, and both negotiated representations set the header.
123
+
124
+ Verification: production build, strict TypeScript and Oxlint passed. A real
125
+ production-server test checked nine pages, 16 local link targets, search results,
126
+ Markdown exports, JSON downloads, the root redirect and unknown-page 404s.
127
+ Browser checks covered desktop and 390-pixel mobile layouts, sidebar navigation,
128
+ Ctrl+K search and result navigation. Computed body colors were black and white;
129
+ each page had one H1 and browser error inspection was empty. Wide parity tables
130
+ scroll within their container rather than widening the mobile page.
131
+
132
+ The simplification pass ran in the parent context across reuse, quality and
133
+ efficiency. No behavior-preserving refactor was warranted. No runtime review
134
+ finding above was fixed at that stage, and site checks did not modify
135
+ workspace hosts.
136
+
137
+ ## Runtime-fix follow-up
138
+
139
+ All six distinct findings are addressed by the follow-up patch. Both review axes
140
+ reported no additional actionable findings after inspecting the fixes and tests.
141
+
142
+ | Finding | Fix | Verification |
143
+ | --- | --- | --- |
144
+ | Stopped-source race | Ownership and state are checked inside the same locked transaction as snapshot, restore, fork and backup | Real queued snapshot/restore/fork operations reject a source started first under the host lock on two ARM64 hosts |
145
+ | Stale rules on redeploy | Reloadable firewall oneshot explicitly reapplies owned chains transactionally, preserving unrelated chains and the running Incus daemon | Isolated first/repeat application passed; fresh-host CI checks injected stale-rule removal, unrelated-chain retention and unchanged daemon start timestamp |
146
+ | Guest autostart before filtering | Incus requires the firewall and starts after successful installation, including socket activation | Installed dependency verified on two hosts; fresh-host CI tests failed filtering and post-reboot start timestamps |
147
+ | Missing native CI triggers | All `deploy/**` changes trigger the native workflow | Workflow path inspection; the fix push triggers both native architectures and the fresh-host job |
148
+ | Non-root image credentials | Check credential paths and link targets before skipping nonregular entries | 15 real tar cases passed, including generated non-root private keys, symlinks, hardlinks, FIFO entries and clean controls |
149
+ | Oversized owned storage | Reject an existing pool whose configured size differs from `60GiB`, with recovery guidance and no resize | Existing 60 GiB pools pass; fresh-host CI grows a disposable pool to 61 GiB and verifies rejection without resizing |
150
+
151
+ The extended lifecycle suite passed on two configured ARM64 hosts. Each completed
152
+ all 31 developer-tool checks, six sizing combinations, the deterministic race
153
+ cases, Docker, files, jobs, snapshots, forks, restore, private tunnels, desktop,
154
+ network restrictions and scheduled TTL stop/resume. Each removed its fixtures.
155
+ A cross-host control run also passed portable backup/recovery, snapshot rollback,
156
+ permissions, cancellation and usage checks with a 385,265,470-byte archive.
157
+ Both hosts had zero remaining test boxes afterward. Lint, formatting, compilation
158
+ and release archive/privacy checks passed.
159
+
160
+ The [fix verification run](https://github.com/Microck/raft/actions/runs/37496577945)
161
+ passed native ARM64 and AMD64 builds, extended lifecycle with daemon restart,
162
+ capacity transitions, full development-image backup/recovery, startup measurements
163
+ and clean image inspection for commit `27b08c143dc1837d9cfb95f3c3a784318879b284`.
164
+ The disposable AMD64 KVM host passed stale-rule removal without an Incus restart,
165
+ failed-firewall rejection for normal and socket startup, real host reboot with
166
+ filtering before Incus, and rejection of a 61 GiB pool without resizing it.
167
+ ARM64 host reboot remains unverified. Package and documentation-site workflows
168
+ also passed. No published release assets or configured image fingerprints changed.