@microck/raft 0.6.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 (59) hide show
  1. package/README.md +13 -4
  2. package/deploy/build-image.py +4 -2
  3. package/deploy/verify-controls.py +5 -5
  4. package/deploy/verify-features.py +35 -2
  5. package/deploy/verify-jobs.py +264 -0
  6. package/deploy/verify-limits.py +20 -2
  7. package/deploy/verify-live.py +12 -3
  8. package/deploy/verify-output.py +208 -0
  9. package/deploy/verify-release.py +33 -4
  10. package/deploy/verify-run.py +571 -0
  11. package/deploy/verify-self-update.py +9 -1
  12. package/deploy/verify-sync.py +122 -0
  13. package/docs/benchmarks/read-latency-2026-10-29.json +70 -0
  14. package/docs/boat-capabilities.md +60 -7
  15. package/docs/boat-parity-design.md +1 -0
  16. package/docs/boot-performance.mdx +21 -0
  17. package/docs/contract.md +76 -2
  18. package/docs/first-box.mdx +22 -19
  19. package/docs/guides/background-jobs.mdx +38 -31
  20. package/docs/guides/backup-recovery.mdx +3 -2
  21. package/docs/guides/ci-jobs.mdx +209 -0
  22. package/docs/guides/files.mdx +2 -2
  23. package/docs/guides/index.mdx +31 -0
  24. package/docs/guides/long-running-services.mdx +1 -1
  25. package/docs/guides/meta.json +7 -2
  26. package/docs/guides/project-setup.mdx +1 -1
  27. package/docs/guides/scripting-ci.mdx +28 -9
  28. package/docs/guides/snapshots-forks.mdx +3 -1
  29. package/docs/guides/upgrades.mdx +7 -7
  30. package/docs/index.mdx +26 -28
  31. package/docs/maintainers/index.mdx +18 -0
  32. package/docs/maintainers/meta.json +12 -0
  33. package/docs/meta.json +5 -9
  34. package/docs/operations.mdx +7 -0
  35. package/docs/publishing.md +14 -4
  36. package/docs/reference/capacity.mdx +2 -1
  37. package/docs/reference/cli.md +85 -19
  38. package/docs/reference/meta.json +3 -1
  39. package/docs/releases/0.2.0.md +1 -0
  40. package/docs/releases/0.2.1.md +1 -0
  41. package/docs/releases/0.3.0.md +1 -0
  42. package/docs/releases/0.4.0.md +1 -0
  43. package/docs/releases/0.5.0.md +1 -0
  44. package/docs/releases/0.6.0.md +1 -0
  45. package/docs/releases/0.7.0.md +23 -0
  46. package/docs/releases/index.mdx +21 -0
  47. package/docs/releases/meta.json +15 -0
  48. package/docs/requirements.mdx +1 -1
  49. package/docs/reviews/docs-audit-2026-10-24.md +92 -0
  50. package/docs/reviews/performance-audit-2026-10-29.md +90 -0
  51. package/docs/site-development.md +1 -0
  52. package/docs/verification.mdx +21 -3
  53. package/package.json +3 -1
  54. package/raft.py +380 -179
  55. package/raft_files.py +13 -1
  56. package/raft_jobs.py +390 -0
  57. package/raft_output.py +166 -0
  58. package/raft_run.py +330 -0
  59. package/skills/raft-cli/SKILL.md +78 -197
@@ -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
 
@@ -63,7 +69,7 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
63
69
 
64
70
  ## Container lifecycle and execution
65
71
 
66
- - **Stop and resume**: Stop terminates guest processes and preserves disk contents; stopped boxes retain disk but do not reserve guest RAM. Resume starts the container anew with an explicit TTL without restoring process memory.
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.
67
73
  - **Lifetime extension**: `raft extend --ttl` resets a running box's deadline from current host time without restarting; stopped boxes require `raft resume`.
68
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.
69
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`.
@@ -78,6 +84,23 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
78
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.
79
85
  - **File transfer failures**: Controller write errors return an error before replacing a download destination or publishing a backup archive.
80
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
+
81
104
  ## Security and network isolation
82
105
 
83
106
  - **User namespaces**: Guest root maps to an unprivileged host UID. Container nesting and intercepted syscalls support Docker without mounting the host Docker socket.
@@ -97,7 +120,9 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
97
120
 
98
121
  ## Background jobs and desktop
99
122
 
100
- - **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.
101
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.
102
127
 
103
128
  ## Backup and recovery
@@ -106,3 +131,52 @@ Boxes accept an optional note with a fixed template: `purpose` (required), `owne
106
131
  - **Host locking**: Export holds the host lifecycle lock, temporarily delaying expiry checks on the source host.
107
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.
108
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. A host can also delete boxes left stopped automatically; `raft config` shows each location's `deleteStoppedAfterHours`.
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.
@@ -5,11 +5,11 @@ description: Run detached commands with systemd logging and process management.
5
5
 
