@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
@@ -0,0 +1,122 @@
1
+ """Exercise repeated source sync against a disposable native workspace."""
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+
12
+ spec = importlib.util.spec_from_file_location("live", Path(__file__).with_name("verify-live.py"))
13
+ live = importlib.util.module_from_spec(spec)
14
+ spec.loader.exec_module(live)
15
+
16
+
17
+ def verify(location):
18
+ box = live.raft(
19
+ "new",
20
+ "--location",
21
+ location,
22
+ "--ttl",
23
+ "600",
24
+ "--disposable",
25
+ "--purpose",
26
+ "Source sync verification",
27
+ "--owner",
28
+ "verify-sync",
29
+ ).stdout.strip()
30
+ try:
31
+ live.wait_running(box)
32
+ with tempfile.TemporaryDirectory(prefix="raft-sync-") as folder:
33
+ source = Path(folder) / "source"
34
+ source.mkdir()
35
+ subprocess.run(["git", "init", "-q", source], check=True)
36
+ (source / ".gitignore").write_text("ignored\n")
37
+ (source / "ignored").write_text("private")
38
+ (source / "file").write_text("first")
39
+ (source / "nested").mkdir()
40
+ (source / "nested/file").write_text("nested")
41
+ unusual = [
42
+ "space name",
43
+ "a*b",
44
+ "a?b",
45
+ "a[b]",
46
+ "back\\slash",
47
+ "line\nbreak",
48
+ "back\\*star",
49
+ ]
50
+ for path in unusual:
51
+ (source / path).write_text(path)
52
+ (source / "link").symlink_to("file")
53
+ (source / "unsafe").symlink_to("../outside")
54
+ outside = Path(folder) / "outside"
55
+ outside.mkdir()
56
+ (outside / "file").write_text("must not leak")
57
+ subprocess.run(["git", "-C", source, "add", "."], check=True)
58
+
59
+ def sync(*options):
60
+ return live.raft("sync", box, str(source), "/workspace/sync target", *options)
61
+
62
+ def files():
63
+ return json.loads(
64
+ live.execute(
65
+ box,
66
+ "python3",
67
+ "-c",
68
+ """
69
+ import json, pathlib
70
+ root = pathlib.Path('/workspace/sync target')
71
+ print(json.dumps({str(p.relative_to(root)): p.read_text(errors="replace") for p in root.rglob('*') if p.is_file()}))
72
+ """,
73
+ )
74
+ )
75
+
76
+ sync("--dry-run")
77
+ assert (
78
+ live.raft(
79
+ "exec", box, "--", "test", "-e", "/workspace/sync target", check=False
80
+ ).returncode
81
+ != 0
82
+ )
83
+ sync()
84
+ first = files()
85
+ assert first["file"] == "first" and first["nested/file"] == "nested", first
86
+ assert "ignored" not in first and "unsafe" not in first
87
+ assert all(first[path] == path for path in unusual), first
88
+ assert not any(path.startswith(".git/") for path in first), first
89
+ repeated = sync()
90
+ assert not any(
91
+ line.startswith(("<f", ">f")) for line in repeated.stdout.splitlines()
92
+ ), repeated.stdout
93
+ stamp = (source / "file").stat()
94
+ (source / "file").write_text("other")
95
+ os.utime(source / "file", ns=(stamp.st_atime_ns, stamp.st_mtime_ns))
96
+ sync("--dry-run")
97
+ assert files()["file"] == "first"
98
+ sync()
99
+ assert files()["file"] == "other"
100
+ (source / "file").unlink()
101
+ sync()
102
+ assert files()["file"] == "other", "Guest-only files must survive"
103
+ # A directory replaced by an escaping symlink must never cause the
104
+ # tracked descendant to be read from outside the selected source.
105
+ shutil.rmtree(source / "nested")
106
+ (source / "nested").symlink_to(outside)
107
+ sync()
108
+ assert files()["nested/file"] == "nested"
109
+ sync("--all")
110
+ assert files()["ignored"] == "private"
111
+ assert live.raft("sync", box, str(source), "/", check=False).returncode != 0
112
+ print(
113
+ f"{location}: sync filtering, repeat, checksum, dry-run, preservation and links passed"
114
+ )
115
+ finally:
116
+ live.raft("destroy", box)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ cli = argparse.ArgumentParser(description=__doc__)
121
+ cli.add_argument("--location", required=True)
122
+ verify(cli.parse_args().location)
@@ -0,0 +1,70 @@
1
+ {
2
+ "date": "2026-10-29",
3
+ "baselineCommit": "1fd6d62f",
4
+ "unit": "seconds",
5
+ "method": "Five sequential samples per command/version, one configured ARM64 Incus 6.0.6 host. Controller-visible execution includes Python and SSH. Update checks disabled. Exploratory sample; not a boot or workload benchmark.",
6
+ "samples": {
7
+ "list": {
8
+ "baseline": [
9
+ 0.8216385410050862,
10
+ 0.8366823430042132,
11
+ 0.8693760940004722,
12
+ 0.8536870829993859,
13
+ 0.8263150519996998
14
+ ],
15
+ "candidate": [
16
+ 0.4651195309998002,
17
+ 0.4672731250029756,
18
+ 0.4791866140003549,
19
+ 0.47663515599560924,
20
+ 0.45945281199965393
21
+ ]
22
+ },
23
+ "capacity": {
24
+ "baseline": [
25
+ 0.8409752599982312,
26
+ 0.8461970310017932,
27
+ 0.8160613539948827,
28
+ 0.8384463539987337,
29
+ 0.8118131250012084
30
+ ],
31
+ "candidate": [
32
+ 0.47482109300472075,
33
+ 0.48634999999922,
34
+ 0.4781393230005051,
35
+ 0.47350171799917007,
36
+ 0.49643182300496846
37
+ ]
38
+ },
39
+ "doctor": {
40
+ "baseline": [
41
+ 2.2246155190005084,
42
+ 2.1928324990003603,
43
+ 2.2085186969998176,
44
+ 2.260335936996853,
45
+ 2.1928395819995785
46
+ ],
47
+ "candidate": [
48
+ 0.5241887500014855,
49
+ 0.5317754690040601,
50
+ 0.5393725000030827,
51
+ 0.5227220310043776,
52
+ 0.5310975519969361
53
+ ]
54
+ }
55
+ },
56
+ "medians": {
57
+ "list": {
58
+ "baseline": 0.8366823430042132,
59
+ "candidate": 0.4672731250029756
60
+ },
61
+ "capacity": {
62
+ "baseline": 0.8384463539987337,
63
+ "candidate": 0.4781393230005051
64
+ },
65
+ "doctor": {
66
+ "baseline": 2.2085186969998176,
67
+ "candidate": 0.5310975519969361
68
+ }
69
+ }
70
+ }
@@ -1,10 +1,43 @@
1
1
  ---
