feat: server-class staging template — box mints, rig converges, cast registers #69

Closed
dan-claude-bot wants to merge 6 commits from feat/staging-template into main
dan-claude-bot commented 2026-07-17 15:51:06 +00:00 (Migrated from github.com)

Server-class boxes: the staging template

box gains a server-class staging template so staging VMs — registered in the control plane as servers, reachable tailnet-only — are minted by box instead of by a hand-rolled incus launch in a private infra repo.

The layering (companion: heavy-duty/rig#22):

  • box mints the VM from templates/staging/: Debian 13 cloud image, user ops, build-sized resources, docker + rig (+ tmux, the #65 contract) preinstalled — and nothing that joins or admits. Strictly creds-free.
  • rig, run inside (box shellsudo rig bootstrap workload), hardens sshd and joins the tailnet; it holds the auth key in process memory. box never sees it.
  • cast registers the box in the control plane by its tailnet address (out of scope here).

Two new optional box.env allowlist keys carry the server posture, and only those:

  • BOX_REQUIRE_VM=1 — no container fallback (exit 1, naming /dev/kvm) and no explicit --container (exit 2): the VM is the trust boundary and the guest runs docker. The refusal reads the effective mode (after pick_mode), and its message holds for both tiers — /dev/kvm is a host fact, and admin and restricted mints go through the same daemon, so the fix is a KVM host, not a grant.
  • BOX_AUTOSTART=1boot.autostart=true at launch, so the box returns from a host reboot without an operator; clones inherit it via incus copy.

There is still no key for a network or a security.* flag; box's isolation stack is untouched (the guest joins the overlay outbounddocs/box-design.md gains the overlay-join clarification and the snapshot-before-join rule).

Rebased onto main

The branch was rebased onto current main (post #74 restricted tier, CI, test/cli.sh):

  • load_template: the new key arms re-applied onto main's version (spelled-out required-keys guard, SC2015/SC2034 comment blocks — all intact).
  • cmd_new: the BOX_REQUIRE_VM refusal sits in the fresh-mint branch after pick_mode, downstream of main's new tier-aware box-net pre-flight.
  • The staging template now installs tmuxbox tmux runs tmux new-session inside every template's box (#65), and the operator babysits rig bootstrap workload through it.
  • Design-doc section re-placed after main's new multi-user-tiers section.

Template test suite (new)

test/cli.sh's template coverage is now dynamic over templates/*/ — a new template cannot ship unseen (the old check hardcoded blank/claude/codex/grok). Per template:

  • box.env is driven through the real, extracted load_template (the same source-the-pure-function trick as box_tier): unknown keys and missing BOX_IMAGE/BOX_USER fail, with fixtures proving both dies.
  • user-data.yaml exists, declares #cloud-config, parses as YAML (python3+pyyaml, loudly skipped where absent — CI has both), and installs tmux (#65).

Staging-specific: both boot demands proven through the parser; docker + rig present; and a creds-free grep-refusal — no tailscale/authkey/ssh in effective cloud-init lines. Grep guards pin the cmd_new half: the refusal orders after pick_mode, and boot.autostart is stamped only under the T_AUTOSTART guard.

Refusal paths were also driven end-to-end against a shimmed incus: explicit --container → exit 2; no /dev/kvm (mount-namespace) → exit 1 with the KVM message; KVM host → mints.

Gate: bash test/cli.sh — 123 passed, 0 failed; shellcheck -x clean over bin/* **/*.sh.

Closes #68

🤖 Generated with Claude Code

## Server-class boxes: the `staging` template box gains a server-class `staging` template so staging VMs — registered in the control plane as servers, reachable tailnet-only — are minted by box instead of by a hand-rolled `incus launch` in a private infra repo. **The layering** (companion: heavy-duty/rig#22): - **box** mints the VM from `templates/staging/`: Debian 13 cloud image, user `ops`, build-sized resources, docker + rig (+ tmux, the #65 contract) preinstalled — and *nothing* that joins or admits. Strictly creds-free. - **rig**, run inside (`box shell` → `sudo rig bootstrap workload`), hardens sshd and joins the tailnet; it holds the auth key in process memory. box never sees it. - **cast** registers the box in the control plane by its tailnet address (out of scope here). **Two new optional `box.env` allowlist keys** carry the server posture, and only those: - `BOX_REQUIRE_VM=1` — no container fallback (exit 1, naming `/dev/kvm`) and no explicit `--container` (exit 2): the VM is the trust boundary and the guest runs docker. The refusal reads the *effective* mode (after `pick_mode`), and its message holds for both tiers — `/dev/kvm` is a host fact, and admin and restricted mints go through the same daemon, so the fix is a KVM host, not a grant. - `BOX_AUTOSTART=1` — `boot.autostart=true` at launch, so the box returns from a host reboot without an operator; clones inherit it via `incus copy`. There is still no key for a network or a `security.*` flag; box's isolation stack is untouched (the guest joins the overlay *outbound* — `docs/box-design.md` gains the overlay-join clarification and the **snapshot-before-join** rule). ## Rebased onto main The branch was rebased onto current main (post #74 restricted tier, CI, `test/cli.sh`): - `load_template`: the new key arms re-applied onto main's version (spelled-out required-keys guard, SC2015/SC2034 comment blocks — all intact). - `cmd_new`: the `BOX_REQUIRE_VM` refusal sits in the fresh-mint branch after `pick_mode`, downstream of main's new tier-aware box-net pre-flight. - The staging template now installs **tmux** — `box tmux` runs `tmux new-session` inside every template's box (#65), and the operator babysits `rig bootstrap workload` through it. - Design-doc section re-placed after main's new multi-user-tiers section. ## Template test suite (new) `test/cli.sh`'s template coverage is now **dynamic over `templates/*/`** — a new template cannot ship unseen (the old check hardcoded blank/claude/codex/grok). Per template: - `box.env` is driven through the **real, extracted `load_template`** (the same source-the-pure-function trick as `box_tier`): unknown keys and missing `BOX_IMAGE`/`BOX_USER` fail, with fixtures proving both dies. - `user-data.yaml` exists, declares `#cloud-config`, parses as YAML (python3+pyyaml, loudly skipped where absent — CI has both), and installs tmux (#65). Staging-specific: both boot demands proven through the parser; docker + rig present; and a creds-free grep-refusal — no `tailscale`/`authkey`/`ssh` in effective cloud-init lines. Grep guards pin the `cmd_new` half: the refusal orders after `pick_mode`, and `boot.autostart` is stamped only under the `T_AUTOSTART` guard. Refusal paths were also driven end-to-end against a shimmed incus: explicit `--container` → exit 2; no `/dev/kvm` (mount-namespace) → exit 1 with the KVM message; KVM host → mints. Gate: `bash test/cli.sh` — 123 passed, 0 failed; `shellcheck -x` clean over `bin/* **/*.sh`. Closes #68 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot-andresmgsl (Migrated from github.com) reviewed 2026-07-17 15:51:06 +00:00
codex-bot-andresmgsl (Migrated from github.com) reviewed 2026-07-17 15:51:06 +00:00
grok-bot-andresmgsl (Migrated from github.com) reviewed 2026-07-17 15:51:06 +00:00
dan-claude-bot commented 2026-07-18 16:43:16 +00:00 (Migrated from github.com)

Merge order (cross-repo)

Analyzed all open heavy-duty PRs (box #79/#78/#69, rig #30/#29/#21/#19) for file overlap, semantic dependencies, and mergeability. The recommended order:

  1. rig#29 (class-model remnants) — clean vs main, docs-heavy, smallest blast radius.
  2. rig#30 (close-root proves the door) — clean vs main; overlaps #29 only in the README identity-model section (both PRs kept it surgical on purpose) and test/cli.sh → trivial rebase after #29.
  3. box#79 (versioned installs) — clean vs main and the biggest churn on the box side (install.sh rewrite, bin/box, CI). Lands first so the other box PRs rebase onto the new layout exactly once.
  4. box#78 (export/import) — deliberately left install.sh untouched ("owned by a concurrent PR, reconciles at rebase time" — that PR is #79). Rebasing after #79 lets the installer's upgrade-flow message finally name down → export → rm → upgrade → re-import.
  5. box#69 (staging template) — independent feature; overlaps the others only in bin/box / test/cli.sh / CHANGELOG / docs/box-design.md → mechanical rebase.

Closed as superseded (both predate the traits redesign that landed via rig#27/#28, and their content is re-delivered on the new design by their successors' own account):

  • rig#21 → superseded by rig#30
  • rig#19 → superseded by rig#29

Notes:

  • The rig and box tracks are independent — they can merge in parallel; only the order within each repo matters.
  • All five live PRs are currently MERGEABLE/CLEAN against main; the only CONFLICTING ones were the two superseded drafts. Conflicts among the five will only appear as they land — which is what this order minimizes.

This PR is step 5 — mechanical rebase over bin/box / test/cli.sh / CHANGELOG / docs/box-design.md once #79 and #78 are in.

## Merge order (cross-repo) Analyzed all open heavy-duty PRs (box #79/#78/#69, rig #30/#29/#21/#19) for file overlap, semantic dependencies, and mergeability. The recommended order: 1. **rig#29** (class-model remnants) — clean vs main, docs-heavy, smallest blast radius. 2. **rig#30** (close-root proves the door) — clean vs main; overlaps #29 only in the README identity-model section (both PRs kept it surgical on purpose) and `test/cli.sh` → trivial rebase after #29. 3. **box#79** (versioned installs) — clean vs main and the biggest churn on the box side (`install.sh` rewrite, `bin/box`, CI). Lands first so the other box PRs rebase onto the new layout exactly once. 4. **box#78** (export/import) — deliberately left `install.sh` untouched ("owned by a concurrent PR, reconciles at rebase time" — that PR is #79). Rebasing after #79 lets the installer's upgrade-flow message finally name `down → export → rm → upgrade → re-import`. 5. **box#69** (staging template) — independent feature; overlaps the others only in `bin/box` / `test/cli.sh` / `CHANGELOG` / `docs/box-design.md` → mechanical rebase. **Closed as superseded** (both predate the traits redesign that landed via rig#27/#28, and their content is re-delivered on the new design by their successors' own account): - rig#21 → superseded by rig#30 - rig#19 → superseded by rig#29 Notes: - The rig and box tracks are independent — they can merge in parallel; only the order *within* each repo matters. - All five live PRs are currently MERGEABLE/CLEAN against main; the only CONFLICTING ones were the two superseded drafts. Conflicts among the five will only appear as they land — which is what this order minimizes. **This PR is step 5** — mechanical rebase over `bin/box` / `test/cli.sh` / `CHANGELOG` / `docs/box-design.md` once #79 and #78 are in.
dan-claude-bot commented 2026-07-18 16:55:13 +00:00 (Migrated from github.com)

Closing in favor of the thin-template layering decided today: tenant content moves to rig bootstrap roles (heavy-duty/rig#31), templates shrink to creds-free seeds (#81). The parts of this PR that are mint-time box concerns — the BOX_REQUIRE_VM / BOX_AUTOSTART box.env keys and the dynamic template test machinery — are being re-cut as a slim PR. The staging template itself returns as a thin seed once rig#31's staging role exists. Merge order updates accordingly: box track is now #79#78 → slim template-keys PR.

Closing in favor of the thin-template layering decided today: tenant content moves to rig bootstrap roles (heavy-duty/rig#31), templates shrink to creds-free seeds (#81). The parts of this PR that are mint-time box concerns — the BOX_REQUIRE_VM / BOX_AUTOSTART box.env keys and the dynamic template test machinery — are being re-cut as a slim PR. The staging template itself returns as a thin seed once rig#31's staging role exists. Merge order updates accordingly: box track is now #79 → #78 → slim template-keys PR.

Pull request closed

Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#69
No description provided.