@microck/raft 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -76,6 +76,11 @@ see the task guides for [background jobs](docs/guides/background-jobs.mdx),
76
76
  [backup and recovery](docs/guides/backup-recovery.mdx)
77
77
  and [Docker](docs/guides/docker.mdx).
78
78
 
79
+ See [project setup and templates](docs/guides/project-setup.mdx),
80
+ [persistent services](docs/guides/long-running-services.mdx),
81
+ [scripting and CI](docs/guides/scripting-ci.mdx), and
82
+ [upgrades](docs/guides/upgrades.mdx) for complete operator workflows.
83
+
79
84
  stop the source box before taking a snapshot, restoring one or making a fork.
80
85
  stopping a box ends its jobs without saving process memory.
81
86
 
@@ -157,6 +157,62 @@ def verify_archives():
157
157
  )
158
158
 
159
159
 
160
+ def verify_readiness(location, expect_stale=False):
161
+ # Isolate the selected host without losing the controller's verified SSH config.
162
+ with tempfile.TemporaryDirectory(prefix="raft-doctor-test-") as work:
163
+ home = Path(work)
164
+ (home / ".ssh").symlink_to(Path.home() / ".ssh", target_is_directory=True)
165
+ config = home / ".config/raft/incus.json"
166
+ config.parent.mkdir(parents=True)
167
+ config.write_text(json.dumps({location: settings(location)}))
168
+ response = live.raft("doctor", check=False, env={**os.environ, "HOME": str(home)})
169
+ assert location + ": " in response.stdout, response
170
+ if expect_stale:
171
+ assert response.returncode == 1, response
172
+ assert (
173
+ f"raft: {location}: Host expiry worker differs from this CLI; "
174
+ "deploy the current Raft checkout first"
175
+ ) in response.stderr, response
176
+ else:
177
+ assert response.returncode == 0, response
178
+ assert "saved boxes; system containers (shared host kernel)" in response.stdout, response
179
+ print(f"{location}: doctor {'rejected stale worker' if expect_stale else 'passed'}", flush=True)
180
+
181
+
182
+ def verify_transaction_stdin(location):
183
+ program = """import sys, tempfile
184
+ from raft import transaction
185
+ assert transaction(sys.argv[1], 'cat') == ''
186
+ with tempfile.TemporaryFile() as output:
187
+ transaction(sys.argv[1], 'cat', stdout=output)
188
+ output.seek(0)
189
+ assert output.read() == b''
190
+ """
191
+ with subprocess.Popen(
192
+ [sys.executable, "-c", program, location],
193
+ cwd=ROOT,
194
+ stdin=subprocess.PIPE,
195
+ stdout=subprocess.PIPE,
196
+ stderr=subprocess.PIPE,
197
+ ) as process:
198
+ try:
199
+ process.stdin.write(b"must-not-reach-the-host\n")
200
+ process.stdin.flush()
201
+ process.wait(timeout=30)
202
+ finally:
203
+ # Release even a regressed remote reader before leaving this test.
204
+ process.stdin.close()
205
+ try:
206
+ process.wait(timeout=30)
207
+ except subprocess.TimeoutExpired:
208
+ process.kill()
209
+ process.wait()
210
+ raise
211
+ assert process.returncode == 0, process.stderr.read().decode()
212
+ assert transaction(location, "printf lock-released") == "lock-released"
213
+ print(f"{location}: open stdin ignored and lifecycle lock released", flush=True)
214
+
215
+
160
216
  def verify(location, isolated_worker=False):
161
217
  if inventory(location):
