rusta-cli 1.3.0

macOS arm64 CLI for creating and managing Ubuntu VMs on Tart
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
# `rusta` — Feature Specification

`rusta` is a macOS-only CLI for creating and managing Ubuntu VMs on Apple
Silicon using [Tart](https://tart.run/). It is the spiritual successor to
`ubuntu-tart-vm.sh`, but exposes its features through a subcommand-based UX
rather than a single mega-script.

This document specifies behavior; it does not prescribe implementation details
beyond what is required for parity with the existing script.

---

## 1. Runtime requirements

| Requirement       | Detail                                                                 |
| ----------------- | ---------------------------------------------------------------------- |
| Host architecture | `arm64` (Apple Silicon). Any other `uname -m` aborts at startup.       |
| Host OS           | macOS (relies on Tart + Apple Virtualization.framework).               |
| Required CLIs     | `brew` (must be present). `tart` and `sshpass` are auto-installed.     |
| Optional CLIs     | `docker` (host-side, installed automatically for `docker-setup`).      |
| Network           | Outbound HTTPS to `ghcr.io` (OCI image pulls and `rusta versions`).    |

Auto-installed via Homebrew on demand:

- `tart` from tap `cirruslabs/cli/tart`
- `sshpass` (whenever SSH-based steps are needed)
- `docker` (only for `rusta docker-setup`)

---

## 2. Command surface

```
rusta <command> [args...] [flags...]
```

### 2.1 Global flags

Accepted by every subcommand:

| Flag             | Default | Description                                            |
| ---------------- | ------- | ------------------------------------------------------ |
| `--verbose`      | off     | Verbose logging (equivalent to `set -x` + `LogLevel=INFO` on SSH). |
| `--log <file>`   || Tee all stdout/stderr to the given file.               |
| `--help`, `-h`   || Print help for the current (sub)command and exit 0.    |

`rusta` (no args) and `rusta --help` print top-level help and exit 0.

### 2.2 Subcommand summary

| Subcommand                       | Purpose                                                       |
| -------------------------------- | ------------------------------------------------------------- |
| `rusta up [<vm>]`                | Start a VM (headless by default).                             |
| `rusta down [<vm>]`              | Gracefully shut down a VM (`--force` to hard-stop).           |
| `rusta create [<vm>]`            | Create + provision a new Ubuntu VM.                           |
| `rusta delete <vm>`              | Delete a VM (Tart state). Requires confirmation or `--yes`.   |
| `rusta list`                     | List Tart VMs and indicate the current default.               |
| `rusta versions`                 | List available OCI tags across configured sources × images.   |
| `rusta source [add\|rm\|move]`   | Manage the image sources images are cloned from (see §4.12).   |
| `rusta image [add\|rm\|move]`    | Manage the image names (repos) cloned under each source (§4.13).|
| `rusta default [<vm>]`           | Print or set the default VM.                                  |
| `rusta ip [<vm>]`                | Print the guest IP of the VM.                                 |
| `rusta ssh [<vm>] [-- cmd...]`   | Open an SSH session (or run a command) on the VM.             |
| `rusta docker-setup [<vm>]`      | Install Docker in the VM and wire host SSH/Docker context.    |
| `rusta ssh-copy [<vm>]`          | Copy host `~/.ssh/id_*` and `*.pem` into the VM.              |

All subcommands that take `<vm>` accept it as a positional. If omitted, the
**default VM** is used (see §3).

### 2.3 Exit codes

| Code | Meaning                                                                       |
| ---- | ----------------------------------------------------------------------------- |
| 0    | Success (including `--help`, `versions`, no-op when target already in desired state). |
| 1    | Bad usage, unmet prerequisite, validation failure, or runtime error.          |
| 2    | VM not found (when an explicit name was given or default resolution failed).  |

---

## 3. The "default VM" concept

`rusta` maintains a single host-side **default VM** name so that `up`,
`down`, etc. can be invoked without arguments.

### 3.1 State file

- Location: `~/.local/share/rusta/state.toml` (parent dir auto-created).
- Schema:
  ```toml
  default_vm = "ubuntu-2404"

  [vms.ubuntu-2404]
  gui = false

  [[sources]]
  registry = "ghcr.io/cirruslabs"

  [[sources]]
  registry = "ghcr.io/pallewela"

  images = ["ubuntu", "ubuntu-desktop"]
  ```
  `vms.<name>.gui` records the `--gui` choice from `rusta create`. Used
  by `rusta up` to pick the default boot mode (§4.1). VMs created before
  this feature have no `[vms.<name>]` entry and default to headless boot.
  `[[sources]]` is the ordered list of image sources (§4.12); an absent or
  empty list means the seeded `ghcr.io/cirruslabs` default.
  `images` is the ordered list of image names (§4.13); an absent or empty
  list means the seeded `["ubuntu", "ubuntu-desktop"]` defaults. The first
  image (`ubuntu`) is the `create` default.

### 3.2 Resolution rule

When a subcommand needs a VM name and none is given on the command line:

1. If `default_vm` is set in `state.toml` **and** that VM exists in
   `tart list`, use it. Done.
2. If `default_vm` is unset (or names a VM that no longer exists), enumerate
   `tart list`:
   - **Zero VMs** → exit 2 with a hint to `rusta create`.
   - **One or more VMs****interactively prompt** the user to choose one.
     The chosen VM is persisted as `default_vm` before the command proceeds.
3. There is **no hardcoded fallback** (no implicit `ubuntu-2404`).

If stdin is not a TTY (non-interactive context, e.g. CI), the prompt cannot
run; instead `rusta` exits 2 with a message instructing the caller to pass
the VM explicitly or run `rusta default <vm>` first.

### 3.3 The interactive picker

Triggered by §3.2 step 2 when more than one VM exists, or as a confirmation
when exactly one exists:

```
No default VM is set. Pick one:
  1) ubuntu-2404   (stopped)
  2) lab-22        (running)
> 1
Set 'ubuntu-2404' as default for future commands.
```

- Lists all Tart VMs with their current status; selection by number.
- An empty answer or Ctrl-C aborts the command with exit 1 and does **not**
  write state.
- The chosen VM is written to `state.toml` immediately, before the original
  subcommand's work begins.

### 3.4 How the default gets set

The default is set only by these explicit paths — never as a side effect of
`create`, `up`, `down`, etc. when the VM is named on the command line:

- **The interactive picker** (§3.3), the first time the user runs a command
  that needs a default while none is set.
- **`rusta default <vm>`** — explicit set. Exits 2 if `<vm>` does not exist.

Notes:

- `rusta create <vm>` and `rusta create` (which interactively prompts for
  a name — see §4.3) both **leave the default untouched**. The next
  argument-less command that needs an existing VM triggers the picker.
- `rusta default` with no argument prints the currently-set default, or
  prints "no default set" and exits 1 if none is set. It never prompts.
- `rusta delete <vm>` clears the default if it pointed at the deleted VM.

---

## 4. Subcommand details

### 4.1 `rusta up [<vm>] [--graphical|-G|--graphics|--gui] [--no-gui|--no-graphics]`

Boot a VM.

- Resolves `<vm>` per §3.
- If the VM is already running, prints `[skip]` and exits 0.
- Boot mode follows the VM's `create`-time choice: VMs created with
  `--gui` boot with a graphics window; all others boot headless
  (`tart run <vm> --no-graphics`). VMs created before this feature have
  no recorded preference and default to headless.
- `--graphical` (aliases: `-G`, `--graphics`, `--gui`): force a graphics
  window, regardless of the recorded preference.
- `--no-gui` (alias: `--no-graphics`): force headless boot, even for
  GUI-enabled VMs.
- `--graphical` and `--no-gui` are mutually exclusive.
- Backgrounded with the PID written to `~/.local/share/rusta/run/<vm>.pid`
  so subsequent commands can find it.
- Waits for the **tart guest agent** (`tart exec <vm> true`, poll 2s × 60).
- Prints the guest IP once available (best-effort; not fatal if delayed).
- Does **not** re-run provisioning; that only happens during `create`.

### 4.2 `rusta down [<vm>] [--force] [--timeout <secs>]`

Stop a VM.

- Resolves `<vm>` per §3.
- If the VM is already stopped, prints `[skip]` and exits 0.
- **Graceful (default):** issues `sudo shutdown -h now` via `tart exec`,
  then waits up to `--timeout` seconds (default **60s**) for the `tart run`
  process to exit. If the timeout expires without a clean stop, exit 1 with
  a hint to retry with `--force`.
- **`--force` (alias `-f`):** skip the graceful path; call `tart stop <vm>`
  (or kill the recorded PID and fall back to `tart stop` if needed). Exit 1
  only if the VM is still running after the operation.
- Removes the stale `~/.local/share/rusta/run/<vm>.pid` on success.

### 4.3 `rusta create [<vm>] [flags]`

Clone + provision a new Ubuntu VM.

Flags:

| Flag                    | Default          | Description                                                                   |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------- |
| `--version <ver>`       | `24.04`          | Ubuntu release line (OCI tag); resolved against configured sources (§4.12).   |
| `--image <name>`        | first image      | Image family (repo) to clone, e.g. `ubuntu-desktop`; defaults to the first configured image (§4.13). Composes with `--source`. |
| `--gui [pkg]`           | off / `ubuntu-desktop` | Install a desktop. Allowed: `ubuntu-desktop`, `xubuntu-desktop`, `lubuntu-desktop`, `lightdm`. |
| `--cpus <n>`            | `6`              | CPU count.                                                                    |
| `--memory <mb>`         | `8192`           | Memory in MB.                                                                 |
| `--disk <gb>`           | `80`             | Disk size in GB.                                                              |
| `--user <username>`     | `admin`          | Guest login username (image-dependent).                                       |
| `--password <password>` | `admin`          | Guest login password used by `sshpass`.                                       |
| `--ssh-copy-keys`       | off              | After provisioning, copy host SSH keys into the guest (see §4.10).            |
| `--debug-no-headless`   | off              | Run with a graphics window during provisioning (debug only).                  |
| `--source <registry>`   | (all sources)    | Pin resolution to one configured source, by registry prefix or label (§4.12). |
| `--image-ref <ref>`     | (unset)          | Clone this exact image reference verbatim, bypassing source + image resolution. Conflicts with `--source` and `--image`. |

Positional `<vm>` is the VM name. **`rusta create` never assumes a name**:
the default-VM mechanism (§3) does not apply, since `create` is producing a
new VM, not selecting an existing one. If `<vm>` is omitted:

- If stdin is a TTY, **interactively prompt** for the name, offering
  `<image>-<UBUNTU_VERSION_NODOT>` (e.g. default image + `--version 22.04`
  → `ubuntu-2204`; `--image ubuntu-desktop` → `ubuntu-desktop-2204`)
  as a suggested default the user can accept with an empty line:
  ```
  VM name [ubuntu-2404]:
  ```
  Ctrl-C or EOF aborts with exit 1 and creates nothing.
- If stdin is **not** a TTY, exit 1 with a message instructing the caller
  to pass the VM name on the command line. `create` never proceeds with a
  silently-synthesized name.

Name must match `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`.

Behavior:

1. Validate platform/prereqs (arm64, brew, tart auto-install).
2. Resolve the VM name per the rule above (explicit arg or interactive
   prompt). The chosen name is **not** written to `state.default_vm`.
3. If the VM name already exists, **skip creation** and print a recreate
   hint (`rusta delete <vm> && rusta create <vm> ...`); no re-provisioning.
4. Otherwise:
   - Determine the **image** (§4.13): `--image <name>` if given, else the first
     configured image (`ubuntu` by default).
   - Resolve the image reference to clone (§4.12): `--image-ref` verbatim, else
     the first configured source (in priority order) that advertises
     `<image>:<version>`.
   - `tart clone <resolved-ref> <vm>` (e.g. `ghcr.io/cirruslabs/ubuntu:<version>`).
   - `tart set <vm> --cpu <n> --memory <mb> --disk-size <gb>`.
   - Generate `~/.local/share/rusta/provision/<vm>.sh` (kept for debugging).
   - Boot headlessly (or with window under `--debug-no-headless`).
   - Wait for guest agent; upload + execute provisioning script via
     `tart exec`; shut down cleanly. See §5 for the provisioning behavior.
5. **Does not** modify `state.default_vm` (see §3.4) — even when the name
   came from the interactive prompt.
6. If `--ssh-copy-keys`, run the `ssh-copy` flow against the new VM (§4.10),
   which transiently boots it again.

### 4.4 `rusta delete <vm> [--yes]`

Remove a VM from Tart's storage.

- Requires explicit `<vm>` (no default-VM fallback — too destructive to
  silently delete the default).
- Refuses to run if the VM is currently running (suggests `rusta down`
  first); `--force-running` to stop+delete in one shot.
- Prompts for confirmation unless `--yes` (`-y`) is given.
- Clears `state.default_vm` if it pointed at this VM.

### 4.5 `rusta list`

Print a table of all Tart VMs:

```
NAME          STATUS    DEFAULT
ubuntu-2404   running   *
lab-22        stopped
```

The `DEFAULT` column shows `*` next to the resolved default. Exits 0 even
if there are no VMs.

### 4.6 `rusta versions [--source <registry>] [--image <name>]`

List the available OCI tags across the configured **sources × images** matrix
(§4.12, §4.13). For each `(source, image)` cell's `<registry>/<image>` repository:

1. Fetch an anonymous pull token from `<host>/token`.
2. List tags from `<host>/v2/<namespace>/<image>/tags/list`.
3. Filter to tags matching `^\d+\.\d+$`.

Then merge, sort ascending, and print one per line, highlighting `24.04` as
`(default)`.

- With a **single** source **and** a single image, output is unannotated (one
  tag per line) — the legacy format.
- With multiple sources but a single image, each tag is annotated with the
  providing source label(s); when more than one source offers a tag, the one
  `create` would pick (first in priority order) is noted (`(create uses <label>)`).
- With **multiple images**, each tag line groups providers by image (in image
  priority order): `<image>: <src>, <src>  (create uses <src>)`. An image with
  no provider for a tag is omitted from that line.
- `--source <registry>` limits output to a single configured source (by registry
  prefix or label); `--image <name>` limits output to a single image. They compose.
- A `(source, image)` cell where the source **host is unreachable** is skipped
  with a warning. A cell where the source simply **lacks that image** (e.g. a 404)
  is a silent empty cell, not a failure. `versions` exits 1 only when **no** cell
  produced any result.

### 4.7 `rusta default [<vm>]`

- No arg: print the resolved default VM, or "no default set" + exit 1.
- With arg: set `state.default_vm = <vm>` (exit 2 if `<vm>` does not exist).

### 4.8 `rusta ip [<vm>]`

Print `tart ip <vm>` (waits up to 60s). Exit 1 if no IP is obtained.

### 4.9 `rusta ssh [<vm>] [-- cmd args...]`

- Resolves `<vm>` per §3.
- If the VM is not running, exits 1 (does **not** auto-`up`; suggest
  `rusta up <vm>`). Alternative: `--auto-up` flag to boot first.
- Connects via `sshpass -p <password> ssh <user>@<ip>` using the SSH options
  from §6.2.
- Anything after `--` is executed as a remote command; otherwise an
  interactive shell.

### 4.10 `rusta ssh-copy [<vm>]`

Copy host `~/.ssh/id_*` and `*.pem` files into the guest's `~/.ssh/`.

- Resolves `<vm>` per §3.
- Boots the VM if not running; shuts it back down at the end (only when
  `rusta` started it itself — same "started_by_us" pattern as today).
- Verifies host has `~/.ssh`; otherwise exit 1.
- Collects regular files matching `~/.ssh/id_*` and `~/.ssh/*.pem`. If
  none, prints `[skip]` and exits 0.
- Inside guest: `mkdir -p ~/.ssh && chmod 700 ~/.ssh`; `scp` the files;
  normalize permissions (`*.pub` → 644, others → 600; `chmod 700 ~/.ssh`).

### 4.11 `rusta docker-setup [<vm>]`

Install Docker Engine inside an existing VM and wire host-side
`docker context` + `~/.ssh/config` alias.

- Resolves `<vm>` per §3.
- Ensures host has `sshpass` and `docker` CLI (auto-install via Homebrew).
- Boots the VM if not running; shuts it back down at the end if started by
  `rusta`.
- Generates `~/.ssh/id_ed25519` (empty passphrase) if missing.
- `ssh-copy-id` the public key into the guest (password auth).
- Inside the guest, installs Docker via `curl -fsSL https://get.docker.com | sudo sh`
  **only if** `docker` is absent. Adds `$USER` to the `docker` group if not
  already a member. `systemctl enable --now docker`.
- On host: idempotently appends a `Host docker-<vm>` block to
  `~/.ssh/config` (pinned to the observed IP, `IdentitiesOnly yes`, strict
  host-key checking disabled). `chmod 600 ~/.ssh/config`.
- Idempotently creates a Docker context `docker-<vm>` pointing at
  `ssh://<user>@docker-<vm>`.
- Prints a summary including the SSH alias, the context name, the
  three-step usage hint, and the IP-pinning caveat.

### 4.12 `rusta source [list | add <registry> | rm <registry> | move <registry> <pos>]`

Manage the **image sources** that `create` and `versions` consider. A source is a
registry host + namespace prefix (e.g. `ghcr.io/cirruslabs`); rusta appends the
selected image name (§4.13, `ubuntu` by default) to form the repository. Sources
are an **ordered list** stored in the state file (§3.1) under `[[sources]]`; list
position is **priority**.

Model and resolution rules:

- **Seeded default.** When no sources are configured, rusta behaves as if a single
  source `ghcr.io/cirruslabs` were present, preserving prior single-source
  behavior. The default is materialized into the state file on the first mutation.
- **Conflict rule.** When a requested version exists in more than one source, the
  first source in priority (config) order wins.
- **`create` resolution.** With a single candidate source, the reference is built
  directly with no registry query. With two or more, each source's tags are
  queried (token + tag-list, as in §4.6) and the first advertising the version
  wins; unreachable sources are skipped with a warning. If no reachable source
  offers the version, `create` errors (exit 1) and creates nothing.
- **ghcr.io only (v1).** `add` accepts only `ghcr.io` hosts; other registries are
  rejected. `tart clone` itself works with any registry via `--image-ref`, but tag
  listing/aggregation is ghcr-specific for now.

Subactions:

- `list` (default when no subaction) — print configured sources in priority order;
  notes when the built-in default is in effect.
- `add <registry>` — validate + normalize (`<host>/<namespace>`, optional trailing
  `/ubuntu` stripped, no tag) and append. Duplicates are a no-op. First add
  materializes the seeded `cirruslabs` default ahead of the new entry.
- `rm <registry>` — remove by registry prefix; exit 2 if absent. Removing the last
  remaining source re-seeds the default (rusta is never sourceless).
- `move <registry> <pos>` — move a source to 1-based priority position `<pos>`
  (clamped); exit 2 if absent.

Sources are host-independent of Tart/Apple Silicon — `rusta source` skips the
arm64/brew/tart preflight.

### 4.13 `rusta image [list | add <name> | rm <name> | move <name> <pos>]`

Manage the **image names** (repositories) that rusta clones under each source. An
image is a single OCI repository segment (e.g. `ubuntu`, `ubuntu-desktop`); the
namespace comes from the source (§4.12). Images are a **global, ordered list**
stored in the state file (§3.1) under `images`; list position is **priority** and
the **first image is the `create` default**.

Model and resolution rules:

- **Seeded defaults.** When no images are configured, rusta behaves as if the
  images `ubuntu` and `ubuntu-desktop` were present (in that order), with `ubuntu`
  as the `create` default. The defaults are materialized into the state file on the
  first mutation.
- **Global, not per-source.** The list applies to every source. At `create` time
  the selected image is searched across sources (so `create` makes the same number
  of registry queries as before — the image is fixed before resolution). A source
  that does not host the selected image is treated like a source that lacks the
  version (skipped). `versions` walks the full source × image matrix.
- **Selection.** `rusta create` clones the first configured image unless `--image
  <name>` overrides it. `--image` need not be in the configured list (any valid
  name is accepted); `--image-ref` bypasses image resolution entirely.

Subactions:

- `list` (default when no subaction) — print configured images in priority order,
  marking the first as `(default)`; notes when the built-in default is in effect.
- `add <name>` — validate (single segment: no `/`, no `:` tag, lowercase OCI
  grammar) and append. Duplicates are a no-op. First add materializes the seeded
  defaults ahead of the new entry.
- `rm <name>` — remove by name; exit 2 if absent. Removing the last remaining image
  re-seeds the defaults (rusta is never imageless).
- `move <name> <pos>` — move an image to 1-based priority position `<pos>`
  (clamped); exit 2 if absent.

Like `rusta source`, `rusta image` skips the arm64/brew/tart preflight.

---

## 5. Provisioning (used by `rusta create`)

Behavior of the per-VM provisioning script is unchanged from the existing
implementation:

- Persists output to `/var/log/provision.log` inside the guest.
- Sets `DEBIAN_FRONTEND=noninteractive`, `DEBCONF_NONINTERACTIVE_SEEN=true`,
  `NEEDRESTART_MODE=l`, `LC_ALL=C.UTF-8`, `LANG=C.UTF-8`.
- Stops `unattended-upgrades` and `apt-daily{,-upgrade}.{service,timer}`
  and waits for the dpkg/apt lock (cap ~10 minutes).
- **Per-release apt cache fix:** for releases known to ship with stale ARM64
  apt cache files under `<codename>-updates` / `<codename>-security`, remove
  those files before `apt-get update` to avoid dependency-resolution failures.
  Currently applied to:
  - `24.04` (codename `noble`) — paths
    `/var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_noble-{updates,security}_main_binary-arm64_Packages`.
  - `26.04` (codename per release) — same pattern, codename substituted.

  The mapping is data-driven: adding another affected release means adding
  a `{version, codename}` entry, not new code.
- Installs `apt-fast` (via PPA `ppa:apt-fast/stable`) for parallel apt.
- Always installs: `spice-vdagent`, `spice-webdavd`, `curl`, `wget`, `git`.
- Starts `spice-vdagent.socket` and `spice-vdagent.service` (best-effort).

When `--gui` is set:

- Before installing the desktop, pre-creates
  `/etc/NetworkManager/conf.d/10-manage-all.conf` with
  `unmanaged-devices=none`, so NetworkManager takes over from
  systemd-networkd cleanly.
- Installs the desktop meta-package and matching display manager:

  | `--gui` value     | Display manager |
  | ----------------- | --------------- |
  | `ubuntu-desktop`  | `gdm3`          |
  | `xubuntu-desktop` | `lightdm`       |
  | `lubuntu-desktop` | `sddm`          |
  | `lightdm`         | `lightdm`       |

- Restarts NetworkManager, disables
  `systemd-networkd-wait-online.service`, sets default target to
  `graphical.target`, enables the display manager, and suppresses the
  GNOME initial-setup wizard via `~/.config/gnome-initial-setup-done`.

---

## 6. Cross-cutting behavior

### 6.1 Polling timeouts

| Wait                    | Cadence | Cap        |
| ----------------------- | ------- | ---------- |
| Tart IP discovery       | 2s × 60 | ~2 min     |
| SSH readiness           | 3s × 40 | ~2 min     |
| Tart guest agent ready  | 2s × 60 | ~2 min     |
| Guest dpkg/apt lock     | 5s × 120| ~10 min    |
| `rusta down` grace      | 1s × `--timeout` (default 60) | configurable |

All timeouts are fatal on expiry (except graceful `down`, which suggests
`--force`).

### 6.2 SSH options

Used everywhere `rusta` shells into the guest:

```
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
PubkeyAuthentication=no     # password auth is the default; ssh-copy-id flips this on a per-VM basis
LogLevel=ERROR              # INFO under --verbose
ConnectTimeout=10
ServerAliveInterval=30
ServerAliveCountMax=120
```

### 6.3 Process tracking

Background `tart run` processes started by `rusta` write their PID to
`~/.local/share/rusta/run/<vm>.pid`. `rusta down`, `delete`, and the
auto-shutdown tails in `ssh-copy` / `docker-setup` consult this file to
reap the right process. A signal trap kills + reaps the process on
`EXIT|INT|TERM` while `rusta` is the owner.

### 6.4 Logging and output conventions

- TTY-aware ANSI coloring (bold/green/yellow/red/cyan); collapses to empty
  strings when stdout is not a TTY.
- Prefixes: `==>` (info, cyan/bold), `[ok]` (green), `[skip]` (yellow),
  `[error]` (red, to stderr).
- `--log <file>` tees the entire run (including provisioning) to the file.

---

## 7. Filesystem and host-side artifacts

| Path                                            | Purpose                                                       |
| ----------------------------------------------- | ------------------------------------------------------------- |
| `~/.tart/vms/`                                  | VM storage (managed by Tart).                                 |
| `~/.tart/cache/`                                | OCI image cache (managed by Tart).                            |
| `~/.local/share/rusta/state.toml`               | Persistent `rusta` state (default VM, etc.).                  |
| `~/.local/share/rusta/provision/<vm>.sh`        | Generated provisioning script (kept after run for debugging). |
| `~/.local/share/rusta/run/<vm>.pid`             | Tracked PID of a `rusta`-launched `tart run`.                 |
| `~/.ssh/id_ed25519` / `.pub`                    | Auto-generated by `docker-setup` if absent.                   |
| `~/.ssh/config`                                 | Appended with `Host docker-<vm>` block by `docker-setup`.     |
| `<--log file>`                                  | Tee of stdout+stderr when `--log` is given.                   |

Inside the guest:

| Path                                                              | Purpose                            |
| ----------------------------------------------------------------- | ---------------------------------- |
| `/tmp/provision.sh`                                               | Uploaded provisioning script.      |
| `/var/log/provision.log`                                          | Full provisioning output log.      |
| `/etc/NetworkManager/conf.d/10-manage-all.conf` (gui only)        | Forces NM to manage all devices.   |
| `~/.config/gnome-initial-setup-done` (gui only)                   | Suppresses GNOME welcome wizard.   |

---

## 8. Idempotency

- `rusta up` on a running VM → `[skip]`.
- `rusta down` on a stopped VM → `[skip]`.
- `rusta create` with an existing name → `[skip]` + recreate hint; no
  re-provisioning, no resource change.
- `rusta default <vm>` is a pure state write.
- `rusta docker-setup` re-runs are safe: SSH key creation, `~/.ssh/config`
  block, and `docker context` are each guarded by existence checks.
- `rusta ssh-copy` re-runs overwrite the copied files but leave permissions
  correct.

---

## 9. Non-goals

- Non-Ubuntu-family guests; non-OCI Tart images; non-`ghcr.io` sources.
- Per-source image lists and per-image attributes (provisioning, default
  resources): images are a single global list in v1.
- Architectures other than `arm64`.
- Post-creation VM resize (CPU/memory/disk are set once at `create` time).
- Snapshot, suspend/resume, export, or registry-push workflows.
- Multi-VM batch operations.
- Windows or x86_64 Linux hosts.

---

## 10. Behavioral checklist

A working `rusta` should pass each of these end-to-end:

1. `rusta` (no args) → top-level help, exit 0.
2. `rusta --help` and `rusta <cmd> --help` → command-specific help, exit 0.
3. `rusta versions` → lists tags from ghcr.io, `24.04` flagged `(default)`.
4. `rusta create` (interactive, TTY stdin) → prompts `VM name [ubuntu-2404]:`;
   accepting the suggestion creates `ubuntu-2404` with 6 CPU / 8 GB / 80 GB,
   boots, provisions SPICE tools, shuts down. Non-TTY stdin → exits 1
   without creating anything, instructing the caller to pass the VM name.
   `state.default_vm` is **unchanged** in both branches.
5. `rusta create --version 22.04 lab` → creates `lab` from `:22.04`.
   `state.default_vm` is **unchanged**, even when a different default is set.
6. `rusta create --gui` / `--gui xubuntu-desktop` → installs the matching
   desktop and display manager with the NetworkManager workaround. Works
   for **every** Ubuntu version exposed by `rusta versions`, including
   24.04 and 26.04 (both apply the per-release apt cache fix from §5).
7. `rusta create lab --ssh-copy-keys` → after provisioning, transiently
   boots `lab`, copies host `id_*`/`*.pem` files, shuts it down.
8. `rusta up` (no arg, no default set, ≥1 VM exists) → interactive picker
   appears; chosen VM is written to `state.toml` then booted. Re-running
   `rusta up` with no arg now goes straight to that VM.
9. `rusta up` (no arg, no default set, 0 VMs exist) → exit 2 with a hint to
   `rusta create`.
10. `rusta up` (no arg, no default set, non-TTY stdin) → exit 2 without
    prompting; message instructs the caller to pass a VM or run `rusta default`.
11. `rusta up` (no arg, default is set and exists) → boots the default
    headlessly; second invocation is a `[skip]`.
12. `rusta up lab` (explicit name) → boots `lab` regardless of default;
    `state.default_vm` is **unchanged**.
13. Boot-mode defaults track the `create`-time `--gui` choice:
    - `rusta create lab` then `rusta up lab` → boots headless.
    - `rusta create lab --gui` then `rusta up lab` → boots with a
      graphics window with no explicit flag.
    - `rusta up lab --no-gui` (or `--no-graphics`) on a GUI-enabled VM →
      boots headless for that invocation.
    - `rusta up lab --graphical` (or `-G` / `--graphics` / `--gui`) on a
      headless VM → boots with a graphics window for that invocation.
    - `rusta up lab --graphical --no-gui` → exits 1 (mutually exclusive).
14. `rusta down` → graceful shutdown of the default VM within 60s; second
    invocation is `[skip]`. Picker triggers if no default is set.
15. `rusta down lab --force` → hard-stops `lab` even if guest agent is
    unresponsive.
16. `rusta down --timeout 5` → if the guest does not stop within 5s, exits
    1 with a "retry with --force" hint.
17. `rusta list` → tabular VM listing with `*` next to the default (if any).
18. `rusta default` (no arg, none set) → prints "no default set" + exit 1
    (no prompt).
19. `rusta default lab` → sets default to `lab`; exits 2 if `lab` is unknown.
20. `rusta delete lab` → prompts; with `--yes` deletes without prompt;
    clears default if it pointed at `lab`.
21. `rusta ip` / `rusta ip lab` → prints the guest IP.
22. `rusta ssh lab` → interactive SSH session (after `rusta up lab`).
23. `rusta ssh lab -- uname -a` → runs the remote command and exits.
24. `rusta ssh-copy` / `rusta ssh-copy lab` → copies host SSH keys with
    correct permissions, idempotent on re-run.
25. `rusta docker-setup` / `rusta docker-setup lab` → installs Docker,
    writes SSH alias + Docker context, idempotent on re-run.
26. `rusta --verbose <any>` → verbose logging.
27. `rusta --log /tmp/x.log <any>` → entire run tee'd to the file.
28. Non-arm64 host → exit 1 before any Tart calls.
29. Missing `brew` → exit 1.
30. VM-not-found (explicit name) → exit 2 with a clear message.