feat: box export / import — state that survives the box and the host (#70) #78

Merged
dan-claude-bot merged 3 commits from feat/box-export into main 2026-07-18 19:01:57 +00:00

3 commits

Author SHA1 Message Date
dan-claude-bot
b077279850 docs: the upgrade flow speaks export now — the #79 reconciliation
The rebase onto merged #79, with the reconciliation both PRs' bodies
promised: the installer's and the #66 refusals' "copy out by hand (a
portable 'box export' is #70)" placeholders now name the real flow —
down, export (one portable file per box), rm, switch, import. CI keeps
main's versioned layout and uninstall drill, with the #70 round-trip
step ordered before the uninstall (it needs a stack to prove state
survives 'box rm').

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:55:51 +00:00
dan-claude-bot
2bbc53d1dd docs+drill: grok's export nits — --force documented, backups key drilled live
Three of the four round-1 nits, taken: --force's overwrite role on
export now sits in OPTIONS and 'help export' (it was only in the die
text); the restricted-tier rehearsal asserts restricted.backups=allow
beside the snapshots check it mirrors, so the grant key #70 depends on
is proven live, not only grepped; and reset_identity says "instance" —
it has two callers now, and only one of them clones. The install.sh
message stays #79's (the documented merge order).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:55:23 +00:00
dan-claude-bot
1ef3ae739a feat: box export / import — state that survives the box and the host (#70)
'box rm' deletes a box and every snapshot it has; 'box new --from' clones,
but the clone still lives on the same host. Nothing a box held could outlive
a teardown — which made #66's upgrade refusal honest but lossy. This adds
the way out and the way back:

- box export <box> [<file>] [--instance-only]: wraps 'incus export' into one
  portable backup tarball (default <box>-<UTC stamp>.tar.gz), snapshots
  included by default. Requires the box stopped (require_stopped grew an
  honest reason parameter: export is down by OUR decision, not incus's).
  Credentials are SHOUTED, not scrubbed — the artifact carries the box's
  whole disk, and scrubbing a disk image is a promise tarball surgery
  cannot keep.
- box import <file> [--name <box>]: reads the artifact's name from
  backup/index.yaml up front, refuses any name an existing instance holds
  (the resolve_box boundary from the other side), pre-flights the stack
  (require_stack, factored out of cmd_new), imports, then re-stamps the
  HOST's truth onto the artifact's: user.box=1 (legacy tag honored), the
  box-net placement (profile assign, the migrate-host move), fresh volatile
  MACs (imports restore volatile.* verbatim — a re-import beside its
  sibling collided at start with 'MAC address already defined on another
  NIC', measured live on Incus 6.0.4), and reset_identity, exactly like a
  clone.
- restricted tier: box grant now converges restricted.backups allow —
  export rides the backup API, which incus-user's restricted projects block
  by default exactly like snapshots (incus 6.0 permissions.go,
  AllowBackupCreation). Import is plain instance creation and needs no key.
- tests: driven usage errors + fail-closed grep/line-order guards for every
  daemon-gated invariant; CI's rehearsal job now runs a live round-trip
  (mint, write, snapshot, down, export, rm, import, assert the file, the
  snapshot, the tag, the agent, and the collision refusal).

The whole flow was verified against a live Incus 6.0.4 daemon: running-box
refusal, export, overwrite guard, rm, import with and without --name,
re-home onto box-net, sibling re-import with distinct MACs and machine-ids,
pre-export file and snapshot present in both.

Closes #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:55:23 +00:00