diff --git a/CHANGELOG.md b/CHANGELOG.md index de1c3d0..38e4d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ which records not just what changed but what each drill run proved. ## Unreleased +## 0.6.0 — 2026-07-18 + ### Added - **The restricted tier: multi-user hosts** (#74, redesigning #72) — an admin @@ -12,7 +14,7 @@ which records not just what changed but what each drill run proved. hardened `boxnet`, seeing nobody else's; `box revoke ` takes it back (`--purge` deletes their world, and asserts the absence). The tier rides incus-user, whose defaults miss box's contract three measured ways (Debian - 13 / Incus 6.0.4): a private *unhardened* NAT bridge per user, snapshots + 13 / Incus 6.0.4): a private _unhardened_ NAT bridge per user, snapshots blocked, the `box-net` profile invisible — so grant is an idempotent convergence: project narrowed to `boxnet` **and only boxnet** (listing the private bridge too, the obvious fix, would keep an unhardened network one @@ -29,7 +31,7 @@ which records not just what changed but what each drill run proved. `multiuser.sh --container`), so every PR proves the tier's semantics against a live daemon, not a mock. The VM trust boundary itself remains a real-hardware ritual, like the full drill. -- **Global / root install** (#71) — run as root, box installs *once* to +- **Global / root install** (#71) — run as root, box installs _once_ to `/opt/box` (world-readable) with the `box` symlink on `/usr/local/bin`, so every operator on a shared host runs the same tree. Per-user installs are unchanged (`$HOME/.local`); `BOX_HOME`/`BOX_BIN` still override. A per-user @@ -53,7 +55,7 @@ which records not just what changed but what each drill run proved. restricted user; fixed for every tier, and the rehearsal + a grep-guard in `test/cli.sh` now hold it. - **`box tmux` works on every template** (#65) — `box tmux` runs - `tmux new-session` *inside* the box, but the templates did not install tmux, so + `tmux new-session` _inside_ the box, but the templates did not install tmux, so it failed with `tmux: command not found`. `tmux` is now in each template's cloud-init package list (`blank`/`claude`/`codex`/`grok`). @@ -103,11 +105,11 @@ which records not just what changed but what each drill run proved. It now asserts the post-install stack in-group before touching the host, and runs `setup-host` exactly once more — after the clean, which deliberately unsets `dns.mode` and so has to be converged back. `DRILL_OWNS_SETUP=1` - hands sequencing back to the drill. Pre-setup tripwires now read *before* + hands sequencing back to the drill. Pre-setup tripwires now read _before_ `install.sh`, since that is what triggers setup now. - **`install.sh` asks, sets up the host, and no-ops on re-run** (#64) — it now - prompts *"Install box?"*, then on a fresh host installs the tree and asks a - second question, *"Set up this machine as a box host now?"*, running the whole + prompts _"Install box?"_, then on a fresh host installs the tree and asks a + second question, _"Set up this machine as a box host now?"_, running the whole isolation stack if you say yes (previously it only printed a warning and left you a command, so the install reported success and `box new` died on a host with no Incus). Prompts read `/dev/tty`, since under `curl | bash` the script @@ -141,7 +143,7 @@ installer retires — but nothing current carries the old name. is per-port, `--list`/`--remove` manage it, and `box info` shows open exposures — a box with a hole says so. - **Inline resource overrides on `new`** — `--cpu --memory - --disk ` (#57). Resolution most-specific-first: flag > `BOX_CPU` / +--disk ` (#57). Resolution most-specific-first: flag > `BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment (the scripting form) > template `box.env` > defaults. Values pass to Incus verbatim; resources are all a flag can touch. `--from` refuses them — a clone carries its source's diff --git a/README.md b/README.md index 367a21d..21b5930 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ destroying it loses nothing you didn't push. **Strictly creds-free.** A box ships with everything installed and **no** credentials — no agent token, no git PAT, nothing. You authenticate -interactively *inside* the box. The tool never stores or injects a secret. That +interactively _inside_ the box. The tool never stores or injects a secret. That means there's nothing shared or committed, so it's safe for multiple operators out of the box. @@ -23,6 +23,8 @@ runbook that the box's coding agent reads and acts on — there is no `install` step and no host-run setup. See [docs/box-design.md](docs/box-design.md) for the design rationale. +> **0.6.0**: multi-user support. + > **0.5.0**: two new templates (`codex`, `grok`), `box expose` — a > loopback-only door to a box port, for seeing a dev server — and the host > lifecycle as first-class verbs: `box setup-host`, `box teardown-host`, and @@ -52,7 +54,7 @@ rebuild the stack under your boxes. Upgrading is therefore explicit: uninstall what you have and install fresh. Preserve any boxes first — `box down `, copy out anything you need (a portable `box export` is [#70](https://github.com/heavy-duty/box/issues/70)), then `box rm ` -(which deletes the box *and* its snapshots) — then: +(which deletes the box _and_ its snapshots) — then: ```sh rm -rf ~/.local/share/box ~/.local/bin/box # uninstall @@ -67,7 +69,7 @@ declines the host-setup step. ### Global vs per-user install Where box lands depends on **who runs the installer**, because on a shared host -box's tree is *executed by other users* — so it cannot hide in one user's home: +box's tree is _executed by other users_ — so it cannot hide in one user's home: - **As root → global.** The tree goes to `/opt/box` (world-readable) and the `box` symlink to `/usr/local/bin` (already on every login `PATH`). One @@ -113,11 +115,11 @@ socket is all-or-nothing — `incus-admin` group members own every instance on the machine — so box layers a second tier on [incus-user](https://linuxcontainers.org/incus/docs/main/projects/): -| tier | who | what they hold | -|---|---|---| -| **admin** | root, or the `incus-admin` group | everything: all boxes, the stack, `setup-host`, `expose`, `grant` | -| **restricted** | the `incus` group | their **own** boxes only, on the same hardened network | -| none | everyone else | no socket, nothing | +| tier | who | what they hold | +| -------------- | -------------------------------- | ----------------------------------------------------------------- | +| **admin** | root, or the `incus-admin` group | everything: all boxes, the stack, `setup-host`, `expose`, `grant` | +| **restricted** | the `incus` group | their **own** boxes only, on the same hardened network | +| none | everyone else | no socket, nothing | An admin hands the tier out per user, and takes it back: @@ -132,7 +134,7 @@ box revoke dev1 --purge # ...or end their sessions and delete everything the `grant` is an idempotent convergence, not a flag flip, because incus-user's defaults miss box's contract three ways (measured on Debian 13 / Incus 6.0.4, see [the plan doc](docs/plans/2026-07-18-restricted-tier.md)): it pins each -user to a private *unhardened* NAT bridge, it blocks snapshots, and it cannot +user to a private _unhardened_ NAT bridge, it blocks snapshots, and it cannot see the `box-net` profile. Granting rewires all three: the user's project is restricted to `boxnet` **and only boxnet** — the hardened network is not their default placement but the only one their certificate can express — snapshots @@ -175,12 +177,12 @@ claude # if the repo has .box/, the agent reads it and No coding agent is special — each is one template among several, and adding another is just another directory. What ships today: -| Template | What's in it | -| --- | --- | +| Template | What's in it | +| -------- | --------------------------------------------------------- | | `blank` | Bare Debian 13 — same isolation, no tooling. The default. | -| `claude` | Claude Code, creds-free — where this project started | -| `codex` | OpenAI Codex CLI, creds-free | -| `grok` | xAI Grok CLI, creds-free | +| `claude` | Claude Code, creds-free — where this project started | +| `codex` | OpenAI Codex CLI, creds-free | +| `grok` | xAI Grok CLI, creds-free | A template is a directory under `templates/`: a `box.env` (image, user, resources — parsed against a strict allowlist, never sourced) and a @@ -303,7 +305,7 @@ command surface is a table. ## Isolation The contract: **a box reaches the public internet and nothing else.** Not the -host, not your LAN, not another box, not even another box's *name*. What +host, not your LAN, not another box, not even another box's _name_. What enforces it, layer by layer: - **Dedicated NAT bridge** `boxnet`, IPv6 off. Every rule below is @@ -311,7 +313,7 @@ enforces it, layer by layer: contract, not a default. - **`box-isolate` ACL** — drops all egress to private space (RFC1918, CGNAT, link-local), with a single carve-out to the gateway so DNS works. -- **Sibling isolation, at L2** — two boxes on one bridge are *switched*, +- **Sibling isolation, at L2** — two boxes on one bridge are _switched_, never routed, so no L3 rule can separate them (learned the hard way; see below). `security.port_isolation` on every box NIC plus an nft bridge-family drop mean box A cannot exchange frames with box B at all. diff --git a/VERSION b/VERSION index 8f0916f..a918a2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0