162
218
  raise RuntimeError(
@@ -236,6 +292,8 @@ def verify(location, isolated_worker=False):
236
292
  return any(box["name"] == parse_handle(handle)[1] for box in inventory(location))
237
293
 
238
294
  try:
295
+ verify_readiness(location, expect_stale=isolated_worker)
296
+ verify_transaction_stdin(location)
239
297
  box = create("--memory", "1GiB")
240
298
  live.wait_running(box)
241
299
  _, name = parse_handle(box)
package/docs/contract.md CHANGED
@@ -35,7 +35,7 @@ Raft manages persistent unprivileged native-architecture Incus system containers
35
35
  | 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 GiB RAM, and 60 to 2,592,000 seconds (30 days) TTL. |
36
36
  | Admission limit | Hard ceiling of four saved boxes per host, including stopped boxes. |
37
37
  | TTL calculation | Running TTL begins after launch completes, using the host clock. Cold image preparation does not consume TTL. |
38
- | Host locking | Creation, lifecycle transitions, and expiry are serialized by host file lock (`/run/lock/raft-incus.lock`). Native Incus admin calls bypass CLI policy. |
38
+ | 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. |
39
39
 
40
40
  ## Capacity and admission inspection
41
41
 
@@ -51,7 +51,7 @@ Raft manages persistent unprivileged native-architecture Incus system containers
51
51
 
52
52
  - **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.
53
53
  - **Lifetime extension**: `raft extend --ttl` resets a running box's deadline from current host time without restarting; stopped boxes require `raft resume`.
54
- - **Host worker contract**: Disposable creation/fork and retention cleanup require the current worker protocol. A mismatch fails before executing deletion policy or creating a disposable box; deploy the current checkout explicitly.
54
+ - **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.
55
55
  - **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`.
56
56
  - **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.
57
57
  - **Resizing**: Resume and fork accept optional supported CPU/RAM overrides, preserving omitted values. Resume validates stopped state under the lock before changing sizing.
@@ -71,6 +71,9 @@ raft destroy "$box"
71
71
 
72
72
  ## Next steps
73
73
 
74
+ - [Project setup and templates](./guides/project-setup.mdx): Prepare a project and reuse its dependencies.
75
+ - [Lifetime and persistent services](./guides/long-running-services.mdx): Manage deadlines and restart applications after resume.
76
+ - [Scripting and CI](./guides/scripting-ci.mdx): Parse CLI output and handle failures.
74
77
  - [Docker in workspaces](./guides/docker.mdx): Run nested containers and build images.
75
78
  - [File transfer](./guides/files.mdx): Upload and download files over SSH.
76
79
  - [Background jobs](./guides/background-jobs.mdx): Run detached commands under systemd.
@@ -78,5 +81,6 @@ raft destroy "$box"
78
81
  - [Snapshots and forks](./guides/snapshots-forks.mdx): Capture disk states and create same-host clones.
79
82
  - [Backup and recovery](./guides/backup-recovery.mdx): Export portable archives and restore across hosts.
80
83
  - [Troubleshooting](./guides/troubleshooting.mdx): Diagnose common errors and inspect host services.
84
+ - [Upgrades and image changes](./guides/upgrades.mdx): Update the controller, hosts, and image pins.
81
85
 
82
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.
@@ -90,3 +90,6 @@ raft extend "$box" --ttl 1800
90
90
  ```
91
91
 
92
92
  `raft extend` resets the container expiration deadline to `now + TTL` seconds using the host clock. It does not add time to the existing deadline.
93
+
94
+ For deadline inspection and an enabled systemd service that starts again after
95
+ resume or fork, see [Lifetime and persistent services](./long-running-services.mdx).
@@ -66,7 +66,10 @@ Start the desktop and establish a private tunnel:
66
66
  raft desktop "$box" --local 6080
67
67
  ```
68
68
 
69
- `raft desktop` starts `raft-desktop.service` inside the container if needed, waits for Xvfb and VNC readiness, and opens a tunnel to local port `6080`.
69
+ `raft desktop` requests startup of `raft-desktop.service` inside the container if
70
+ needed and opens a tunnel to local port `6080`. The service waits for Xvfb before
71
+ launching its desktop clients. The CLI does not poll noVNC readiness; if the first
72
+ browser request fails during startup, retry after the service initializes.
70
73
 
71
74
  Open the client in your browser:
72
75
 
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: Lifetime and persistent services
3
+ description: Inspect deadlines, manage disposable and persistent boxes, and restart applications after resume.
4
+ ---
5
+
6
+ # Lifetime and persistent services
7
+
8
+ A box's lifetime controls when the host stops or deletes it. Activity, an open
9
+ terminal, and detached jobs do not extend that deadline automatically.
10
+
11
+ ## Choose a lifetime and retention policy
12
+
13
+ ```sh title="CONTROLLER: Create a persistent box"
14
+ box=$(raft new --location lab --ttl 3600)
15
+ ```
16
+
17
+ TTL is in seconds: 60 to 2,592,000 (30 days), with a creation default of 600.
18
+ The host sets the deadline after launch completes, using its own clock.
19
+ Raft has no option to disable automatic expiry.
20
+
21
+ | Operation | Persistent box (default) | Disposable box |
22
+ | --- | --- | --- |
23
+ | `stop` or TTL expiry | Stops processes; keeps disk and snapshots | Deletes the box and snapshots |
24
+ | `resume --ttl SECONDS` | Starts the saved box with a new deadline | Only possible while the box still exists |
25
+ | `destroy` | Deletes the box and snapshots | Deletes the box and snapshots |
26
+
27
+ Use `raft new --disposable` for temporary work. A fork is persistent unless
28
+ `--disposable` is explicitly supplied; recovered boxes are persistent.
29
+ Back up work before stopping a disposable box.
30
+
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.
34
+
35
+ ## Read and reset the deadline
36
+
37
+ Select a running handle from `raft list`. These examples use controller `jq`:
38
+
39
+ ```sh title="CONTROLLER: Inspect and extend a running box"
40
+ raft list
41
+ box="lab:rf-a1b2c3d4e5f60718" # Replace with your actual handle
42
+ raft info "$box" | jq '{status, expires: .config["user.raft.expires"]}'
43
+ raft extend "$box" --ttl 7200
44
+ raft info "$box" | jq '{status, expires: .config["user.raft.expires"]}'
45
+ ```
46
+
47
+ `user.raft.expires` is a Unix timestamp encoded as a string, not a remaining
48
+ duration. `raft list` exposes it as `stopAt`; it may be empty on stopped boxes.
49
+ `extend` sets `now + TTL`, rather than adding time to the old deadline.
50
+ It requires a running box. Start a stopped box with `raft resume "$box" --ttl 7200`.
51
+
52
+ The host timer checks every 15 seconds. Graceful shutdown, a host outage, or a
53
+ long transaction holding the lifecycle lock can delay expiry. `raft gc --location lab`
54
+ runs an immediate deadline check. A deadline is not a guarantee that a job will
55
+ finish before shutdown; extend before starting work that needs more time.
56
+
57
+ ## Restart a service after resume
58
+
59
+ One-off detached commands do not survive a stop. To start an application again
60
+ after resume or fork, save a systemd unit on disk and enable it. This starts a new
61
+ process; it does not recover process memory or an interrupted request.
62
+
63
+ The following example serves `/workspace` as the guest `developer` user. Use a
64
+ persistent running box and ensure files the application needs are readable by
65
+ that user. For an actual application, replace `ExecStart` and the working directory.
66
+
67
+ ```sh title="CONTROLLER: Install and enable an example service"
68
+ raft exec "$box" -- bash -lc 'cat > /etc/systemd/system/workspace-web.service <<"UNIT"
69
+ [Unit]
70
+ Description=Workspace HTTP example
71
+ After=network.target
72
+
73
+ [Service]
74
+ User=developer
75
+ WorkingDirectory=/workspace
76
+ ExecStart=/usr/bin/python3 -m http.server 8080 --bind 0.0.0.0
77
+ Restart=on-failure
78
+ RestartSec=2
79
+
80
+ [Install]
81
+ WantedBy=multi-user.target
82
+ UNIT
83
+ systemctl daemon-reload
84
+ systemctl enable --now workspace-web.service'
85
+ raft exec "$box" -- systemctl status workspace-web.service --no-pager
86
+ raft exec "$box" -- journalctl -u workspace-web.service -n 30 --no-pager
87
+ ```
88
+
89
+ Open `raft forward "$box" --remote 8080 --local 8080` and visit
90
+ `http://127.0.0.1:8080`. The tunnel runs in the foreground; close it with `Ctrl+C`.
91
+
92
+ ```sh title="CONTROLLER: Check service startup after resume"
93
+ raft stop "$box"
94
+ raft resume "$box" --ttl 3600
95
+ raft exec "$box" -- systemctl is-active workspace-web.service
96
+ ```
97
+
98
+ Reopen the tunnel after resume. A fork or snapshot template copies the enabled
99
+ unit too, so its new box starts its own service. Application secrets on disk are
100
+ also copied; see [Project setup and templates](./project-setup.mdx).
101
+
102
+ To disable the example, run:
103
+
104
+ ```sh
105
+ raft exec "$box" -- systemctl disable --now workspace-web.service
106
+ ```
107
+
108
+ See [Background jobs](./background-jobs.mdx) for one-off tasks and
109
+ [Desktop and private services](./desktop-services.mdx) for tunnel behavior.
@@ -3,12 +3,16 @@
3
3
  "collapsible": true,
4
4
  "defaultOpen": false,
5
5
  "pages": [
6
+ "project-setup",
6
7
  "docker",
7
8
  "files",
8
9
  "background-jobs",
10
+ "long-running-services",
11
+ "scripting-ci",
9
12
  "desktop-services",
10
13
  "snapshots-forks",
11
14
  "backup-recovery",
15
+ "upgrades",
12
16
  "troubleshooting"
13
17
  ]
14
18
  }
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: Project setup and templates
3
+ description: Upload or clone a project, run setup scripts, and create reusable same-host snapshot templates.
4
+ ---
5
+
6
+ # Project setup and templates
7
+
8
+ Raft starts a clean development image. Prepare a project with ordinary guest
9
+ commands, then save a snapshot to reuse installed dependencies. There is no
10
+ managed repository registry, environment registry, setup hook, or secret-injection
11
+ service. Commands run as guest root in `/workspace` unless you choose another user.
12
+
13
+ ## Put a project in the box
14
+
15
+ Create a persistent box after [host setup](../operations.mdx):
16
+
17
+ ```sh title="CONTROLLER: Create a project box"
18
+ box=$(raft new --location lab --ttl 3600)
19
+ ```
20
+
21
+ Upload a local project, excluding credentials and files you do not want copied.
22
+ The example assumes `./my-project` contains your project and `setup.sh`:
23
+
24
+ ```sh title="CONTROLLER: Upload local project"
25
+ raft upload "$box" ./my-project /workspace/my-project --recursive
26
+ ```
27
+
28
+ The destination directory must not exist. For a public repository, an alternative
29
+ is guest `git clone`; replace the example URL with your repository:
30
+
31
+ ```sh title="CONTROLLER: Clone instead of uploading"
32
+ raft exec "$box" -- git clone https://github.com/OWNER/REPOSITORY.git /workspace/my-project
33
+ ```
34
+
35
+ Choose one method. Private repositories require authentication configured inside
36
+ the guest; Raft does not forward your controller's GitHub login or SSH agent.
37
+ The clean image does not bundle coding agents or their credentials. Install and
38
+ authenticate any agent inside the box using that tool's own instructions.
39
+
40
+ ## Run setup explicitly
41
+
42
+ Use `bash -lc` for directory changes or shell syntax:
43
+
44
+ ```sh title="CONTROLLER: Run project setup"
45
+ raft exec "$box" -- bash -lc 'cd /workspace/my-project && bash ./setup.sh'
46
+ ```
47
+
48
+ Make setup scripts safe to rerun. Raft does not execute them automatically on
49
+ create, resume, or fork. A failing synchronous command returns its exit code;
50
+ see [Scripting and CI](./scripting-ci.mdx).
51
+
52
+ For slow setup, use a detached job and inspect its result:
53
+
54
+ ```sh title="CONTROLLER: Run slow setup instead of synchronous setup"
55
+ job=$(raft exec "$box" --detach -- bash -lc 'cd /workspace/my-project && bash ./setup.sh')
56
+ raft status "$box" "$job"
57
+ raft logs "$box" "$job"
58
+ ```
59
+
60
+ Wait for successful completion before making a template. Extending the lifetime
61
+ may be necessary; a detached job does not prevent expiry.
62
+
63
+ ## Supply application configuration
64
+
65
+ For an application that reads a local `.env` file, upload it explicitly and set
66
+ permissions for the application's user. This example assumes a controller file
67
+ `./app.env` and an application running as `developer`:
68
+
69
+ ```sh title="CONTROLLER: Install a private application configuration file"
70
+ raft upload "$box" ./app.env /workspace/my-project/.env
71
+ raft exec "$box" -- chown developer:developer /workspace/my-project/.env
72
+ raft exec "$box" -- chmod 600 /workspace/my-project/.env
73
+ ```
74
+
75
+ The application must load that file; upload does not export environment variables.
76
+ If running the whole project as `developer`, adjust project ownership separately.
77
+ Do not place secrets in command arguments, committed files, or logs. Host journals
78
+ can capture guest command lines. Raft does not rotate or scrub credentials.
79
+
80
+ ## Save and reuse a prepared template
81
+
82
+ Finish setup, remove credentials that should not be inherited, and stop the box:
83
+
84
+ ```sh title="CONTROLLER: Save the prepared filesystem"
85
+ raft stop "$box"
86
+ raft snapshot "$box" prepared
87
+ ```
88
+
89
+ Create another box from the snapshot:
90
+
91
+ ```sh title="CONTROLLER: Start a copy with explicit resources"
92
+ copy=$(raft new --from "$box/prepared" --ttl 3600 --cpu 2 --memory 4GiB)
93
+ raft exec "$copy" -- ls -la /workspace/my-project
94
+ ```
95
+
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.
100
+
101
+ The snapshot includes packages, project files, credentials, and enabled services.
102
+ It does not include process memory. Changing the original after snapshot creation
103
+ does not update the saved template. Keep its source box and snapshot until you
104
+ no longer need to create copies; destroying the source deletes its snapshots.
105
+
106
+ To copy the stopped box's current filesystem instead, use
107
+ `raft fork "$box" --ttl 3600`. Fork inherits omitted CPU/RAM limits and does not
108
+ copy the source snapshots. See [Snapshots and forks](./snapshots-forks.mdx).
109
+
110
+ For a different host, use [Backup and recovery](./backup-recovery.mdx) with matching
111
+ CPU architecture. For services that start on resume, see
112
+ [Lifetime and persistent services](./long-running-services.mdx).
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: Scripting and CI
3
+ description: Parse CLI output, preserve command exit codes, wait for readiness, and handle interrupted operations.
4
+ ---
5
+
6
+ # Scripting and CI
7
+
8
+ Raft is a controller CLI using host SSH. A Linux CI runner needs Python 3.11.8+,
9
+ OpenSSH, the installed CLI, host inventory, verified SSH host keys, batch
10
+ authentication, and hosts with passwordless sudo. No Raft HTTP API, SDK, or API key
11
+ exists. Give access only to trusted jobs: the configured SSH account has host sudo
12
+ and controls all boxes on that host.
13
+
14
+ ## Output formats
15
+
16
+ Raft does not have a global `--json` option. Parse each command's actual format:
17
+
18
+ | Command | stdout |
19
+ | --- | --- |
20
+ | `new`, `fork`, `recover` | One location-qualified handle |
21
+ | `exec --detach` | One `rfcmd-` job ID |
22
+ | `list` | JSONL: one object per box, with `box`, `state`, `stopAt` |
23
+ | `info` | One pretty-printed native Incus metadata JSON object |
24
+ | `usage` | One JSON object containing running resource counters |
25
+ | `limits --json` | One JSON object per location (JSONL across multiple hosts) |
26
+ | `prune` | JSONL candidates or deleted records with `box`, `action` |
27
+ | `status` | systemd `ActiveState`, `SubState`, `ExecMainStatus` property lines |
28
+ | `logs` | Guest journal text |
29
+ | `exec` | Payload stdout/stderr |
30
+ | Other inspection/maintenance commands | Human-readable text or no output |
31
+
32
+ `list` emits nothing when there are no boxes. `stopAt` is the host Unix deadline
33
+ as a string, or an empty string when absent. `info` uses native Incus keys rather
34
+ than a normalized Raft schema. `usage` includes `memoryBytes`, `memoryLimit`,
35
+ `cpuTime`, `effectiveCpus`, `filesystemUsedBytes`, `filesystemAvailableBytes`, and
36
+ `filesystemScope`; disk values describe the shared pool, not an individual box.
37
+ See [Capacity limits](../reference/capacity-limits.mdx) for capacity calculations.
38
+
39
+ These examples require `jq` on the controller:
40
+
41
+ ```sh title="CONTROLLER: Parse records without mixing stderr"
42
+ raft list --location lab | jq -r 'select(.state == "Running") | .box'
43
+ raft limits --location lab --json | jq '{location, savedSlots, warnings}'
44
+ raft prune --location lab --older-than 7 | jq -r '.box'
45
+ ```
46
+
47
+ Do not redirect `2>&1` into machine-readable output. Recovery reports its allocated
48
+ handle on stderr before import finishes; only a successful completion prints the
49
+ final handle on stdout.
50
+
51
+ ## Exit codes and job results
52
+
53
+ | Situation | Exit code |
54
+ | --- | --- |
55
+ | Successful control operation | `0` |
56
+ | Synchronous `exec` | Transport/command return code; preserves payload exit status when transport succeeds |
57
+ | Recognized runtime/configuration failure | `1`, with `raft: ...` on stderr |
58
+ | Argument parsing failure | `2`, with argparse diagnostics on stderr |
59
+ | Caught keyboard interruption | `130` |
60
+
61
+ SSH transport failures may return SSH's status rather than the payload's result.
62
+ The foreground tunnel replaces the CLI process with SSH and uses SSH's exit code.
63
+ A zero exit from `status` means the query succeeded; read `ExecMainStatus` for the
64
+ job result and `ActiveState`/`SubState` for whether it finished. Do not interpret a
65
+ running job's initial `ExecMainStatus=0` as completion. See
66
+ [Background jobs](./background-jobs.mdx).
67
+
68
+ ## Create, wait, run, and stop
69
+
70
+ `new` returns after Incus starts the box, before every guest service is necessarily
71
+ ready. Probe a harmless command before running setup. This Bash example waits up
72
+ to about a minute plus transport time, executes an uploaded project check, and
73
+ attempts to stop the persistent box on exit. It assumes `./my-project/check.sh`
74
+ exists and the configured host has a free saved slot:
75
+
76
+ ```bash title="CONTROLLER or trusted Linux CI runner"
77
+ set -euo pipefail
78
+ box=""
79
+ cleanup() {
80
+ result=$?
81
+ trap - EXIT
82
+ if [ -n "$box" ]; then
83
+ if ! raft stop "$box"; then
84
+ echo "Cleanup failed; inspect $box" >&2
85
+ [ "$result" -ne 0 ] || result=1
86
+ fi
87
+ fi
88
+ exit "$result"
89
+ }
90
+ trap cleanup EXIT
91
+
92
+ box=$(raft new --location lab --ttl 3600)
93
+ printf 'Created %s\n' "$box" >&2
94
+ ready=false
95
+ for attempt in {1..30}; do
96
+ if raft exec "$box" -- true; then
97
+ ready=true
98
+ break
99
+ fi
100
+ sleep 2
101
+ done
102
+ if [ "$ready" != true ]; then
103
+ echo "Guest did not become command-ready: $box" >&2
104
+ exit 1
105
+ fi
106
+
107
+ raft upload "$box" ./my-project /workspace/my-project --recursive
108
+ raft exec "$box" -- bash -lc 'cd /workspace/my-project && bash ./check.sh'
109
+ ```
110
+
111
+ The stopped box remains available for inspection and still consumes a saved slot.
112
+ Destroy it after collecting needed files, or preview old stopped boxes with prune.
113
+ For disposable CI boxes, deletion on stop/expiry removes artifacts and snapshots;
114
+ download results before cleanup. A new box or a retry still consumes a saved slot.
115
+
116
+ Command readiness does not mean Docker or a web application is ready. Probe the
117
+ specific dependency (`docker info`, an HTTP health endpoint, or systemd status)
118
+ with its own bounded wait. Extend the deadline before work exceeding the current
119
+ lifetime. This example's retries are only for the harmless readiness probe.
120
+
121
+ ## Interrupted operations and retries
122
+
123
+ Raft does not automatically retry commands or provide idempotency keys. If SSH
124
+ drops during creation, resume, import, or execution, the remote operation may have
125
+ already happened. Inspect `raft list`, `raft info`, guest logs, and the host before
126
+ repeating an operation that creates boxes or changes data. A failed create can
127
+ leave a saved box even when no handle reached stdout.
128
+
129
+ For an interrupted recovery, inspect the handle printed on stderr and follow
130
+ [Host crash during recovery](./troubleshooting.mdx#host-crash-during-recovery).
131
+ For uncertain command execution, check its effects before rerunning it; use
132
+ idempotent setup scripts where possible. Do not treat a lost connection as proof
133
+ of deletion or of a command not having run.
@@ -19,15 +19,36 @@ raft doctor
19
19
 
20
20
  `raft doctor` connects to each host over SSH and reports:
21
21
  - Incus version running in project `raft`
22
+ - Compatibility of the installed expiry worker with the CLI
22
23
  - Active status of host systemd services (`incus`, `raft-expire.timer`, `raft-network.service`)
23
24
  - Host root filesystem disk usage (`df -h /`)
24
25
  - Status and usage of the `raft-data` Btrfs pool
25
26
  - Count of existing saved containers
26
27
 
27
28
  <Callout type="info" title="Doctor scope">
28
- `raft doctor` reports runtime daemon and storage status. It does not validate image fingerprints, inspect nftables bridge rules, enforce admission caps, or emit 5 GiB low-disk warnings.
29
+ `raft doctor` reports runtime daemon, expiry-worker compatibility, and storage status. It does not validate image fingerprints, inspect nftables bridge rules, enforce admission caps, or emit 5 GiB low-disk warnings.
29
30
  </Callout>
30
31
 
32
+ ### Expiry worker differs from the CLI
33
+
34
+ An active expiration timer can still run an older worker. Installing or updating
35
+ the controller CLI does not deploy host code. `doctor` checks the same worker
36
+ protocol as disposable creation and retention cleanup, without running expiry or
37
+ changing any boxes. It exits nonzero and names the location on a mismatch.
38
+
39
+ Review the host deployment changes, then deploy from the checkout matching your
40
+ CLI. Replace `lab` with the location reported by the diagnostic:
41
+
42
+ ```sh title="CONTROLLER: Explicitly update a reviewed host deployment"
43
+ python3 deploy/deploy.py --location lab
44
+ raft doctor
45
+ ```
46
+
47
+ Deployment updates host infrastructure as well as the expiry worker. Do not run
48
+ it on a shared host without authorization. Do not remove the compatibility check
49
+ or mark an old worker as current; older workers may retain expired disposable
50
+ boxes instead of deleting them.
51
+
31
52
  ### Listing active and stopped containers
32
53
 
33
54
  Inspect workspace states across locations:
@@ -74,13 +95,17 @@ Host system journals can capture command lines, arguments, and environment varia
74
95
 
75
96
  ### Manually triggering garbage collection
76
97
 
77
- The host timer stops expired containers every 15 seconds. Trigger an immediate expiration run from the controller:
98
+ The host timer enforces expiry every 15 seconds. Trigger an immediate expiration run from the controller:
78
99
 
79
100
  ```sh title="CONTROLLER: Run expiration workers manually"
80
101
  raft gc
81
102
  ```
82
103
 
83
- Expired containers transition from `Running` to `Stopped`. Expiration never deletes container files or snapshots.
104
+ Expired persistent containers transition from `Running` to `Stopped` and retain
105
+ files and snapshots. Expired disposable containers are deleted, including their
106
+ snapshots. Use `raft gc --location lab` to check only one host. The timer normally
107
+ checks every 15 seconds; host outages, graceful shutdown and long locked
108
+ operations can delay expiry. See [Lifetime and persistent services](./long-running-services.mdx).
84
109
 
85
110
  ## Common error scenarios
86
111
 
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: Upgrades and image changes
3
+ description: Update the controller CLI and host worker, replace image fingerprints, and understand existing-box behavior.
4
+ ---
5
+
6
+ # Upgrades and image changes
7
+
8
+ The controller CLI, host deployment scripts, and workspace image are separate
9
+ parts of Raft. Updating one does not automatically update the others.
10
+
11
+ | Component | Update method | Effect on existing boxes |
12
+ | --- | --- | --- |
13
+ | Controller CLI | Install the chosen npm or Python release | Changes controller commands; does not rewrite guest files |
14
+ | Host worker and firewall | Run the chosen release's deployer per host | Replaces host policy code and reapplies filtering |
15
+ | Pinned development image | Import an image and change the configuration fingerprint | Used for future image-based `new`; existing boxes keep their files |
16
+
17
+ ## Choose a release and prepare
18
+
19
+ Review [release notes](https://github.com/Microck/raft/releases) and use matching
20
+ CLI and deployment sources. The examples below pin `v0.2.0`; replace that version
21
+ consistently when adopting another release.
22
+
23
+ ```sh title="CONTROLLER: Inspect the current installation"
24
+ raft --version
25
+ raft doctor
26
+ raft list
27
+ ```
28
+
29
+ Save a copy of your controller inventory. Export important persistent boxes using
30
+ [Backup and recovery](./backup-recovery.mdx) before host changes. Backup requires a
31
+ stopped box. Stopping a disposable box deletes it, so copy needed files out before
32
+ stopping one. Schedule redeployment when interruptions are acceptable.
33
+
34
+ ## Update the controller
35
+
36
+ For an npm installation:
37
+
38
+ ```sh title="CONTROLLER: Install a pinned CLI release"
39
+ npm install -g @microck/raft@0.2.0
40
+ raft --version
41
+ ```
42
+
43
+ For a Python installation, download the wheel and `SHA256SUMS` from the same
44
+ release, verify the checksum, then install it with `uv`:
45
+
46
+ ```sh title="CONTROLLER: Install a verified release wheel instead of npm"
47
+ sha256sum --ignore-missing -c SHA256SUMS
48
+ uv tool install --force ./raft-0.2.0-py3-none-any.whl
49
+ raft --version
50
+ ```
51
+
52
+ Proceed only if checksum verification succeeds. Choose one installation method
53
+ and check which executable your shell uses with `command -v raft`.
54
+
55
+ For an editable source installation, updating the checkout changes the installed
56
+ tool's source. Keep uncommitted work separate and run `uv sync --locked` for the
57
+ chosen checkout. Raft has no `self-update` command.
58
+
59
+ ## Redeploy each host
60
+
61
+ Obtain a separate checkout of the matching release:
62
+
63
+ ```sh title="CONTROLLER: Obtain and deploy matching host code"
64
+ git clone --branch v0.2.0 https://github.com/Microck/raft.git raft-v0.2.0
65
+ cd raft-v0.2.0
66
+ python3 deploy/deploy.py --location lab
67
+ raft doctor
68
+ ```
69
+
70
+ Repeat for each configured location. The deployer uses your existing
71
+ `~/.config/raft/incus.json`, verifies owned project/pool/network/profile state,
72
+ replaces host scripts and units, reapplies firewall rules, and starts the expiry
73
+ timer. It does not import an image or migrate guest files.
74
+
75
+ Do not bypass a conflicting-resource or storage-size error. Inspect the host and
76
+ preserve backups before explicit recovery; redeployment does not repair arbitrary
77
+ infrastructure drift. Disposable creation/fork and prune check the worker protocol
78
+ and fail if it differs from the CLI. That check is not an automatic upgrade.
79
+
80
+ ## Replace the image pin
81
+
82
+ Follow [Acquire a native workspace image](../operations.mdx#acquire-a-native-workspace-image)
83
+ to download and verify or build the image for each host's architecture. Retrieve
84
+ its full fingerprint and replace only that location's `image` field in the
85
+ controller inventory. Keep the previous fingerprint if you may need it again.
86
+
87
+ Changing the pin affects ordinary `raft new` calls. Resume retains the existing
88
+ filesystem; forks and `new --from` use their source filesystem, not the new image.
89
+ Install guest updates explicitly inside an existing box, or create a new box and
90
+ transfer the project and needed data. Recovering an old backup also restores that
91
+ backup's guest filesystem rather than rebuilding it from the new image.
92
+
93
+ ## Verify the update
94
+
95
+ ```sh title="CONTROLLER: Smoke-check a new persistent box"
96
+ box=$(raft new --location lab --ttl 600)
97
+ raft exec "$box" -- node --version
98
+ raft exec "$box" -- docker info
99
+ raft stop "$box"
100
+ raft resume "$box" --ttl 600
101
+ raft exec "$box" -- ls /workspace
102
+ raft destroy "$box"
103
+ ```
104
+
105
+ Creation can return before guest services are ready. Use the readiness pattern in
106
+ [Scripting and CI](./scripting-ci.mdx) if an immediate check fails during startup.
107
+ For dedicated test infrastructure, [Verification](../verification.mdx) describes
108
+ the real-host suites and their effects.
109
+
110
+ To revert an image choice for future boxes, restore the previous fingerprint
111
+ while that image remains imported. This does not revert existing boxes. Raft has
112
+ no automatic migration or rollback of host policy or guest packages; review the
113
+ chosen release and backups before downgrading those components.
package/docs/index.mdx CHANGED
@@ -27,10 +27,14 @@ Stopping a workspace preserves files and packages on disk. Resuming resets its r
27
27
 
28
28
  | Task | Guide |
29
29
  | :--- | :--- |
30
+ | Prepare a project and reuse its installed dependencies | [Project setup and templates](./guides/project-setup.mdx) |
30
31
  | Run nested containers and build images | [Docker in workspaces](./guides/docker.mdx) |
31
32
  | Transfer files between controller and workspace | [File transfer](./guides/files.mdx) |
32
33
  | Run detached commands with systemd logs | [Background jobs](./guides/background-jobs.mdx) |
34
+ | Manage deadlines and restart services after resume | [Lifetime and persistent services](./guides/long-running-services.mdx) |
35
+ | Automate the CLI and parse its output | [Scripting and CI](./guides/scripting-ci.mdx) |
33
36
  | Forward guest ports or open noVNC desktop | [Desktop and private services](./guides/desktop-services.mdx) |
34
37
  | Capture disk state or clone workspaces | [Snapshots and forks](./guides/snapshots-forks.mdx) |
35
38
  | Export portable backups across hosts | [Backup and recovery](./guides/backup-recovery.mdx) |
39
+ | Update the CLI, host worker, and workspace image | [Upgrades and image changes](./guides/upgrades.mdx) |
36
40
  | Inspect host services and diagnose errors | [Troubleshooting](./guides/troubleshooting.mdx) |
@@ -33,18 +33,31 @@ sudo -n true && echo "Passwordless sudo is functional"
33
33
 
34
34
  <Step>
35
35
 
36
- ### Clone repository and install controller CLI
36
+ ### Install the CLI and obtain deployment scripts
37
37
 
38
- On your controller machine, clone the repository and install the `raft` CLI tool using `uv`:
38
+ On your Linux controller with Python 3.11.8+, install the published CLI:
39
39
 
40
- ```sh title="CONTROLLER: Clone repository and install CLI"
41
- git clone https://github.com/Microck/raft.git raft
40
+ ```sh title="CONTROLLER: Install published CLI"
41
+ npm install -g @microck/raft@0.2.0
42
+ raft --version
43
+ ```
44
+
45
+ Installation does not configure hosts or download images. Clone the matching
46
+ release to obtain the deployment scripts and example configuration:
47
+
48
+ ```sh title="CONTROLLER: Obtain deployment scripts"
49
+ git clone --branch v0.2.0 https://github.com/Microck/raft.git raft
42
50
  cd raft
51
+ ```
52
+
53
+ Alternatively, install from that checkout using `uv` instead of npm:
54
+
55
+ ```sh title="CONTROLLER: Clone repository and install CLI"
43
56
  uv sync --locked
44
57
  uv tool install --editable .
45
58
  ```
46
59
 
47
- Add `~/.local/bin` to `PATH` and verify the CLI:
60
+ For the `uv` installation, add `~/.local/bin` to `PATH` and verify the CLI:
48
61
 
49
62
  ```sh title="CONTROLLER: Verify CLI installation"
50
63
  export PATH="$HOME/.local/bin:$PATH"
@@ -122,16 +135,16 @@ Workspaces require an unprivileged Debian 13 development image matching the host
122
135
 
123
136
  <Tab value="Download release image">
124
137
 
125
- Download the release archive, checksum, and manifest from [Raft releases](https://github.com/Microck/raft/releases). The `sha256sum` check gates import so corrupted archives are rejected:
138
+ Download the release archive, checksum, and manifest from [Raft releases](https://github.com/Microck/raft/releases). These examples pin `v0.2.0`, matching the CLI and deployment checkout above. The `sha256sum` check gates import so corrupted archives are rejected:
126
139
 
127
140
  <Tabs items={["ARM64", "AMD64"]}>
128
141
 
129
142
  <Tab value="ARM64">
130
143
 
131
144
  ```sh title="HOST (ARM64): Download, verify, and import release image"
132
- curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.1.0/raft-dev-arm64.tar.gz
133
- curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.1.0/SHA256SUMS
134
- curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.1.0/manifest-arm64.json
145
+ curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.2.0/raft-dev-arm64.tar.gz
146
+ curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.2.0/SHA256SUMS
147
+ curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.2.0/manifest-arm64.json
135
148
 
136
149
  sha256sum --ignore-missing -c SHA256SUMS && \
137
150
  sudo incus --project raft image import ./raft-dev-arm64.tar.gz --alias raft-dev
@@ -142,9 +155,9 @@ sha256sum --ignore-missing -c SHA256SUMS && \
142
155
  <Tab value="AMD64">
143
156
 
144
157
  ```sh title="HOST (AMD64): Download, verify, and import release image"
145
- curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.1.0/raft-dev-amd64.tar.gz
146
- curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.1.0/SHA256SUMS
147
- curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.1.0/manifest-amd64.json
158
+ curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.2.0/raft-dev-amd64.tar.gz
159
+ curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.2.0/SHA256SUMS
160
+ curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.2.0/manifest-amd64.json
148
161
 
149
162
  sha256sum --ignore-missing -c SHA256SUMS && \
150
163
  sudo incus --project raft image import ./raft-dev-amd64.tar.gz --alias raft-dev
@@ -222,3 +235,5 @@ Check that Incus, the network service, and the expiration timer are active. For
222
235
  ## Next step
223
236
 
224
237
  Proceed to [First working box](./first-box.mdx) to launch, verify, and manage your first workspace.
238
+
239
+ For later releases, follow [Upgrades and image changes](./guides/upgrades.mdx).
@@ -50,7 +50,16 @@ Select CPU and memory limits when launching with `raft new`:
50
50
  - `--cpu`: `1` or `2` shared vCPUs managed by Incus (default: `1`).
51
51
  - `--memory`: `1GiB`, `2GiB`, or `4GiB` cgroup limit (default: `2GiB`).
52
52
 
53
- Resizing existing containers or modifying limits upon resume is not supported.
53
+ Change supported CPU/RAM limits when resuming a stopped box or creating a fork:
54
+
55
+ ```sh
56
+ raft resume "$box" --ttl 1800 --cpu 2 --memory 4GiB
57
+ copy=$(raft fork "$box" --ttl 1800 --memory 2GiB)
58
+ ```
59
+
60
+ Resume requires a stopped box; fork requires a stopped source. Omitted limits
61
+ stay unchanged on resume and inherit the source limits on fork. Live resizing is
62
+ not exposed by the CLI. See [Snapshots and forks](../guides/snapshots-forks.mdx).
54
63
 
55
64
  ## Host resource reserves and active states
56
65
 
@@ -10,6 +10,10 @@ Command-line reference for the `raft` controller tool.
10
10
  Syntax:
11
11
  `raft <subcommand> [options] [arguments]`
12
12
 
13
+ `raft --help` lists commands, `raft <subcommand> --help` lists its arguments,
14
+ and `raft --version` prints the installed version without loading configuration.
15
+ Output is command-specific; see [Scripting and CI](../guides/scripting-ci.mdx).
16
+
13
17
  Workspaces are referenced by location-qualified handles formatted as `<location>:<instance-name>` (for example `lab:rf-a1b2c3d4e5f60718`).
14
18
 
15
19
  ## Command index
@@ -31,7 +35,7 @@ Workspaces are referenced by location-qualified handles formatted as `<location>
31
35
 
32
36
  Create and launch a new workspace container.
33
37
 
34
- `raft new [--location LOCATION] [--ttl TTL] [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB}]`
38
+ `raft new [--location LOCATION] [--ttl TTL] [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB}] [--disposable] [--from LOCATION:BOX/SNAPSHOT]`
35
39
 
36
40
  - `--location LOCATION`: Target host location in `~/.config/raft/incus.json`. Defaults to first configured location.
37
41
  - `--ttl TTL`: Lifetime in seconds before automatic stop (`60` to `2592000`). Default: `600`.
@@ -59,6 +63,9 @@ Start a stopped workspace with a renewed lifetime deadline.
59
63
 
60
64
  - `--ttl TTL`: Lifetime in seconds from resumption (`60` to `2592000`). Required.
61
65
 
66
+ CPU/RAM overrides on resume apply before startup. Omitted values stay unchanged.
67
+ A running box is rejected before changing its limits.
68
+
62
69
  ### `extend`
63
70
 
64
71
  Reset the expiration deadline for a running workspace without restarting it.
@@ -75,8 +82,6 @@ Permanently delete a workspace container and its snapshots.
75
82
 
76
83
  Stops the container if running, removes all associated Btrfs snapshots, and releases host storage.
77
84
 
78
- CPU/RAM overrides on resume apply before startup. Omitted values stay unchanged. A running box is rejected before changing its limits.
79
-
80
85
  ## Execution and inspection commands
81
86
 
82
87
  ### `exec`
@@ -252,7 +257,7 @@ Inspect host resource capacity, active allocations, and recommended workspace li
252
257
 
253
258
  ### `doctor`
254
259
 
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.
260
+ Inspect host connectivity, Incus version, expiry-worker compatibility, active systemd units (`incus`, `raft-expire.timer`, `raft-network.service`), root filesystem usage, storage pool status, and saved box count across configured hosts. An incompatible worker returns a nonzero exit status and names the failing location without running expiry or modifying boxes.
256
261
 
257
262
  `raft doctor`
258
263
 
@@ -262,6 +267,10 @@ List existing workspace containers across configured hosts.
262
267
 
263
268
  `raft list [--location LOCATION]`
264
269
 
270
+ Prints one JSON object per box (JSONL), with `box`, `state`, and `stopAt` fields.
271
+ An empty inventory prints no records. See [Scripting and CI](../guides/scripting-ci.mdx)
272
+ for output formats, exit codes, readiness, and failure handling.
273
+
265
274
  ### `gc`
266
275
 
267
276
  Enforce deadlines immediately. Persistent boxes stop; disposable boxes are deleted.
@@ -39,7 +39,7 @@ The configuration file maps location names to host definition objects:
39
39
  ### Location keys
40
40
 
41
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.
42
+ - **Default location**: The first location listed is the default for `raft new`. `raft list`, `raft limits`, `raft gc`, and `raft prune` accept `--location` filters. Without a filter they check all configured hosts; `raft doctor` always checks all configured hosts.
43
43
 
44
44
  ### Host entry fields
45
45
 
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Raft 0.2.1
3
+ ---
4
+
5
+ # Raft 0.2.1
6
+
7
+ - `raft doctor` now rejects an incompatible host expiry worker and identifies the location that needs deployment. It does not run expiry or change boxes.
8
+ - Lifecycle transactions close controller stdin so allocation cannot wait for piped input while holding the host lock.
9
+
10
+ Install on Linux with Python 3.11.8+ using `npm install -g @microck/raft@0.2.1`, or install the attached Python wheel. Updating the CLI does not update host workers. Deploy the matching checkout to hosts that fail the readiness check.
11
+
12
+ Exec, SSH, and file-transfer behavior is unchanged. Piped input to `raft exec` is unsupported.
13
+
14
+ ARM64 and AMD64 development images accompany this release. Verify `SHA256SUMS` before importing an image for your host architecture.
@@ -57,9 +57,15 @@ The [final runtime validation](https://github.com/Microck/raft/actions/runs/3754
57
57
 
58
58
  ## New workspace controls
59
59
 
60
- `python3 deploy/verify-features.py --location lab` verifies recursive trees and unsafe-archive rejection, templates from a running source, snapshot deletion, CPU/RAM overrides, disposable timer deletion, and retention preview/apply. It uses only its own fixtures. Run the archive checks without `--location` to avoid allocating boxes.
60
+ `python3 deploy/verify-features.py --location lab` verifies `doctor` readiness against the installed worker, recursive trees and unsafe-archive rejection, templates from a running source, snapshot deletion, CPU/RAM overrides, disposable timer deletion, and retention preview/apply. It uses only its own fixtures. Run the archive checks without `--location` to avoid allocating boxes.
61
61
 
62
- For an older deployment whose services should stay unchanged, `--isolated-worker` stages the current worker under a private temporary path. This checks worker logic without installing it; automatic timer integration is verified on disposable CI hosts. Current deployments use the normal verification mode; the isolated mode rejects them before allocation.
62
+ The live suite also holds a controller stdin pipe open while running real remote
63
+ readers through both captured and streamed lifecycle transactions. Both must
64
+ receive EOF without consuming controller input, and a subsequent transaction
65
+ must acquire the same host lock. This covers allocation stalls caused by Incus
66
+ waiting for stdin while the expiration worker waits for the lifecycle lock.
67
+
68
+ For an older deployment whose services should stay unchanged, `--isolated-worker` first verifies that `doctor` rejects the stale installed worker with a location-specific deployment diagnostic, then uses the current worker under a private temporary path. This checks worker logic without installing it; automatic timer integration is verified on disposable CI hosts. Current deployments use the normal verification mode; the isolated mode rejects them before allocation.
63
69
 
64
70
  ## Workspace controls validation (2026-10-07)
65
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microck/raft",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Self-hosted Linux workspaces powered by Incus, an open-source alternative to boat.dev",
5
5
  "license": "MIT",
6
6
  "repository": {
package/raft.py CHANGED
@@ -16,7 +16,7 @@ import uuid
16
16
 
17
17
  import raft_files
18
18
 
19
- __version__ = "0.2.0"
19
+ __version__ = "0.2.1"
20
20
 
21
21
  CONFIG = Path.home() / ".config/raft/incus.json"
22
22
  PROJECT = "raft"
@@ -304,7 +304,9 @@ def transaction(location, script, *arguments, stdout=None):
304
304
  *arguments,
305
305
  ]
306
306
  connection = [
307
+ # Lifecycle transactions take arguments, never interactive or piped input.
307
308
  "ssh",
309
+ "-n",
308
310
  "-T",
309
311
  "-o",
310
312
  "BatchMode=yes",
@@ -710,6 +712,10 @@ def dispatch(args):
710
712
  print(output, end="")
711
713
  elif args.action == "doctor":
712
714
  print(location + ": " + incus(location, "version").strip())
715
+ try:
716
+ transaction(location, CHECK_WORKER)
717
+ except RuntimeError as error:
718
+ raise RuntimeError(f"{location}: {error}") from error
713
719
  response = subprocess.run(
714
720
  [
715
721
  "ssh",