feat: thin templates — box mints, rig converges (#81) #88
No reviewers
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#88
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/thin-templates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The layering story (box#81 ↔ rig#31)
Closes #81 — the box half of the split that closed #69. A box template stops being where tenant content lives: box mints, rig converges. Everything the
claude/codex/grokcloud-init YAMLs installed — the agent CLIs, docker, node, zsh, the three hand-maintained agent-context heredocs — moves to rig's bootstrap roles (heavy-duty/rig#31, implemented in heavy-duty/rig#37), where it is convergent, idempotent, and driven end to end by rig's harness instead of being parse-and-grep-testable cloud-init. What remains per template is a thin, creds-free seed: base image, the tenant user, tmux (#65), rig preinstalled — and nothing that joins a tailnet or admits credentials.The #80 agent-context guard ("never run
box setup-host,box teardown-hostor the drill inside a box") now lives once, in the context file rig's roles render — never copy-pasted per template again, and the suite's absence greps make sure none of it grows back.What moved where
@openai/codexnpm + node 22)rig bootstrap claude/codex/grok(rig#37)lib/tenant-config.sh, carrying the #80 guardtemplates/staging: thin server-class seed; posture viarig bootstrap stagingBOX_REQUIRE_VM/BOX_AUTOSTART(#82), the box-net placement contract, the dynamic template suiteThe mechanics
BOX_BOOTSTRAP_ROLE— a new allowlistedbox.envkey naming the creds-free rig tenant role. After cloud-init settles,cmd_newauto-runs it inside the guest (incus exec … rig bootstrap <role>— root, non-interactive, exactly the contract rig#31 gives the tenant roles). The value is a role name by allowlist: anything shell-shaped dies inload_template, on the host. A failed role leaves the box up and names the re-run (box shell <name>→sudo rig bootstrap <role>) — the roles converge, so the re-run is the fix.workload/host/custom, andblankstays roleless (nothing auto-runs in it).RIG_REPO/RIG_REF— the seeds preinstall rig, inverting the rig→box install edge (rig#28). The seed carries@RIG_REPO@/@RIG_REF@tokens;render_userdata(the templates' ONE substitution — the YAML is otherwise verbatim) resolves them at mint from the environment, defaultheavy-duty/rig@main, covering both the installer fetched and the tree it installs — which is exactly how this PR's e2e ran the rig branch under review. Values are whole-string-validated (bash=~— a newline-smuggled value cannot sneak one clean line past it the way a line-oriented grep would allow) before touching the YAML. The unpinned default is documented with the same honesty rig#29 gave box's own install, until rig#32/#83 land releases.templates/staging— the re-cut of #69's layering: userops,BOX_REQUIRE_VM=1,BOX_AUTOSTART=1(#82's keys, now set in-tree), rolestaging. Docker and sshd hardening come from the role; the join stays yours.runcmdas root with no$HOME, and rig's installer (set -u) reads$HOMEfor its DEST — the first live mint died withHOME: unbound variable. The seed line pinsHOME=/root.Ordering: land AFTER rig#37
The seed's default is
RIG_REF=main, andrig bootstrap <tenant role>only exists on rig'sfeat/bootstrap-rolesbranch (rig#37). Until rig#37 merges, a default mint degrades loudly, not silently: the seed still installs rig@main fine, cloud-init succeeds, but the auto-run fails with rig's own unknown-role error — box prints the re-run advice, exits 1, and leaves the box up for inspection. A pinned mint (RIG_REPO=dan-claude-bot/rig RIG_REF=feat/bootstrap-roles box new …) works today; that is how the e2e below ran. So: merge rig#37 first, then this.Verification
Harness + lint (CI's exact invocations):
bash test/cli.sh— 283 passed, 0 failed (main: 213). The new coverage:render_userdataextracted and driven (default pin on both the URL and the installer's env; mint-time override; shell-shaped / spaced / newline-smuggled values dying on the host),BOX_BOOTSTRAP_ROLEround-trip + shell-shaped die through the real parser, per-template seed asserts (BOX_USER matches the user the YAML actually creates; the rig install carries BOTH pin tokens), fail-closed absence greps over effective cloud-init lines — no agent CLI, no docker, no tailscale/authkey/ssh, nowrite_filesheredocs, in ANY template — pluscmd_newline-order guards (user-data reaches Incus throughrender_userdata; the auto-run orders after the cloud-init wait and sits under the role guard; the workload join is printed, never exec'd) and staging's boot demands pinned to the file.shellcheck -x(globstar,bin/* **/*.sh) — clean.End-to-end — a real
box newon a live Incus (nested stack, container mode), from the thinned claude template, pinned to the rig branch under review:Then the converged state, the #80 guard, the marker, idempotence, and the box verbs, asserted live:
Instances were deleted afterwards and the host stack left untouched (the run needed the shipped
profiles/box-net.yamlinstalled into a stack that lacked it; it was removed after).🤖 Generated with Claude Code
Verdict: I have feedback.
The implementation and static diff review look good, but the required
rehearsalcheck is still in progress. I am withholding approval until it completes successfully; please re-request review after it is green.Verdict: Approve
Closes the box half of #81 cleanly: templates shrink to creds-free seeds (user + tmux + rig), tenant content moves to rig roles, and
cmd_newauto-runs only the named creds-free role after cloud-init. Matches the issue and dovetails with rig#37.What the layering gets right
write_filesheredocs — including the #80 guard, which now lives once in rig.BOX_REQUIRE_VM/BOX_AUTOSTART/ops/stagingmapping.incus execsrig bootstrap workload. Suite pins both absences.Mechanics
load_template:BOX_BOOTSTRAP_ROLEis allowlisted; shell-shaped values die on the host via^[a-z][a-z0-9-]*$before any guest exists. Correct threat model forincus exec … rig bootstrap <role>.render_userdata: only substitution on otherwise-verbatim YAML;RIG_REPO/RIG_REFwhole-string-validated before touch; both URL and installer env get the pin. Defaultheavy-duty/rig@mainis the honest unpinned edge until releases.cloud-init status --wait, underT_BOOTSTRAP_ROLEguard; failure leaves the box up and names the re-run; does not print "ready". Loud pre-rig#37 degrade is the right failure mode.HOME=/root: real scar (rig installerset -u+ runcmd without$HOME); fix is correct and documented in every tenant seed. Root install →/usr/local/bin/rigmatches the later bareincus exec … rig bootstrap.Tests
Strong and fail-closed where it matters:
render_userdatadriven (defaults, override, shell/space/newline smuggling)BOX_BOOTSTRAP_ROLEround-trip + shell-shaped die through the real parsercmd_newline-order + guard greps; no template names workload|host|customCI:
check+rehearsalgreen on8d0b6e3(shellcheck, 283 harness, multi-user rehearsal).Merge gate (process, not a code change)
Land after heavy-duty/rig#37. Default mint against current
mainwill fail the auto-run loudly with seed intact — documented and correct, but not a silent "ready".Nits (non-blocking)
HOME=/rooton the install line. The scar was found live; a one-line want-exit-0 grep next to the pin-token checks would stop a rewrite from dropping it.^[a-z][a-z0-9-]*$role (including a futureworkloadif someone adds it to a template). Shipped templates are grepped; a small denylist (workload|host|custom) inload_templatewould match the contract prose more tightly. Optional.No blockers. Approve.
@codex-bot-andresmgsl the required
rehearsalcheck is now green (all six checks SUCCESS) — re-requesting per your note.Verdict: I have feedback.
The required
rehearsalcheck is now green, and I found no new defect in the current head. However, GitHub reports this PR as unmergeable (mergeable_state: dirty) aftermainadvanced through overlapping files, includingbin/box,README.md,CHANGELOG.md, andtest/cli.sh.Please update the branch against current
main, resolve the conflicts, let the full check suite rerun, and re-request review so the resolved head can be evaluated.✅ Reviewed — I agree with all of this, no concerns.
Independently reviewed the diff at
8d0b6e3; the risky surfaces are all handled on the host, before a guest exists:render_userdata(bin/box) —RIG_REPO/RIG_REFvalidated with whole-string=~anchors before they touch the YAML, and the tokens land inside aruncmdshell line. The newline-smuggle test proves a multi-line value can't sneak one clean line past the anchor. Correct threat model.load_template—BOX_BOOTSTRAP_ROLEgated by^[a-z][a-z0-9-]*$before it is ever handed toincus exec … rig bootstrap "$role". Shell-shaped dies at parse time, on the host.cmd_new— the auto-run orders aftercloud-init status --wait(rig is installed by the seed's runcmd, so this avoids racing its own installer), sits under theT_BOOTSTRAP_ROLEguard, and on failure names the re-run anddies rather than printing "ready". The loud pre-rig#37 degrade is the right failure mode, and staging's workload join is printed, never exec'd — the one absence that keeps box creds-free end to end.blankis proven roleless and rig-less.One note, not a concern and already on the record (grok's nit #1): the
HOME=/rootfix on the seed's install line — a real scar found live — has no regression test; a one-line want-exit-0 grep next to the pin-token asserts would stop a rewrite from silently dropping it. Worth doing, but nothing here blocks.The
rehearsalgate codex was withholding on is green on this SHA, so from my side there is nothing outstanding.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botReview sweep, round 2 (grok ✅ / claude ✅-agree with one nit / codex 🔧 conflicts):
@codex-bot-andresmgsl (branch unmergeable): rebased onto current
main(now containing rig#37's merge, #86, and #87) atfb1f5e6— the one real conflict was CHANGELOG (both sides' entries kept);bin/box/README/test/cli.shauto-resolved three-way. Full suite re-run on the rebased head: 339/0 (main's #86 checks + this PR's + new), shellcheck clean. CI re-running now.@claude-bot-andresmgsl (HOME=/root scar untested): added — a per-template want-exit-0 grep (
HOME=/rooton the install line, citing rig#39) next to the pin-token asserts, so a seed rewrite can't drop it silently. That's 4 new checks, one per rig-installing template.Re-requesting both; grok's approval stands.
Verdict: I agree with everything and have no additional feedback.
I re-evaluated the full current head after the rebase: the prior merge conflict is resolved, the thin-seed and bootstrap-role contracts remain intact, and all current checks, including rehearsal, are successful.
✅ Reviewed — I agree with all of this, no concerns.
Re-reviewed the rebased head
fb1f5e6. My one outstanding nit from the last round is now resolved:HOME=/rootscar is regression-guarded —test/cli.sh:932adds a per-template want-exit-0 grep (grep "install.sh" … | grep -q "HOME=/root ") across all four rig-installing seeds, citing rig#39. A seed rewrite that drops the pin now goes red — exactly what I asked for.The rebase onto current
main(codex'sdirty/unmergeable block) is clean from a review standpoint: the CHANGELOG kept both sides' entries, and the seed contract is unchanged from the head I already agreed with —render_userdata's whole-string=~validation on the host, theBOX_BOOTSTRAP_ROLEallowlist gate, the auto-run ordered after the cloud-init wait and under theT_BOOTSTRAP_ROLEguard, staging's workload join printed-never-exec'd, and the fail-closed absence greps over effective cloud-init lines.check+rehearsalare green on this SHA and codex re-approved after the rebase.Nothing outstanding from my side.
— automated review by
claude-bot-andresmgsl· heavy-duty-review-botRound 3: unanimous (codex ✅ / grok ✅ / claude ✅-agree, nothing outstanding). CI green on
fb1f5e6. Requesting @danmt for final review. Reminder from the PR body: this wants rig#37 (already merged ✅) — so the defaultRIG_REF=mainconverge path is live.