From 050c90966e8afd9cb10c8658fa6488a3d4f20f7d Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Mon, 20 Jul 2026 11:09:23 +0000 Subject: [PATCH 1/2] feat: mark 'bootstrapped' after a rig hook box watched succeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit converged anything. This takes the checkpoint one step later: after the rig bootstrap hook has run and box has WATCHED IT SUCCEED, the box is converged and untouched. 'box restore bootstrapped' keeps the tenant role and undoes only what happened after it, which is the undo an operator reaches for far more often and which otherwise costs a ~10-minute re-mint. The policy is #128's, shared rather than copied: snapshot_pristine and the new snapshot_bootstrapped are thin wrappers over one snapshot_mark, so the never-fatal contract and the 'dir' skip exist in exactly one place. What does not generalise is the prose — the two marks name different moments — so each wrapper owns its own narration. Deliberately conditional where 'pristine' is unconditional. 'pristine' marks a MOMENT every fresh mint has; 'bootstrapped' marks an EVENT, and a blank box has no such event, so it gets no mark rather than a byte-identical duplicate of 'pristine' at twice the disk cost. A FAILED hook gets none either: the failure message already sends the operator to 'box shell', which is a run box does not watch, so box hands over 'box snapshot bootstrapped' instead of labelling a convergence it never saw. The label is therefore documented as one-directional on every surface: its presence means the hook converged untouched, its absence means nothing at all. Same durability caveats restated everywhere it appears — it dies with the box on 'box rm', and no filesystem rollback reaches off-box state (heavy-duty/rig#62). Closes #130 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 27 ++++++++ README.md | 38 +++++++++++ bin/box | 157 ++++++++++++++++++++++++++++++++++++++------- docs/box-design.md | 15 +++++ test/cli.sh | 142 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 356 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b518af..eda2970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,33 @@ which records not just what changed but what each drill run proved. class), which under `pipefail` could have narrated the wrong inheritance shape on a clone that does carry a `pristine`. +- **A mint that converges a tenant role marks a `bootstrapped` snapshot after + the hook succeeds** (#130, the half #104 deferred and #128 argued out) — + `pristine` throws the tenant role away; `bootstrapped` keeps it and throws + away only what you did afterwards, which is the undo an operator reaches for + far more often and which otherwise costs a ~10-minute re-mint. Same policy + as `pristine`, sharing one function with it (`snapshot_mark`) so the + never-fatal contract exists in exactly one place: default on, + `BOX_SNAPSHOT_BOOTSTRAPPED=0` opts out, a `dir` pool skips it loudly — with + two marks that disk objection is twice the size, so a CoW-less host is not + asked to pay for one full root copy per mint, let alone two — and a failed + snapshot warns without failing a good mint. **Deliberately conditional where + `pristine` is unconditional**: `pristine` marks a *moment* every fresh mint + has, `bootstrapped` marks an *event* — a rig hook box ran and watched + succeed — and a blank box has no such event, so it gets no mark rather than + a byte-identical duplicate of `pristine` at twice the cost. When the hook + **fails**, box takes no mark either and says so where the operator is + looking: the failure message now hands over `box snapshot + bootstrapped` to take after the by-hand re-run, because box will not label a + convergence it did not watch. So the label is documented as one-directional + on every surface — its **presence** means the hook converged and nothing has + touched the box since; its **absence** means nothing at all (a blank + template, a `dir` pool, an opt-out, or a hand-converged box). Same durability + caveats as `pristine`, restated rather than referenced: it dies with the box + on `box rm` (`box export` is the durable path), and no filesystem rollback + reaches off-box state such as a tailnet device record or a runner + registration (heavy-duty/rig#62). + ### Changed - **`state:needs-human` no longer waits on the cron to become true** (#141) diff --git a/README.md b/README.md index 133382d..eb24004 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,42 @@ multi-GB copy rather than a near-free copy-on-write mark, so the mint installs by default precisely so snapshots are cheap. `BOX_SNAPSHOT_PRISTINE=0` skips the mark on any host. +### `bootstrapped` — the same undo, one step later + +A mint whose template names a bootstrap role marks a second snapshot, +`bootstrapped` ([#130](https://github.com/heavy-duty/box/issues/130)), once +`rig bootstrap` has run and box has **watched it succeed**. That is the box +converged and not yet touched — the state you actually wanted back most of +the time. + +```sh +box restore work bootstrapped # keep the tenant role, undo what you did to it +box restore work pristine # throw the tenant role away too +``` + +Same rules as `pristine`: default on, never fatal, skipped loudly on a `dir` +pool (with two marks that disk cost is twice the size, so a CoW-less host is +not asked to pay it), and `BOX_SNAPSHOT_BOOTSTRAPPED=0` skips it anywhere. +The same two caveats apply, unchanged: it **dies with the box** on `box rm` +(`box export` is the durable path), and it **cannot reach off-box state** — +a tailnet device record, a runner registration (rig#62). + +**Read the label in one direction only.** Its _presence_ means the mint-time +hook converged and nothing has touched the box since. Its _absence_ means +nothing at all, because box only marks a hook it watched: + +- A **blank** box runs no hook, so there is no convergence to mark. It gets + none — `pristine` and `bootstrapped` would be the same disk state at twice + the cost, and a label claiming a convergence that never happened is worse + than no label. +- A box whose hook **failed** gets none either. box tells you to re-run the + role by hand through `box shell`, and a by-hand run happens in a shell box + does not watch — so box hands you `box snapshot bootstrapped` to take + at the moment it is true, rather than inventing a fact. +- A `dir`-pool host and `BOX_SNAPSHOT_BOOTSTRAPPED=0` both skip it. + +`box info ` is what actually tells you which labels a box has. + ## Survive the host: `box export` / `box import` Snapshots live _inside_ a box, and `box rm` deletes the box **and** its @@ -441,6 +477,8 @@ box restore [--force] # roll back to a snapshot — destructive, asks first # 'pristine' is auto-marked at mint: back to # pristine Debian + box's seed, before rig ran + # 'bootstrapped' too, if a rig hook converged: + # the role kept, everything since undone box export [] [--instance-only] # one portable file (snapshots incl.) — survives rm & host box import [--name ] diff --git a/bin/box b/bin/box index 45b7d52..09a279a 100755 --- a/bin/box +++ b/bin/box @@ -311,6 +311,22 @@ none (copying a snapshot). On a 'dir'-backend host, where a snapshot is a full copy rather than a CoW mark, the mint skips it and says so. BOX_SNAPSHOT_PRISTINE=0 skips it anywhere. +A mint whose template names a bootstrap role marks a second snapshot, +'bootstrapped' (#130), once the rig hook has run and box has watched it +SUCCEED — the box converged and not yet touched. It is the cheaper undo: +'pristine' throws the tenant role away, 'bootstrapped' keeps it and throws +away only what you did afterwards. Same rules: never fatal, skipped on a +'dir' pool, and BOX_SNAPSHOT_BOOTSTRAPPED=0 skips it anywhere. + +It is deliberately NOT unconditional the way 'pristine' is. A blank box +runs no hook, so there is no convergence to mark and it gets none — the +label marks an event, not a mint. And if the hook FAILS, box tells you to +re-run it by hand and takes no mark afterwards: a by-hand run happens in a +shell box does not watch, so box will not label a convergence it never saw. +It names 'box snapshot bootstrapped' at that moment instead. So read +the label one way only: its PRESENCE means the hook converged untouched; +its ABSENCE means nothing at all. + box new --name scratch # blank, the default box new --name work --template claude-box box new --name lean --template claude-box --cpu 2 --memory 3GiB @@ -393,8 +409,14 @@ have. box snapshot work authed -One label is taken for you: every fresh mint marks 'pristine' after -cloud-init and before rig's tenant role runs (#104) — see 'box help restore'. +Two labels are taken for you (see 'box help restore'): every fresh mint +marks 'pristine' after cloud-init and before rig's tenant role runs (#104), +and a mint with a bootstrap role marks 'bootstrapped' after box has watched +that role converge (#130). 'bootstrapped' is skipped where there was no hook +to watch — a blank box, or a hook box told you to re-run by hand — so its +absence proves nothing. Take it yourself in that case: + + box snapshot work bootstrapped Snapshots do not outlive their box: 'box rm' deletes a box and every snapshot it has. They are an undo, not a backup — 'box export' is the @@ -424,6 +446,25 @@ a backup — it dies with the box on 'box rm'; 'box export' is what survives. A box minted with --from has no 'pristine' of its own unless it inherited one from its source, because a clone has no pristine moment to capture. +'bootstrapped' is the other label box takes for you (#130), and it is the +one you probably want more often: it is the box AFTER the tenant role +converged and before you touched it. + + box restore work bootstrapped # keep the role, undo what you did to it + box restore work pristine # throw the role away too + +The same two caveats apply to it exactly as written above — it dies with the +box on 'box rm' ('box export' is what survives), and it cannot undo anything +that left the box, a tailnet join or a runner registration among them +(heavy-duty/rig#62). + +Its absence proves NOTHING. box takes it only after a rig hook it ran and +watched succeed, so a blank box has none, a 'dir'-pool host has none, and a +box whose hook failed and was re-run by hand through 'box shell' has none +either — box will not label a convergence it did not watch. 'box info ' +tells you which labels a box actually has; take the mark yourself any time +with 'box snapshot bootstrapped'. + Destructive, so it asks first — naming the snapshot it is rolling back to, because the whole risk is picking the wrong label. --force (-f) skips the prompt; with no TTY to ask on it refuses rather than assuming yes. @@ -1118,14 +1159,29 @@ storage_driver() { printf '%s' "$driver" } -# Take the 'pristine' snapshot, or say loudly why not. Default ON: the value -# of this mark only exists if it is already there on the bad day, and nobody -# takes it by hand at the one moment it is true. The escape hatch is the -# BOX_LAUNCH_TIMEOUT shape — an environment knob, not another flag on 'new'. -snapshot_pristine() { - local instance="$1" name="$2" driver - if [ "${BOX_SNAPSHOT_PRISTINE:-1}" = 0 ]; then - echo "box: skipping the 'pristine' snapshot (BOX_SNAPSHOT_PRISTINE=0)." +# The shared policy behind every mark box takes for you (#104, #130): +# read the opt-out, read the storage driver, refuse to double the disk cost on +# a pool with no copy-on-write, and NEVER fail a mint over a checkpoint. +# +# It lives once on purpose. What generalises here is the MECHANISM, and the +# never-fatal contract inside it is exactly the property that must not be got +# subtly different in two places — one function, one place to review it, one +# place a future third mark inherits it from. What does NOT generalise is the +# PROSE: 'pristine' and 'bootstrapped' name different moments and are worth +# different sentences, so each wrapper below owns its own narration and passes +# it in. Two thin wrappers over one policy is the honest split; a single +# stringly-parameterised function taking five sentences would not be. +# +# The opt-out variable name is DERIVED from the label (BOX_SNAPSHOT_PRISTINE, +# BOX_SNAPSHOT_BOOTSTRAPPED) so the message can never drift from the knob the +# operator actually has to set. +# +# snapshot_mark