2
- title: Boat feature parity
2
+ title: Raft and Boat
3
+ description: Compare Raft’s self-hosted containers with Boat’s managed VMs and workflows.
3
4
  ---
4
5
 
5
- # Boat capability coverage
6
+ # Raft and Boat
6
7
 
7
8
  Raft is an independent open-source alternative to [boat.dev](https://boat.dev).
9
+ ## Choose by operating model
10
+
11
+ - **Raft:** bring a dedicated Ubuntu host and manage it through host SSH. Boxes
12
+ are unprivileged containers sharing its kernel. You operate storage, upgrades
13
+ and backups; there is no hosted billing, organization account or HTTP API.
14
+ - **Boat:** a managed service with full VMs, account/API workflows, public hosting
15
+ and integrated agents. Its pricing, snapshot retention and security model are
16
+ different from Raft's.
17
+
18
+ Start with [Raft setup](./operations.mdx) to use it, or expand the detailed matrix
19
+ when evaluating a specific capability. Similar commands are not a promise of
20
+ identical behavior, isolation or cost.
21
+
22
+ ## What remains for parity
23
+
24
+ The largest missing pieces are a remote API with scoped credentials and SDKs,
25
+ managed agent conversations, versioned environments and secrets, public HTTPS
26
+ ingress, and team access. These require product and infrastructure work beyond
27
+ adding CLI commands.
28
+
29
+ Operational gaps include automatic snapshots, shared templates, snapshot file
30
+ browsing, retained command history, process inspection and telemetry export.
31
+ Smaller CLI gaps include reverse tunnels, shell completions, uniform JSON errors,
32
+ friendly names and macOS/Windows controller support.
33
+
34
+ VM kernel isolation and nested KVM require a different runtime mode. Raft's
35
+ current container boundary cannot provide them through a flag. Browser-only
36
+ streaming is also separate from the existing full desktop.
37
+
38
+ <details>
39
+ <summary>Detailed capability matrix and verification evidence</summary>
40
+
8
41
  ✅ supported · ❌ unavailable through the compared product · ❓ not established by Boat's documentation.
9
42
 
10
43
  A shared checkmark means both offer the workflow, not identical behavior or isolation.
@@ -25,6 +58,7 @@ Tests verify Raft behavior, not hosted Boat infrastructure. See [verification](.
25
58
  | Usage | ✅ | ✅ | Cgroup memory, CPU counters, and shared-pool or own-disk space. No billing meter |
26
59
  | Root terminal | ✅ | ✅ | PTY over host SSH and native Incus exec. No directly exposed guest SSH endpoint |
27
60
  | Synchronous exec | ✅ | ✅ | Literal argv and preserved exit code. Shell syntax requires an explicit shell |
61
+ | Disposable foreground CI | ✅ | Preview | Unreleased 0.7.0: `run` orchestrates one fresh box, Git-aware upload, output collection and cleanup. Fixed timeout, no lease renewal/keep mode; host sudo SSH rather than scoped API keys. [CI guide](./guides/ci-jobs.mdx) |
28
62
  | Detached exec | ✅ | ✅ | Guest systemd unit, status, logs, cancellation. No process checkpoint or restart after stop |
29
63
  | File transfer | ✅ | ✅ | Binary file upload/download. Recursive tree transfer for running boxes; no directory merges |
30
64
  | Named snapshots | ✅ | ✅ | Create/list/restore with stopped source, checked inside the lifecycle lock. No running snapshot consistency guarantee |
@@ -42,24 +76,43 @@ Tests verify Raft behavior, not hosted Boat infrastructure. See [verification](.
42
76
  | Bounded host storage pools | ❓ | ✅ | Raft configures a shared 60 GiB Btrfs pool and, when enabled, a fully allocated LVM thin pool for own box disks that it never overcommits, and rejects size drift. Boat does not document an operator-managed host storage pool |
43
77
  | Strict per-box disk allocation | ✅ | ✅ | On hosts with own box disks enabled, `new --disk SIZE` gives a box a fixed-size, copy-on-write ext4 disk that guest and Docker writes cannot exceed. Shared-pool boxes, the default, have no per-box quota |
44
78
  | VM kernel isolation | ✅ | ❌ | VM deployment not implemented |
45
- | Nested KVM / Android emulator acceleration | ✅ | ❌ | Boat documents KVM on standard hosts; overflow cloud VMs lack it. Raft has no guest KVM device or VM mode |
79
+ | Nested KVM / Android emulator acceleration | ✅ | ❌ | Boat currently documents `/dev/kvm` on every sandbox. Raft has no guest KVM device or VM mode |
46
80
  | External guest SSH | ✅ | ❌ | Boat supports a public IP or an SSH endpoint; public IPv4 is not guaranteed. Raft uses host-mediated private access |
47
81
  | Named environments and secrets | ✅ | ❌ | Files/configuration can be prepared explicitly. No environment registry or secret-injection service |
48
- | Organizations | ✅ | ❌ | Boat shares billing and plan limits; resources stay with their creator. Raft is single-operator |
82
+ | Organizations | ✅ | ❌ | Boat supports team-visible sandboxes, optional shared access, roles and per-member environment/profile restrictions. Raft is single-operator |
49
83
  | Lifecycle webhooks | ✅ | ❌ | Missing. Deferred until an external consumer needs them |
50
84
  | Automatic deletion/retention | ✅ | ✅ | Disposable delete-on-stop/expiry; opt-in stopped-box retention cleanup with preview |
51
85
  | Dashboard | ✅ | ❌ | Missing. CLI and private desktop cover current workflow |
52
86
  | SDK/API keys | ✅ | ❌ | Host SSH authentication. No remote Raft service API |
53
- | CLI-wide JSON mode | ✅ | ❌ | Boat supports JSON/JSONL for most commands; argument errors may use stderr. Raft provides JSON for list/info/usage/capacity and JSONL for prune |
87
+ | CLI-wide JSON mode | ✅ | Preview | Boat supports JSON/JSONL for most commands; argument errors may use stderr. Raft provides command-specific JSON/JSONL for locations, list, info, usage, capacity, config and prune, and unreleased global JSONL events for reports, streams, errors and exit codes |
54
88
  | Snapshot file browsing/download | ✅ | ❌ | Full native archives only; no Raft snapshot tree or selected-file interface |
55
89
  | CLI self-update | ✅ | ✅ | `raft self-update` upgrades npm global installs through the registry and uv tools from a release wheel checked against `SHA256SUMS`. A daily stderr notice reports new releases. Boat also updates itself after commands; Raft never installs without the command and never changes hosts |
56
- | Dynamic shell completions | ✅ | ❌ | Static `--help` only |
90
+ | Dynamic shell completions | ✅ | Preview | Unreleased Bash, Zsh and Fish scripts query the installed parser for commands, options and fixed choices |
57
91
  | Managed agents | ✅ | ❌ | Not implemented. No prompt/events/steer/interrupt/conversation API |
92
+ | Automatic incremental snapshots | ✅ | ❌ | Boat documents saves every minute and on stop. Raft retains the container disk and creates named snapshots only on request; no scheduled off-host snapshot service |
93
+ | Share saved templates across accounts | ✅ | ❌ | Boat provides revocable deployment codes and credential cleanup. Raft copies complete filesystems on the same host or imports trusted whole archives; no sharing identity or sanitization service |
94
+ | Versioned environments and upgrades | ✅ | ❌ | Boat versions repositories, setup and secrets, with explicit upgrades. Raft has image pins, controller defaults and filesystem templates, not an environment registry |
95
+ | AI credential profiles and proxy | ✅ | ❌ | No provider credential manager, sandbox access switch, usage accounting or AI request proxy |
96
+ | OpenTelemetry export | ✅ | ❌ | No built-in OTLP metrics or lifecycle-event export |
97
+ | Command history while stopped | ✅ | Preview | Unreleased retained detached-job results survive stop and deletion for seven days, with a 1 MiB log cap. No general process history or samples |
98
+ | Process inspection command | ✅ | ❌ | Raft can execute guest tools such as ps, but has no first-class process/history API or stopped-box process samples |
99
+ | Merge source into existing directories | ✅ | Preview | Boat push replaces matching files in an existing destination. Unreleased `sync` replaces matching files with Git filtering and checksum-based incremental transfer; it preserves guest-only files. Recursive upload still requires a new destination |
100
+ | Human-readable names and current/self aliases | ✅ | ❌ | Raft uses exact location-qualified handles and ownership notes; no rename or implicit current-box selection |
101
+ | macOS and Windows controllers | ✅ | ❌ | Raft's controller requires Linux; native ARM64/AMD64 guest support is a separate capability |
102
+ | Unresponsive-machine recovery | ✅ | ❌ | Boat recover power-cycles an unresponsive VM. Raft recover imports an archive and is not the equivalent operation |
58
103
 
59
104
  ## Reference check
60
105
 
61
- Checked on 2026-10-07 against Boat's [CLI reference](https://docs.boat.dev/cli-reference), [machine capabilities](https://docs.boat.dev/machines), [SSH/file access](https://docs.boat.dev/ssh-access), [snapshots](https://docs.boat.dev/snapshots), [data retention](https://docs.boat.dev/data-retention), and [billing and limits](https://docs.boat.dev/billing).
106
+ Rechecked on 2026-10-29 against Boat's [CLI reference](https://docs.boat.dev/cli-reference), [machine capabilities](https://docs.boat.dev/machines), [SSH/file access](https://docs.boat.dev/ssh-access), [snapshots](https://docs.boat.dev/snapshots), [environments](https://docs.boat.dev/environments), [environment versions](https://docs.boat.dev/environments/versions), [organizations](https://docs.boat.dev/organizations), [AI proxy API](https://docs.boat.dev/api/reference/ai-credentials/get-ai-credentials-proxy), and [telemetry API](https://docs.boat.dev/api/reference/account/get-opentelemetry-export).
107
+
108
+ Some Boat overview/API text still describes creator-private resources; the dedicated organization guide and current CLI document shared sandboxes. This matrix follows those specific workflow descriptions. Boat's browser-only view restricts display and input, not access by other code inside its sandbox. These are documented capabilities, not independently tested security guarantees.
109
+
110
+ Raft's latest published release was verified as [0.6.0](https://github.com/Microck/raft/releases/tag/v0.6.0) on 2026-10-29. `run` and the new help/read-performance changes remain development-branch features until released.
62
111
 
63
112
  The new workspace-control suite passed on ARM64 and AMD64 at runtime commit `a7fef1f5` in [run 37603576786](https://github.com/Microck/raft/actions/runs/37603576786). The baseline native architecture and disposable AMD64 host tests passed in [run 37496577945](https://github.com/Microck/raft/actions/runs/37496577945). This audit compares documented features, not a live differential test against Boat. Managed agent workflows remain out of scope.
64
113
 
65
114
  Recursive transfers, resume/fork sizing, snapshot templates/deletion and disposable lifecycle are implemented. Directory transfers require a running box; templates stay on the source host. Public hosting, VM isolation, organization sharing, and remote APIs require additional infrastructure. noVNC tests do not establish parity for Boat frame rate, browser confinement or application set.
115
+
116
+ CI comparison sources (reviewed 2026-10-22): [Boat CI](https://docs.boat.dev/ci), [CLI run](https://docs.boat.dev/cli-reference#boat-run-command), [scoped API keys](https://docs.boat.dev/api-keys/scopes), and [snapshot contents](https://docs.boat.dev/snapshots/whats-saved). Matching workflows do not imply matching security boundaries or costs.
117
+
118
+ </details>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  title: Design decisions
3
+ description: "Architecture choices for contributors: Incus, SSH, clean images and private access."
3
4
  ---
4
5
 
5
6
  # Architecture and references
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  title: Boot performance
3
+ description: Dated native boot measurements, sample limits and reproducible benchmark commands.
3
4
  ---
4
5
 
5
6
  # Native images and boot performance
@@ -8,6 +9,26 @@ Raft builds Debian 13 system container images for native ARM64 and AMD64 hosts.
8
9
 
9
10
  Timings depend on software, image state, hardware, host load, and network transport. These measurements do not compare Raft with hosted Boat.
10
11
 
12
+ ## Controller read latency, 2026-10-29
13
+
14
+ The development CLI batches host reads into one SSH connection per command.
15
+ Five samples per version on one ARM64 Incus 6.0.6 host gave these medians:
16
+
17
+ | Command | Baseline at 1fd6d62f | Candidate | Reduction |
18
+ | --- | ---: | ---: | ---: |
19
+ | `list --location HOST` | 0.837 s | 0.467 s | 44% |
20
+ | `capacity --location HOST` | 0.838 s | 0.478 s | 43% |
21
+ | `doctor` for one host | 2.209 s | 0.531 s | 76% |
22
+
23
+ [Raw samples](benchmarks/read-latency-2026-10-29.json) include all timings.
24
+ Both versions used the same controller, host and SSH configuration, with
25
+ `RAFT_NO_UPDATE_CHECK=1`. Each sample started a fresh Python process; doctor
26
+ called `raft.doctor(location)` directly because the baseline CLI cannot select
27
+ one host. Five baseline samples preceded five candidate samples per command.
28
+ These are exploratory network-dependent timings, not boot or guest workload
29
+ speedups. The changes retain fast metadata reads and the worker compatibility
30
+ lock; they do not weaken lifecycle checks or change host services.
31
+
11
32
  ## Native CI comparison
12
33
 
13
34
  [Verified native run](https://github.com/Microck/raft/actions/runs/37447415532), 2026-10-06. Five samples per architecture on 4-CPU runners with 1 guest CPU and 2 GiB guest RAM:
package/docs/contract.md CHANGED
@@ -1,9 +1,13 @@
1
1
  ---
2
2
  title: Runtime contract
3
+ description: Detailed implementation behavior and safety boundaries for maintainers.
3
4
  ---
4
5
 
5
6
  # Raft contract
6
7
 
8
+ This is the implementation reference for maintainers and advanced operators.
9
+ For a first run, use [host setup](./operations.mdx) and [your first box](./first-box.mdx).
10
+
7
11
  Raft manages persistent unprivileged native-architecture Incus system containers on configured ARM64 or AMD64 Linux hosts. Containers share the host kernel. Images must match host CPU architecture; foreign-architecture emulation and VM workflows are not implemented.
8
12
 
9
13
  ## Image releases
@@ -27,6 +31,8 @@ Raft manages persistent unprivileged native-architecture Incus system containers
27
31
  - **Firewall precedence**: Redeployment reapplies firewall rules. Incus startup requires successful firewall application before the daemon starts, including socket activation.
28
32
  - **Guided onboarding**: `raft host add LOCATION --ssh TARGET` probes the target read-only and refuses unsupported hosts, including macOS, before any change. It prints the exact plan and applies it only after an interactive yes or `--yes`; `--dry-run` stops after the plan. It runs the same deployer as `deploy/deploy.py`, imports an image only after its SHA-256 matches (release `SHA256SUMS` or the local archive digest) and its architecture matches the host, then registers the location with the image fingerprint. It finishes with `doctor` and a disposable create/exec/destroy smoke test; if either fails, it removes the new location from the configuration and leaves host changes for a rerun. It uses existing key-based SSH and never collects passwords or changes SSH, other firewall rules or IP configuration. It requires the deployment files from the npm package or a source checkout.
29
33
  - **CLI coverage**: The single-operator workflow spans lifecycle, execution, files, snapshots, jobs, tunnels, backup, and advisory capacity inspection; see the [command reference](./reference/cli.md).
34
+ - **Capability discovery**: Installed `--help` and nested command help describe the current commands, arguments and operational constraints without contacting hosts. `config --location NAME` and `doctor --location NAME` inspect only that configured host; omitting the flag preserves inspection of all hosts. A location filter on `config set/unset` is rejected because defaults belong to the controller.
35
+ - **Read latency**: Listing collects policy and inventory through one SSH connection per host. Capacity collects resources and inventory through one connection. Doctor performs its sequential checks through one connection, retaining the worker compatibility lock. These are read-only observations, not locked snapshots; lifecycle validation remains inside its existing transactions.
30
36
 
31
37
  ## Configuration and lifecycle defaults
32
38
 
@@ -35,7 +41,7 @@ Raft manages persistent unprivileged native-architecture Incus system containers
35
41
  | Host identification | Configuration requires named SSH targets using letters, digits, underscores, or hyphens. |
36
42
  | Qualified handles | Every handle includes location and instance name (`<location>:<name>`). |
37
43
  | Image pinning | Workspace creation requires the immutable 64-hex image fingerprint, not an updating alias. |
38
- | Lifecycle defaults | Built-in defaults are the first configured location, 1 CPU, 2 GiB RAM, the shared pool, and 600-second lifetime. `~/.config/raft/config.json` can override them for `new` only; explicit flags win. Boxes from a snapshot (`new --from`) and forks keep the source's CPU, RAM and disk instead of any default, configured or built-in; each flag overrides only its own value. `--cpu` takes any positive whole number, `--memory` any positive whole GiB (`16GiB`), `--disk` a positive whole GiB or `shared`, and TTL 60 to 2,592,000 seconds (30 days). |
44
+ | Lifecycle defaults | Built-in defaults are the first configured location, 1 CPU, 2 GiB RAM, the shared pool, a persistent box and 600-second lifetime. `~/.config/raft/config.json` can override them for `new` only; explicit flags win, and `--persistent` overrides a `disposable` default. Boxes from a snapshot (`new --from`) and forks keep the source's CPU, RAM and disk instead of any default, configured or built-in; each flag overrides only its own value. `--cpu` takes any positive whole number, `--memory` any positive whole GiB (`16GiB`), `--disk` a positive whole GiB or `shared`, and TTL 60 to 2,592,000 seconds (30 days). |
39
45
  | Box count and size | Raft limits neither how many boxes a host holds nor their size. CPU or RAM limits larger than the whole host (CPU count, or `MemTotal` rounded up to whole GiB) are refused under the host lock before any change, because Incus would clip them silently. Other failures come from Incus, such as too little pool space. |
40
46
  | TTL calculation | Running TTL begins after launch completes, using the host clock. Cold image preparation does not consume TTL. |
41
47
  | 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. |
@@ -44,14 +50,15 @@ Raft manages persistent unprivileged native-architecture Incus system containers
44
50
 
45
51
  `raft locations` lists configured locations as JSONL with `location`, `default` and `note` fields without contacting hosts. `raft note LOCATION TEXT` and `raft note LOCATION --clear` edit the optional `note` field in the controller configuration atomically, preserving location order and mode `0600`. Notes are advisory guidance; Raft shows them in `locations`, `capacity` and on stderr from `new`, and never enforces them.
46
52
 
47
- Boxes accept an optional note with a fixed template: `purpose` (required), `owner` and `next`, all nonempty strings. `raft new --purpose TEXT [--owner TEXT] [--next TEXT]` and `raft note BOX ...` store it as JSON in the box's `user.raft.note` Incus key; `raft note BOX --clear` removes it. Each edit replaces the whole note. `raft list` reports it as `note`, or `null`, and reports each box's `disk` as `"shared"` or its own disk size. `new` always writes the key, so `new --from` never inherits a snapshot's note. Forks and recovered backups keep it unless `fork` or `recover` gets note flags, which replace the whole note. Box notes never change lifecycle behavior.
53
+ Boxes accept an optional note with a fixed template: `purpose` (required), `owner` and `next`, all nonempty strings. `raft new --purpose TEXT [--owner TEXT] [--next TEXT]` and `raft note BOX ...` store it as JSON in the box's `user.raft.note` Incus key; `raft note BOX --clear` removes it. Each edit replaces the whole note. `raft list` reports it as `note`, or `null`, reports each box's `disk` as `"shared"` or its own disk size, and reports `deleteAt`, the Unix time retention deletes a stopped box, or `""`. `new` always writes the key, so `new --from` never inherits a snapshot's note. Forks and recovered backups keep it unless `fork` or `recover` gets note flags, which replace the whole note. Box notes never change lifecycle behavior.
48
54
 
49
- ## Controller defaults and note policies
55
+ ## Controller defaults and host policies
50
56
 
51
- - **Controller defaults**: `~/.config/raft/config.json` holds `{"defaults": {...}}` with optional `location`, `ttl`, `cpu`, `memory` and `disk` for `new`. Precedence is explicit flag, then configured default, then built-in default. `cpu`, `memory` and `disk` apply only to boxes from the image: `new --from` keeps the snapshot's location, CPU, RAM and disk unless a flag overrides them, and `--disk shared` is refused there. A `disk` default on a host without `raft-disks`, or larger than its unreserved space, fails like `--disk` and names `defaults.disk` as the source. Other commands take no defaults. A value is valid exactly when the same text is valid for the flag. Unknown keys, bad values and malformed JSON fail `new` with the file path and key, before any host access. `raft config set/unset` validate first, write atomically under a controller lock, keep mode `0600`, print the change and can repair the key they edit. The file is separate from `incus.json`, so location names never collide with setting names.
52
- - **Note policy storage**: `raft policy LOCATION --require FIELDS` stores required box note fields, which must include `purpose`, in the host's Incus project key `user.raft.require`; `--clear` empties it. Each change runs under the host lock, prints the old and new fields, records UTC time, the SSH account (`SUDO_USER`) and the self-reported controller user and hostname in `user.raft.require.changed`, and logs to the host journal with tag `raft`. Redeployment leaves these keys unchanged.
57
+ - **Controller defaults**: `~/.config/raft/config.json` holds `{"defaults": {...}}` with optional `location`, `ttl`, `cpu`, `memory`, `disk` and `disposable` for `new`. Precedence is explicit flag, then configured default, then built-in default. `cpu`, `memory` and `disk` apply only to boxes from the image: `new --from` keeps the snapshot's location, CPU, RAM and disk unless a flag overrides them, and `--disk shared` is refused there. `ttl` and `disposable` apply to `new --from` as well, because a snapshot carries no lifecycle; `new --disposable` and `--persistent` are mutually exclusive overrides. `fork` takes no defaults and is persistent without `--disposable`. A `disk` default on a host without `raft-disks`, or larger than its unreserved space, fails like `--disk` and names `defaults.disk` as the source. Other commands take no defaults. A value is valid exactly when the same text is valid for the flag. Unknown keys, bad values and malformed JSON fail `new` with the file path and key, before any host access. `raft config set/unset` validate first, write atomically under a controller lock, keep mode `0600`, print the change and can repair the key they edit. The file is separate from `incus.json`, so location names never collide with setting names.
58
+ - **Policy storage**: `raft policy LOCATION [--require FIELDS|none] [--delete-stopped-after HOURS|never]` sets either host policy or both; each option changes only its own. Required box note fields, which must include `purpose`, live in the Incus project key `user.raft.require`, and `--require none` empties it. Stopped-box retention lives in `user.raft.delete-stopped-after` as a positive whole number of hours, with the time it was set in `user.raft.delete-stopped-after.since`; `never` empties both. Both values are validated before any host change. Each change runs under the host lock, prints the old and new value, records UTC time, the SSH account (`SUDO_USER`) and the self-reported controller user and hostname in the policy's `.changed` key, and logs to the host journal with tag `raft`. Redeployment leaves these keys unchanged.
53
59
  - **Note policy enforcement**: `new` (including `--from`), `fork`, `recover` and `raft note BOX` check the note the box will carry. The controller reads the policy first and fails with the missing flags and a complete corrected command before changing anything. The host repeats the check inside the transaction that writes the note, under the host lock, so a concurrent policy change cannot be missed. That transaction also holds the oversize and own-disk reservation checks; all of them run before Incus creates anything. `fork` checks the given note or, without one, the source note read under the lock. `recover` checks a given note before upload; an archived note is checked after import, and a failing import is deleted in the same transaction. `note --clear` and replacement notes that drop a required field are rejected.
54
- - **Policy scope**: The policy is a guardrail for agents, not an access control. Anyone with host SSH and sudo can change it or create boxes with native Incus commands, and controllers without policy support do not check it. `raft config` reports every location's policy and last change.
60
+ - **Retention enforcement**: The host expiry worker deletes a stopped Raft box, with its snapshots, once the hours have passed since the later of its `user.raft.stopped-at` and the policy's `.since` time, so setting, shortening or renewing a policy never deletes a box sooner than its hours after the change. It runs on the 15-second timer and on `raft gc`, under the host lock, and writes each deletion to the host journal with tag `raft`. Running boxes are never touched. A stopped Raft box without `user.raft.stopped-at`, from a guest poweroff, a native stop, a failed start or `raft recover`, gets the worker's current time as its stop time, never an earlier guess, whether or not a policy is set. A malformed retention value makes the worker delete nothing by retention, still enforce deadlines, and exit nonzero; the CLI reports it with the fix. Setting retention requires the current worker protocol; clearing it does not.
61
+ - **Policy scope**: Policies are guardrails for agents, not access controls. Anyone with host SSH and sudo can change them or create boxes with native Incus commands, and controllers without note policy support do not check it. A native `incus start` leaves a stale stop time that retention trusts. `raft config` reports every location's policies and last changes.
55
62
 
56
63
  `raft capacity [--location NAME] [--json]` inspects hosts without modifying state. Text and JSON (`notice`) lead with: "Advisory estimate only. Raft enforces no box count or size budget; create the boxes your task needs." It reports effective CPUs, total and available RAM, saved and active box counts, active resource limits, headroom, free space in the host disk and the shared pool, and for `raft-disks` its data space, reserved and used bytes and metadata use (`boxDiskPool`, `null` when own box disks are not enabled).
57
64
 
@@ -62,14 +69,14 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
62
69
 
63
70
  ## Container lifecycle and execution
64
71
 
65
- - **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.
72
+ - **Stop and resume**: For a persistent box, 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.
66
73
  - **Lifetime extension**: `raft extend --ttl` resets a running box's deadline from current host time without restarting; stopped boxes require `raft resume`.
67
- - **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.
68
- - **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`.
69
- - **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.
74
+ - **Host worker contract**: `doctor`, disposable creation/fork, prune and setting a retention policy require the current worker protocol (2 since 0.6.0, which added retention). `doctor` checks it without running expiry or changing boxes, and reports the failing location. A mismatch fails before executing deletion policy or creating a disposable box; deploy the current checkout explicitly. Installing a new CLI does not update host workers.
75
+ - **Disposable lifecycle**: `new --disposable`, or a configured `disposable` default without `--persistent`, opts a box into deletion on stop or TTL expiry, including its snapshots. Clones and recovered boxes default to persistent whatever the default; fork accepts an explicit `--disposable`.
76
+ - **Manual cleanup**: `prune --older-than DAYS` previews persistent stopped boxes with a recorded stop timestamp. `--yes` deletes matching boxes under the host lifecycle lock. Running boxes and boxes without a recorded stop time are excluded. Automatic deletion of persistent stopped boxes happens only under a host retention policy.
70
77
  - **Resizing**: Resume and fork accept optional CPU/RAM overrides, preserving omitted values. Resume validates stopped state under the lock before changing sizing. Neither changes a box's disk.
71
- - **Expiration cleanup**: The host timer checks expirations every 15 seconds, stopping expired persistent containers and deleting expired disposable containers. Expiry may be delayed by graceful shutdown, long locked transactions, and host outage until startup.
72
- - **Destroy**: Removes the container instance and its snapshots, releasing storage. Destroy confirms absence; SSH failure is not proof of deletion. Disposable boxes created with `new --disposable` are deleted on explicit stop or expiry. Persistent boxes still require explicit destroy for deletion.
78
+ - **Expiration cleanup**: The host timer checks expirations every 15 seconds, stopping expired persistent containers, deleting expired disposable containers and applying stopped-box retention. Expiry may be delayed by graceful shutdown, long locked transactions, and host outage until startup.
79
+ - **Destroy**: Removes the container instance and its snapshots, releasing storage. Destroy confirms absence; SSH failure is not proof of deletion. Disposable boxes created with `new --disposable` are deleted on explicit stop or expiry. Persistent boxes still require explicit destroy, prune or a retention policy for deletion.
73
80
  - **Execution transport**: Incus exec and file APIs travel over authenticated host SSH without a public Incus listener or injected credentials. Interactive SSH runs a PTY over host SSH and Incus exec.
74
81
  - **State inspection and tunnels**: `raft list` and `raft info` read native metadata without guest process counters during shutdown; tunnels use the managed NIC.
75
82
  - **Resource reporting**: `raft usage` reports running cgroup memory, CPU microseconds, CPU affinity, and filesystem space with `filesystemScope`: the shared pool, or `own SIZE box disk`. It provides no billing.
@@ -77,6 +84,23 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
77
84
  - **Directory transfer completion**: The receiver verifies the sender's exact archive byte count before publication. A truncated stream, including one ending between complete entries, is rejected without publishing a partial tree.
78
85
  - **File transfer failures**: Controller write errors return an error before replacing a download destination or publishing a backup archive.
79
86
 
87
+ ## Source sync
88
+
89
+ `sync BOX SOURCE DESTINATION` incrementally copies source into a running guest.
90
+ It requires rsync on the controller and guest. By default, Git selects tracked
91
+ and unignored files and excludes `.git`; submodules are rejected. `--all` opts
92
+ into the entire source tree, including ignored and hidden files. Git filtering
93
+ is not a secret scanner. Missing tracked files are omitted.
94
+
95
+ Matching files are replaced, but guest-only files are never deleted. Checksums
96
+ find changed content even when size and timestamps match. Relative symlinks
97
+ that stay within the source tree are copied; rsync skips unsafe links and
98
+ special files and reports them. Destination must be an absolute non-root path;
99
+ its parent must exist. `--dry-run` reports changes without writing guest files.
100
+ Files are replaced individually through rsync temporary files, not as an atomic
101
+ tree. An interrupted sync can leave some files updated; rerunning converges.
102
+ No host filesystem path is exposed: rsync runs through Incus inside the guest.
103
+
80
104
  ## Security and network isolation
81
105
 
82
106
  - **User namespaces**: Guest root maps to an unprivileged host UID. Container nesting and intercepted syscalls support Docker without mounting the host Docker socket.
@@ -96,7 +120,9 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
96
120
 
97
121
  ## Background jobs and desktop
98
122
 
99
- - **Detached jobs**: Commands run as guest systemd units with journal logging and exit status. Job inspection and cancellation reject unknown IDs in the selected workspace. `raft cancel` stops the unit's control group. Stopping the container terminates jobs.
123
+ - **Detached jobs**: Launching requires a running guest and rejects unavailable guests before allocating a record. Commands run as guest systemd units. A transient host supervisor captures the first 1 MiB of combined stdout/stderr in a root-private host store and records completion. `status` returns JSON with state, exitCode, timestamps and droppedBytes; `logs` returns the retained bytes. A confirmed normal exit records its status; signal termination records 128 plus the signal number. Completed results survive workspace stop, deletion and host reboot. Running jobs do not survive guest stop or host reboot; a stopping guest or missing supervisor is reported as interrupted, even if a shutdown handler returns zero. Cancel stops the guest unit's control group and records cancellation even if its host supervisor has already exited. Cancellation waits at most 120 seconds; a timeout preserves the cancellation request and requires inspection or retry. Unknown IDs and mismatched workspace handles are rejected without starting the guest.
124
+ - **Job supervision bounds**: Host submission is asynchronous and waits at most 10 seconds. A submission timeout preserves the record for status inspection because the supervisor may already be queued. Post-run guest inspection waits at most 60 seconds; an unconfirmed result is interrupted. Host reconciliation waits at most 10 seconds and reports an error without changing job states if systemd does not respond. Failed pre-submission setup removes its incomplete record.
125
+ - **Job retention**: `/var/lib/raft-jobs` holds at most 512 records, with one 1 MiB log per record. Results expire seven days after completion. Commands prune expired records when the store is accessed; an idle host may keep expired files until that next access. Full stores reject new jobs rather than evicting unexpired results. Completed records and captured logs are flushed to disk before publication. Unreadable records remain on the host for inspection and count toward the record limit, but do not block access to other jobs. Logs can contain secrets and stay root-private. No historical guest-journal fallback is provided; jobs created by an older controller must be inspected with that version before upgrading.
100
126
  - **Desktop service**: On-demand Xvfb, Openbox, and noVNC stack on display `:99` without a login manager. Clipboard exchange is available without login-manager delay. Desktop provides full workspace access, not browser confinement. Closing the tunnel leaves the service running.
101
127
 
102
128
  ## Backup and recovery
@@ -105,3 +131,52 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
105
131
  - **Host locking**: Export holds the host lifecycle lock, temporarily delaying expiry checks on the source host.
106
132
  - **Recovery**: `raft recover` imports archives into a stopped container with a fresh name, new MAC address, and cleared TTL. It reads the archive's recorded pool from `backup/index.yaml`: own-disk backups return to `raft-disks` with their size after the reservation check, failing on a host without the pool, and other archives land in `raft-data`. The host note policy applies after the reservation check. Recovery never overwrites existing boxes.
107
133
  - **Archive security**: Archives contain full container filesystems, including credentials. Native archive configuration is not sanitized; backups do not imply scheduled off-host uploads. Store archives privately and import only trusted exports.
134
+
135
+ ## Disposable foreground CI
136
+
137
+ This section specifies the unreleased 0.7.0 development implementation, not
138
+ published 0.6.0. See [release status](./releases/index.mdx).
139
+
140
+ `run` creates a uniquely marked disposable box and establishes its fixed host
141
+ expiry before startup. It streams one foreground command without retrying it,
142
+ optionally transfers a Git-filtered or explicitly complete source tree, collects
143
+ result directories even after payload failure, then checks ownership and deletes
144
+ under the lifecycle lock. The controller handles timeout and cancellation with
145
+ bounded collection/cleanup attempts and reports unconfirmed cleanup as failure.
146
+ Unacknowledged creation is cancelled under the same lock with a private host
147
+ record; a queued creation checks it before allocation. These root-only records
148
+ remain in `/run/raft-ci-cancelled` until reboot so late SSH requests cannot outlive
149
+ their cancellation. Ordinary acknowledged runs leave no record.
150
+ Host expiry is a backstop dependent on the worker and lock, not a strict deletion
151
+ guarantee after transport failure. Existing nonzero payload status is preserved.
152
+ This is not a registered runner, a scoped CI identity or VM-grade isolation.
153
+ See [Disposable CI jobs](./guides/ci-jobs.mdx) for the full contract.
154
+
155
+ ## Structured output and completion
156
+
157
+ `raft --json COMMAND ...` writes only JSON Lines events to stdout. Native
158
+ structured reports use `{"type":"data","value":...}`. Other output uses
159
+ `stdout` or `stderr` events with `encoding: "base64"` and a `data` field so
160
+ arbitrary bytes survive unchanged. Stream chunk boundaries are not lines.
161
+ Expected failures add an `error` event with `code` (`usage` or `operation`) and
162
+ `message`. The final `exit` event contains `code`, matching the process status,
163
+ including payload failures. Local subprocess signal termination becomes
164
+ `128 + signal` in both the exit event and process status. Usage errors return 2;
165
+ top-level operation errors return 1. `run` also emits operation errors for setup,
166
+ artifact collection and cleanup failures, while retaining its existing payload,
167
+ timeout and cleanup exit-status precedence.
168
+ Help and version also end with an exit event. Release notices are disabled in
169
+ JSON mode. `--json` belongs before the command; arguments after a payload's
170
+ `--` belong to that payload. Existing command-local JSON flags keep their
171
+ existing output when global JSON mode is absent.
172
+
173
+ Interactive SSH and foreground tunnels/desktop reject JSON mode before any
174
+ side effect because these commands require terminal or session ownership.
175
+ Other commands retain their existing behavior. JSON events are flushed as
176
+ output arrives; memory use does not grow with payload output.
177
+
178
+ `raft completions bash|zsh|fish` prints a shell integration script without
179
+ reading host configuration. It asks the installed parser for command, option
180
+ and fixed-choice candidates when completing, so added commands and options
181
+ need no second maintained catalog. It does not contact hosts or suggest
182
+ workspace IDs or secret values.
@@ -12,13 +12,13 @@ Launch and manage a workspace after provisioning your host and pinning the image
12
12
  Create a workspace with `raft new`:
13
13
 
14
14
  ```sh title="CONTROLLER: Create a new workspace"
15
- box=$(raft new --location lab --ttl 600)
15
+ box=$(raft new --location lab --ttl 600 --persistent)
16
16
  echo "Created workspace: $box"
17
17
  ```
18
18
 
19
19
  The command returns a location-qualified handle formatted as `<location>:<instance-name>` (for example `lab:rf-a1b2c3d4e5f60718`).
20
20
 
21
- Omitting `--location` targets the first location in `~/.config/raft/incus.json`. The `--ttl` flag sets the lifetime in seconds (default: 600).
21
+ Without a configured location default, omitting `--location` targets the first location in `~/.config/raft/incus.json`. The `--ttl` flag sets the lifetime in seconds (default: 600).
22
22
 
23
23
  ## Step 2: Verify command execution and Docker daemon
24
24
 
@@ -50,37 +50,40 @@ Type `exit` or press `Ctrl+D` to disconnect.
50
50
  Commands run as root in `/workspace` inside an unprivileged Linux user namespace. Container root maps to an unprivileged high UID on the host, sharing the host kernel.
51
51
  </Callout>
52
52
 
53
- ## Step 4: Stop, resume, and destroy
53
+ ## Step 4: Stop and resume
54
54
 
55
55
  Manage the workspace lifecycle:
56
56
 
57
- ```sh title="CONTROLLER: Stop, resume, and destroy workspace"
57
+ ```sh title="CONTROLLER: Stop and resume workspace"
58
58
  # Stop running processes while preserving files and snapshots
59
59
  raft stop "$box"
60
60
 
61
61
  # Resume the stopped workspace with a renewed lifetime
62
62
  raft resume "$box" --ttl 600
63
63
 
64
- # Permanently destroy the workspace and release host capacity
65
- raft destroy "$box"
66
64
  ```
67
65
 
68
66
  - **`raft stop`**: Stops running processes and frees active allocations while retaining files and snapshots.
69
67
  - **`raft resume`**: Starts a stopped container with a new expiration deadline (`now + TTL`). Does not restore process memory.
70
68
  - **`raft destroy`**: Permanently deletes the container, filesystem, and snapshots from the host storage pool.
71
69
 
70
+ ## Clean up when finished
71
+
72
+ <Callout type="warn" title="Permanent deletion">
73
+ Destroy only the tutorial box after copying out anything you need. This removes
74
+ its files and snapshots; it is not the same as stopping it.
75
+ </Callout>
76
+
77
+ ```sh
78
+ raft destroy "$box"
79
+ ```
80
+
72
81
  ## Next steps
73
82
 
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.
77
- - [Docker in workspaces](./guides/docker.mdx): Run nested containers and build images.
78
- - [File transfer](./guides/files.mdx): Upload and download files over SSH.
79
- - [Background jobs](./guides/background-jobs.mdx): Run detached commands under systemd.
80
- - [Desktop and private services](./guides/desktop-services.mdx): Forward guest ports and open noVNC desktop.
81
- - [Snapshots and forks](./guides/snapshots-forks.mdx): Capture disk states and create same-host clones.
82
- - [Backup and recovery](./guides/backup-recovery.mdx): Export portable archives and restore across hosts.
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.
85
-
86
- For temporary work, create with `--disposable`. Stopping or expiry then deletes the box and its snapshots. For persistent boxes, `raft prune --older-than 7` previews old stopped workspaces; add `--yes` only after reviewing the candidates.
83
+ - [Set up a project](./guides/project-setup.mdx).
84
+ - [Choose another task guide](./guides/index.mdx).
85
+ - [Troubleshoot a failed command](./guides/troubleshooting.mdx).
86
+
87
+ This tutorial uses `--persistent` so a disposable controller default cannot delete
88
+ the box on stop. A host retention policy can still delete it after it has been
89
+ stopped long enough; inspect `raft config` before leaving valuable work there.