Commit graph

7 commits

Author SHA1 Message Date
dan-claude-bot
8598cf96fe fix: grant the 'incus' membership to incus-admin members too (#101 review)
The previous revision skipped `usermod -aG incus` for an incus-admin member,
reasoning that 'incus' is a strict subset of what incus-admin already opens.
That is true of the daemon API and false of the filesystem. On Debian 13 /
Incus 6.0.4 the two sockets are two files with two owning groups:

  /var/lib/incus/unix.socket       group incus-admin  0660
  /var/lib/incus/unix.socket.user  group incus        0660

incus-admin opens the first and not the second, and only the second
provisions a user-<uid> project. So for the incus-admin-ONLY user — the
canonical #99 case — the pinned provisioning touch took EACCES, the `|| true`
swallowed it, no project appeared, and the grant died blaming a healthy
incus-user. Both reviewers converged on this independently and were right.

The membership is now granted for everyone, with output carrying the concern
the old no-op was built around (it is the key to a file, not a privilege;
box_tier still reads them as admin). Everything downstream moves with it:

- the backout rolls that membership back and verified, while refusing to call
  the rollback a lockout — incus-admin is untouched and still opens the host
- revoke's bare path takes the membership back and reports `partial:` instead
  of "no-op, nothing was taken", still declining to call them "out"
- grant's closing "gpasswd -d <user> incus-admin (no re-grant needed)" is now
  a true promise: they keep 'incus', so the drop lands them in their project
- the socket existence probe goes through $SUDO, matching revoke's measured
  discipline about /var/lib/incus lying to a non-root admin

Tests: the cli.sh assertions that encoded the old no-op design are flipped and
the decision is pinned at the seam that broke; the sudo shim now runs `test`
for real in both directions. Because the shims model neither INCUS_SOCKET nor
permissions and so cannot reproduce the EACCES, drill/multiuser.sh gains
criterion (o): an incus-admin-only member granted on real Incus in CI, with
the membership, the project, a live connect() to unix.socket.user, and the
post-drop landing all measured.

Mutation-checked: 11 of the new/flipped assertions fail against the previous
implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:33:37 +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
dan-claude-bot
34bf39be86 docs: the versioned install — upgrade and uninstall as first-class flows
README's install/upgrade/uninstall sections rewritten for the versioned
layout (side-by-side installs, 'box use' flips, 'box uninstall' replaces the
rm -rf prose); CHANGELOG Unreleased entries; the plan doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:02:37 +00:00
dan-claude-bot
8e6f3a4bb8 grant/rehearsal: the codex round — verified rollback, loud partial states, and the raw-attach guarantee measured (#75)
Review 4727756972 (A2): the backout no longer trusts gpasswd — it re-reads
the live group database after removal; verified-absent gets the safe
message, anything else screams ROLLBACK INCOMPLETE, exits nonzero, and
names the exact remediation. The concurrent-login window (a session begun
between usermod and backout keeps the group) is CLOSED to the extent the
database can't reach: the backout detects live processes and names
loginctl terminate-user, and the success wording claims only what was
verified.

Review 4727641752 (A1): a failed grant for a user whose membership predates
the run (the hand-added-user scenario) now fails LOUDLY — they retain
socket access on part-converged policy, and the message says so with both
remediations (box revoke now, or fix and re-run). Their membership is not
stripped: breaking a working user over a failed re-grant is its own hazard.
The default-profile eth0 removal is deliberately not restored on failure —
that mutation only reduces capability, and restoring it would move the
failure state AWAY from fail-closed. Injected-failure coverage is criterion
(n), both flavors: fresh-user backout (fault at the LAST mutation, so the
rollback runs after every earlier one) with the group's absence verified
and a converging re-run; blocked narrowing staged for real with an
instance-local NIC parked on the private bridge.

Review A3, resolution 3 with the measurement demanded: criterion (m)
launches exactly 'incus launch --network boxnet' as the restricted user and
probes the raw NIC from inside — egress works, RFC1918 dropped (the ACL is
the network's), sibling probes dropped BOTH directions (the nft drop is the
host's), name enumeration blocked. The scoped guarantee is now stated in
box-design.md and measured on every run: box-minted instances carry per-NIC
port_isolation; raw attachments keep every network- and host-owned control,
losing only that redundant L2 layer. Instrument lesson kept as MU-5: the
probe's first cut minted the non-cloud image — no DHCP client, no lease,
and a dead NIC passes every negative probe vacuously; it now requires the
lease before believing its own answers.

Rehearsal: 54/54 (containers). test/cli.sh: 82 checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 06:41:05 +00:00
dan-claude-bot
565e7693e8 docs: log MU-4 (the CI job's version-drift catch) and refresh the counts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 05:38:04 +00:00
dan-claude-bot
2097982ac2 docs: the restricted tier — README, design doc, plan doc with measured results (#74)
The plan doc records what was measured and why each decision fell where it
did: the private bridge is worse than unhardened (a live NAT bridge with
IPv6 on), incus-user blocks snapshots, no daemon-level template exists (read
in incus-user's source), widening survives re-sync (same source, then
measured live). box-design gains the access-tiers section — including why
narrowing to boxnet-only is the load-bearing decision and why the nft bridge
drop is the layer a restricted user cannot strip. RUNS.md logs MU-1..3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:09:48 +00:00
dan-claude-bot
f7b93a91cd docs(plan): stake out restricted-tier PR for #74 (placeholder)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 01:06:34 +00:00