chore: bump version for cleanness

This commit is contained in:
Dan 2026-07-18 14:39:45 +01:00
parent 014d662bd1
commit 77599ab3ee
3 changed files with 28 additions and 24 deletions

View file

@ -5,6 +5,8 @@ which records not just what changed but what each drill run proved.
## Unreleased ## Unreleased
## 0.6.0 — 2026-07-18
### Added ### Added
- **The restricted tier: multi-user hosts** (#74, redesigning #72) — an admin - **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 <user>` takes it back hardened `boxnet`, seeing nobody else's; `box revoke <user>` takes it back
(`--purge` deletes their world, and asserts the absence). The tier rides (`--purge` deletes their world, and asserts the absence). The tier rides
incus-user, whose defaults miss box's contract three measured ways (Debian 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 blocked, the `box-net` profile invisible — so grant is an idempotent
convergence: project narrowed to `boxnet` **and only boxnet** (listing the convergence: project narrowed to `boxnet` **and only boxnet** (listing the
private bridge too, the obvious fix, would keep an unhardened network one 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 `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 against a live daemon, not a mock. The VM trust boundary itself remains a
real-hardware ritual, like the full drill. 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 `/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 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 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 restricted user; fixed for every tier, and the rehearsal + a grep-guard in
`test/cli.sh` now hold it. `test/cli.sh` now hold it.
- **`box tmux` works on every template** (#65) — `box tmux` runs - **`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 it failed with `tmux: command not found`. `tmux` is now in each template's
cloud-init package list (`blank`/`claude`/`codex`/`grok`). 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 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 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` 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`, since that is what triggers setup now.
- **`install.sh` asks, sets up the host, and no-ops on re-run** (#64) — it 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 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 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 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 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 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 is per-port, `--list`/`--remove` manage it, and `box info` shows open
exposures — a box with a hole says so. exposures — a box with a hole says so.
- **Inline resource overrides on `new`** — `--cpu <n> --memory <size> - **Inline resource overrides on `new`** — `--cpu <n> --memory <size>
--disk <size>` (#57). Resolution most-specific-first: flag > `BOX_CPU` / --disk <size>` (#57). Resolution most-specific-first: flag > `BOX_CPU` /
`BOX_MEMORY` / `BOX_DISK` environment (the scripting form) > template `BOX_MEMORY` / `BOX_DISK` environment (the scripting form) > template
`box.env` > defaults. Values pass to Incus verbatim; resources are all a `box.env` > defaults. Values pass to Incus verbatim; resources are all a
flag can touch. `--from` refuses them — a clone carries its source's flag can touch. `--from` refuses them — a clone carries its source's

View file

@ -8,7 +8,7 @@ destroying it loses nothing you didn't push.
**Strictly creds-free.** A box ships with everything installed and **no** **Strictly creds-free.** A box ships with everything installed and **no**
credentials — no agent token, no git PAT, nothing. You authenticate 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 means there's nothing shared or committed, so it's safe for multiple operators
out of the box. 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 step and no host-run setup. See [docs/box-design.md](docs/box-design.md) for the
design rationale. design rationale.
> **0.6.0**: multi-user support.
> **0.5.0**: two new templates (`codex`, `grok`), `box expose` — a > **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 > 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 > 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 <box>`, what you have and install fresh. Preserve any boxes first — `box down <box>`,
copy out anything you need (a portable `box export` is copy out anything you need (a portable `box export` is
[#70](https://github.com/heavy-duty/box/issues/70)), then `box rm <box>` [#70](https://github.com/heavy-duty/box/issues/70)), then `box rm <box>`
(which deletes the box *and* its snapshots) — then: (which deletes the box _and_ its snapshots) — then:
```sh ```sh
rm -rf ~/.local/share/box ~/.local/bin/box # uninstall rm -rf ~/.local/share/box ~/.local/bin/box # uninstall
@ -67,7 +69,7 @@ declines the host-setup step.
### Global vs per-user install ### Global vs per-user install
Where box lands depends on **who runs the installer**, because on a shared host 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 - **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 `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 the machine — so box layers a second tier on
[incus-user](https://linuxcontainers.org/incus/docs/main/projects/): [incus-user](https://linuxcontainers.org/incus/docs/main/projects/):
| tier | who | what they hold | | tier | who | what they hold |
|---|---|---| | -------------- | -------------------------------- | ----------------------------------------------------------------- |
| **admin** | root, or the `incus-admin` group | everything: all boxes, the stack, `setup-host`, `expose`, `grant` | | **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 | | **restricted** | the `incus` group | their **own** boxes only, on the same hardened network |
| none | everyone else | no socket, nothing | | none | everyone else | no socket, nothing |
An admin hands the tier out per user, and takes it back: 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 `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, 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 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 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 restricted to `boxnet` **and only boxnet** — the hardened network is not their
default placement but the only one their certificate can express — snapshots 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 No coding agent is special — each is one template among several, and adding
another is just another directory. What ships today: 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. | | `blank` | Bare Debian 13 — same isolation, no tooling. The default. |
| `claude` | Claude Code, creds-free — where this project started | | `claude` | Claude Code, creds-free — where this project started |
| `codex` | OpenAI Codex CLI, creds-free | | `codex` | OpenAI Codex CLI, creds-free |
| `grok` | xAI Grok CLI, creds-free | | `grok` | xAI Grok CLI, creds-free |
A template is a directory under `templates/`: a `box.env` (image, user, A template is a directory under `templates/`: a `box.env` (image, user,
resources — parsed against a strict allowlist, never sourced) and a resources — parsed against a strict allowlist, never sourced) and a
@ -303,7 +305,7 @@ command surface is a table.
## Isolation ## Isolation
The contract: **a box reaches the public internet and nothing else.** Not the 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: enforces it, layer by layer:
- **Dedicated NAT bridge** `boxnet`, IPv6 off. Every rule below is - **Dedicated NAT bridge** `boxnet`, IPv6 off. Every rule below is
@ -311,7 +313,7 @@ enforces it, layer by layer:
contract, not a default. contract, not a default.
- **`box-isolate` ACL** — drops all egress to private space (RFC1918, - **`box-isolate` ACL** — drops all egress to private space (RFC1918,
CGNAT, link-local), with a single carve-out to the gateway so DNS works. 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 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 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. bridge-family drop mean box A cannot exchange frames with box B at all.

View file

@ -1 +1 @@
0.5.0 0.6.0