6
6
  # Background jobs
7
7
 
8
- Raft runs detached background commands under the guest systemd init system using transient service units.
8
+ Raft runs detached commands under guest systemd and captures their results with a transient host supervisor.
9
9
 
10
10
  ## Target workspace handle
11
11
 
12
- Commands in this guide target a running workspace. Select one from `raft list` (see [First working box](../first-box.mdx)):
12
+ Launching a job requires a running workspace. Select one from `raft list` (see [First working box](../first-box.mdx)):
13
13
 
14
14
  ```sh title="CONTROLLER: Select running workspace"
15
15
  raft list
@@ -33,49 +33,56 @@ The command returns a 32-hex systemd unit name prefixed with `rfcmd-` (such as `
33
33
 
34
34
  ## Checking job status
35
35
 
36
- Use `raft status` to query guest systemd unit properties:
36
+ `raft status "$box" "$job"` returns a retained JSON record:
37
37
 
38
- ```sh title="CONTROLLER: Inspect background job state"
39
- raft status "$box" "$job"
38
+ ```json
39
+ {"job":"rfcmd-...","box":"lab:rf-...","state":"completed","exitCode":23,"startedAt":1793280000,"finishedAt":1793280010,"droppedBytes":0}
40
40
  ```
41
41
 
42
- Active jobs report running substate:
43
- ```text
44
- ActiveState=active
45
- SubState=running
46
- ExecMainStatus=0
47
- ```
48
-
49
- Because Raft runs detached commands with `RemainAfterExit=yes`, completed jobs retain their exit record:
50
- ```text
51
- ActiveState=active
52
- SubState=exited
53
- ExecMainStatus=0
54
- ```
55
-
56
- <Callout type="info" title="Exit status vs CLI return code">
57
- `ExecMainStatus` reports the command payload exit code inside the container. The `raft status` command returns exit code 0 when the query succeeds. It queries unit state and does not report process PIDs or start timestamps.
58
- </Callout>
59
-
60
- ## Viewing job logs
42
+ States are `starting`, `running`, `completed`, `cancelled`, `interrupted` and
43
+ `failed`. `completed` means the command finished, not that it succeeded; inspect
44
+ `exitCode`. An interrupted job has no confirmed payload exit code. A transport interruption can leave the guest command running;
45
+ inspect or cancel it before rerunning the payload. Timestamps
46
+ are host Unix seconds. A successful status query itself returns zero.
61
47
 
62
- Retrieve output recorded in the guest systemd journal:
48
+ ## Viewing retained output
63
49
 
64
- ```sh title="CONTROLLER: Retrieve job log output"
50
+ ```sh
65
51
  raft logs "$box" "$job"
66
52
  ```
67
53
 
68
- The command runs `journalctl --no-pager -o cat -u <job>` inside the container. It prints recorded logs without a pager or stream follow mode.
54
+ A host supervisor retains the first 1 MiB of combined stdout and stderr. Further
55
+ output is drained and counted in `droppedBytes` when the job ends. Logs and final
56
+ status remain readable after the workspace stops or is deleted; keep its exact
57
+ original handle. They are not copied by workspace snapshots or forks.
69
58
 
70
- ## Cancelling jobs
59
+ Results live in the root-private host directory `/var/lib/raft-jobs`. They expire
60
+ seven days after completion. Job commands prune expired records on access, so an
61
+ idle host may retain expired files until the next access. The store holds at most
62
+ 512 records and refuses new jobs when full instead of evicting unexpired results.
63
+ Unreadable records remain on the host for inspection and count toward the limit.
64
+ They do not block other jobs. A query for a damaged record reports its host path;
65
+ inspect that directory and its retained output before deciding how to recover it.
66
+ Output can contain secrets; this is host-local retention, not an off-host backup.
71
67
 
72
- Terminate a running detached command:
68
+ ## Cancelling jobs
73
69
 
74
- ```sh title="CONTROLLER: Terminate background job"
70
+ ```sh
75
71
  raft cancel "$box" "$job"
76
72
  ```
77
73
 
78
- `raft cancel` executes `systemctl stop <job>`, terminating processes in the job's systemd control group.
74
+ Cancellation stops the guest systemd control group. Retained output remains
75
+ available. If a job is still starting and its guest unit is not yet registered,
76
+ cancellation reports an error; inspect its state before retrying. Unknown job
77
+ IDs and IDs belonging to another workspace are rejected. Cancellation waits up to
78
+ 120 seconds. If it times out, the cancellation request remains recorded; inspect
79
+ the job and retry cancellation.
80
+
81
+ Host reboot or guest shutdown interrupts active jobs. The next inspection marks
82
+ an absent supervisor as interrupted rather than assuming success. Completed
83
+ results survive host reboot. This development implementation has no fallback to
84
+ old guest-only journals; inspect old jobs with their controller version before
85
+ upgrading.
79
86
 
80
87
  ## Extending workspace lifetime
81
88