diff --git a/bin/box b/bin/box index b8fd45a..2618795 100755 --- a/bin/box +++ b/bin/box @@ -233,11 +233,20 @@ one of its snapshots — login state, git creds and clones carry over, isolation is preserved, and the clone knows its template's user without being told. +A tenant template (claude, codex, grok, staging) is a THIN seed — the user, +tmux, rig (#81) — and after cloud-init box auto-runs the creds-free tenant +role inside it ('rig bootstrap ', rig#31): that role installs the +agent CLI / server posture and the agent-context file. rig is preinstalled +from RIG_REPO/RIG_REF in the mint environment (default heavy-duty/rig@main, +unpinned — an honest edge until rig has releases). Anything that joins a +tailnet or holds a key stays operator-run, never auto-run. + --name Required. The box's name. --template Template to mint from; 'box templates' lists them. - A template sets image, user, resources and boot + A template sets image, user, resources, boot demands (independently: BOX_REQUIRE_VM insists on - VM mode, BOX_AUTOSTART survives host reboots) — + VM mode, BOX_AUTOSTART survives host reboots) and + a creds-free tenant role (BOX_BOOTSTRAP_ROLE) — never the network: every template gets the same isolation. --from [/] Clone src's live state, or its snapshot . @@ -267,10 +276,15 @@ EOF templates) cat <<'EOF' List the templates this install can mint, with their descriptions. A template is a directory under templates/: a box.env (image, user, resources, boot -demands — parsed against an allowlist, never sourced) and a user-data.yaml -(cloud-init, passed to Incus verbatim). Templates cannot touch the network -or security flags — the shared box-net profile is the placement contract, so -every template gets the same isolation. +demands, tenant role — parsed against an allowlist, never sourced) and a +user-data.yaml (cloud-init, passed to Incus verbatim except the rig pin +tokens @RIG_REPO@/@RIG_REF@, resolved at mint from the environment). +Templates cannot touch the network or security flags — the shared box-net +profile is the placement contract, so every template gets the same isolation. + +Templates are thin, creds-free seeds (#81): the user, tmux and rig — what a +box BECOMES lives in rig's bootstrap roles (rig#31), auto-run at mint via +BOX_BOOTSTRAP_ROLE. box mints; rig converges. box templates box new --name scratch --template blank @@ -842,23 +856,26 @@ reset_identity() { wait_agent "$i" } -# Templates set image, user, resources, boot demands and cloud-init — NOTHING -# else. The box.env file is parsed against this allowlist, never sourced: -# sourcing would hand every template arbitrary bash execution on the HOST at -# mint time. And there is deliberately no key for a network or a security flag -# — the shared box-net profile is the placement contract, so no template can -# weaken isolation. 'blank' is a box with nobody home, not a box with the -# safety off. The two boot demands are for server-class templates (#68): -# BOX_REQUIRE_VM=1 refuses the container fallback (the VM is the trust +# Templates set image, user, resources, boot demands, a tenant role and +# cloud-init — NOTHING else. The box.env file is parsed against this allowlist, +# never sourced: sourcing would hand every template arbitrary bash execution on +# the HOST at mint time. And there is deliberately no key for a network or a +# security flag — the shared box-net profile is the placement contract, so no +# template can weaken isolation. 'blank' is a box with nobody home, not a box +# with the safety off. The two boot demands are for server-class templates +# (#68): BOX_REQUIRE_VM=1 refuses the container fallback (the VM is the trust # boundary, and a server-class guest runs docker), and BOX_AUTOSTART=1 stamps # boot.autostart so the box survives a host reboot without an operator. +# BOX_BOOTSTRAP_ROLE (#81) names the rig role box auto-runs after mint — the +# thin-template split: the seed is user + tmux + rig, and what the box BECOMES +# is 'rig bootstrap '. Only creds-free roles belong here, by contract. load_template() { local t="$1" dir line key val dir="$root/templates/$t" [ -d "$dir" ] || die "no such template: $t (see 'box templates')" [ -f "$dir/box.env" ] || die "template '$t' has no box.env" T_DESC=""; T_IMAGE=""; T_USER=""; T_CPU=""; T_MEMORY=""; T_DISK="" - T_REQUIRE_VM=""; T_AUTOSTART="" + T_REQUIRE_VM=""; T_AUTOSTART=""; T_BOOTSTRAP_ROLE="" while IFS= read -r line || [ -n "$line" ]; do case "$line" in ''|\#*) continue ;; esac case "$line" in @@ -882,7 +899,8 @@ load_template() { BOX_DISK) T_DISK="$val" ;; BOX_REQUIRE_VM) T_REQUIRE_VM="$val" ;; BOX_AUTOSTART) T_AUTOSTART="$val" ;; - *) die "template '$t': unknown key '$key' — a template sets image, user, resources and boot demands, nothing else (there is no key for a network, on purpose)" ;; + BOX_BOOTSTRAP_ROLE) T_BOOTSTRAP_ROLE="$val" ;; + *) die "template '$t': unknown key '$key' — a template sets image, user, resources, boot demands and a tenant role, nothing else (there is no key for a network, on purpose)" ;; esac done <"$dir/box.env" # Not 'A && B || die': if T_IMAGE is set but T_USER is not, that idiom still @@ -891,6 +909,12 @@ load_template() { if [ -z "$T_IMAGE" ] || [ -z "$T_USER" ]; then die "template '$t': BOX_IMAGE and BOX_USER are required" fi + # A bootstrap role is a rig role NAME and nothing more — it is handed to + # 'incus exec … rig bootstrap ' at mint, so anything shell-shaped in + # the value must die here, on the host, before a guest ever sees it. + if [ -n "$T_BOOTSTRAP_ROLE" ] && ! [[ "$T_BOOTSTRAP_ROLE" =~ ^[a-z][a-z0-9-]*$ ]]; then + die "template '$t': BOX_BOOTSTRAP_ROLE is not a sane role name: $T_BOOTSTRAP_ROLE" + fi # Resolution, most specific wins: inline flag (--cpu/--memory/--disk, #57) # > BOX_* environment (how a small host or the drill shrinks every box it # mints) > the template's file > defaults. Values pass to Incus verbatim — @@ -901,6 +925,31 @@ load_template() { T_DISK="${disk:-${BOX_DISK:-${T_DISK:-60GiB}}}" } +# The ONE substitution a template gets — user-data.yaml is otherwise passed to +# Incus verbatim. The tenant seeds preinstall rig, which inverts the rig→box +# install edge (rig#28: rig installs box on hosts; now box guests install rig), +# and that edge needs a pin point (#81): the seed carries @RIG_REPO@ / +# @RIG_REF@ tokens, resolved here from the mint environment — RIG_REPO +# (default heavy-duty/rig) and RIG_REF (default main). Both directions track +# main unpinned today, said honestly (the same treatment rig#29 gave box's own +# unpinned install) until a release flow exists (rig#32 / #83). The values are +# allowlist-validated BEFORE touching the YAML: they land inside a runcmd +# shell line, so a quote, a space or a newline smuggled through the +# environment must die on the host, never execute in the guest. bash's =~ +# anchors to the whole string — a multi-line value cannot sneak one clean +# line past it the way a line-oriented grep would. +render_userdata() { + local f="$1" repo="${RIG_REPO:-heavy-duty/rig}" ref="${RIG_REF:-main}" data + [[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] \ + || die "RIG_REPO must look like owner/repo: $repo" + [[ "$ref" =~ ^[A-Za-z0-9._/-]+$ ]] \ + || die "RIG_REF must be a plain ref name (letters, digits, . _ / -): $ref" + data="$(cat "$f")" + data="${data//@RIG_REPO@/$repo}" + data="${data//@RIG_REF@/$ref}" + printf '%s\n' "$data" +} + cmd_templates() { local d t desc echo "TEMPLATES" @@ -969,7 +1018,7 @@ cmd_new() { --config user.box.user="$T_USER" \ --config limits.cpu="$T_CPU" \ --config limits.memory="$T_MEMORY" \ - --config cloud-init.user-data="$(cat "$root/templates/$t/user-data.yaml")" \ + --config cloud-init.user-data="$(render_userdata "$root/templates/$t/user-data.yaml")" \ "${extra[@]}" wait_agent "$instance" echo "box: waiting for phase-1 (cloud-init)..." @@ -1003,6 +1052,24 @@ cmd_new() { echo " not serving, a VPN resolver the box inherits). Diagnose it: box doctor" >&2 die "cloud-init failed — the box is incomplete, so refusing to hand it over" fi + # The tenant convergence (#81): the seed above is thin — the user, tmux, + # rig — and what the box BECOMES is rig's job. A template that names a + # bootstrap role gets it auto-run here, as root inside the guest, because + # the tenant roles are creds-free and non-interactive BY CONTRACT + # (rig#31): no prompts, no tailnet, no keys — nothing that joins or + # admits may ever ride this hook. The creds-holding steps (staging's + # workload join) stay operator-run through 'box shell', and the role is + # idempotent, so a failed or interrupted run is re-runnable as-is. + if [ -n "$T_BOOTSTRAP_ROLE" ]; then + echo "box: converging the tenant — rig bootstrap $T_BOOTSTRAP_ROLE (rig's own narration follows)..." + if ! incus exec "$instance" -- rig bootstrap "$T_BOOTSTRAP_ROLE" &2 + echo "box: rig bootstrap $T_BOOTSTRAP_ROLE FAILED in $name." >&2 + echo "box: the box is up and the seed is intact — the role converges, so re-run it:" >&2 + echo " box shell $name # then: sudo rig bootstrap $T_BOOTSTRAP_ROLE" >&2 + die "the tenant role did not converge — the box is incomplete, so refusing to call it ready" + fi + fi fi # The login hint belongs to the claude template — read the EFFECTIVE # template off the instance, so a clone of a claude box gets it too and a @@ -1011,6 +1078,9 @@ cmd_new() { [ -z "$eff" ] && [ "$(incus config get "$instance" user.claudebox 2>/dev/null || true)" = 1 ] && eff=claude if [ "$eff" = claude ]; then echo "box: ready — 'box shell $name'. Log into Claude inside: run 'claude' then /login." + elif [ "$eff" = staging ]; then + echo "box: ready — 'box shell $name'. The tailnet join stays operator-run (it holds a key box must never see):" + echo " box shell $name # then: sudo rig bootstrap workload --hostname $name" else echo "box: ready — 'box shell $name'." fi diff --git a/templates/claude/box.env b/templates/claude/box.env index 3cbfdd4..6ae89fc 100644 --- a/templates/claude/box.env +++ b/templates/claude/box.env @@ -1,12 +1,17 @@ -# The claude template — Debian 13 + Claude Code, creds-free. +# The claude template — a thin, creds-free seed (#81): Debian 13, the +# 'claude' user, tmux and rig. What the box BECOMES — the Claude Code CLI, +# docker, node, the agent-context file with its #80 guard — is rig's job: +# box auto-runs 'rig bootstrap claude' after mint (heavy-duty/rig#31). # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. -# BOX_USER must match the user user-data.yaml creates (the cloud-init file is -# passed to Incus verbatim, so the duplication is deliberate and by hand). -BOX_DESCRIPTION="Claude Code on Debian 13, creds-free — where this project started" +# BOX_USER must match the user user-data.yaml creates (the duplication is +# deliberate and by hand) — and it is the tenant user the rig role converges +# (rig dies loudly if the seed did not create it). +BOX_DESCRIPTION="Claude Code on Debian 13, creds-free — box mints, rig converges" BOX_IMAGE="images:debian/13/cloud" BOX_USER="claude" BOX_CPU="4" BOX_MEMORY="8GiB" BOX_DISK="60GiB" +BOX_BOOTSTRAP_ROLE="claude" diff --git a/templates/claude/user-data.yaml b/templates/claude/user-data.yaml index 970d898..fbee359 100644 --- a/templates/claude/user-data.yaml +++ b/templates/claude/user-data.yaml @@ -1,61 +1,30 @@ #cloud-config +# A thin, creds-free seed (#81): the tenant user, tmux (#65), and rig — +# nothing that joins a tailnet or admits credentials, no agent CLI, no +# docker, no context-file heredoc. Everything this box becomes comes from +# 'rig bootstrap claude' (heavy-duty/rig#31), which box auto-runs after +# mint; the agent-context file — including the #80 guard (never run +# 'box setup-host' or the drill inside a box) — is rendered by that role, +# once, instead of being copy-pasted per template. users: - name: claude shell: /bin/bash sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: true -write_files: - - path: /home/claude/.claude/CLAUDE.md - owner: "claude:claude" - permissions: '0644' - defer: true - content: | - # You are running inside a box (template: claude) - - A box is a trust-less, network-isolated, ephemeral VM created by the - `box` CLI. Keep this context in mind: - - - **Creds-free by default.** The box starts with no Claude and no git - credentials. If you need to authenticate Claude, the operator runs `/login` - interactively. For git, the operator adds their own credentials (a PAT or - `gh auth login`). Never assume credentials are present; never ask for or - store secrets on disk beyond what the operator sets up. - - **Isolated.** The box reaches the public internet but nothing on the host or - local network. There is no inbound path. - - **Disposable.** Nothing here is backed up. State is discarded when the box is - removed; the operator persists work via git push and via `box snapshot`. - - **Bootstrap runbook.** If the repository you are working in contains a - `.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install - dependencies, start services, template environment files, seed data, and - smoke-test — and follow it. It is documentation for you, not a script the - host runs. package_update: true +# tmux: 'box tmux' runs 'tmux new-session' INSIDE the box (#65). +# curl + ca-certificates: the rig installer below rides them, and a bare +# cloud image is not guaranteed to ship either. packages: - - git - - gh + - tmux - curl - ca-certificates - - gnupg - - ripgrep - - jq - - tmux - - age - - unzip - - build-essential - - zsh runcmd: - - curl -fsSL https://get.docker.com | sh - - usermod -aG docker claude - - curl -fsSL https://deb.nodesource.com/setup_22.x | bash - - - apt-get install -y nodejs - - sudo -u claude bash -lc 'curl -fsSL https://claude.ai/install.sh | bash' - # 'box exec -- claude …' runs through a NON-interactive shell, which - # reads no .bashrc/.zshrc — a PATH export there is invisible to it. The - # symlink makes claude reachable from every shell, interactive or not. - - ln -sf /home/claude/.local/bin/claude /usr/local/bin/claude - - echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/claude/.bashrc - - chsh -s /usr/bin/zsh claude - - sudo -u claude bash -lc 'RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"' - - echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/claude/.zshrc - - echo 'set -g mouse on' > /home/claude/.tmux.conf - - chown claude:claude /home/claude/.tmux.conf + # Preinstall rig so the box can converge — and re-converge — via + # 'rig bootstrap claude'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # box substitutes them at mint from the RIG_REPO/RIG_REF environment + # (default heavy-duty/rig @ main — unpinned, tracking main, the same + # honest edge as rig's own unpinned box install, until rig#32 ships a + # release flow). The pin covers both the installer fetched AND the tree + # it installs, so a branch under review is testable end to end. + - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash diff --git a/templates/codex/box.env b/templates/codex/box.env index 08d5e58..21b30d4 100644 --- a/templates/codex/box.env +++ b/templates/codex/box.env @@ -1,11 +1,17 @@ -# The codex template — Debian 13 + OpenAI Codex CLI, creds-free. +# The codex template — a thin, creds-free seed (#81): Debian 13, the +# 'codex' user, tmux and rig. What the box BECOMES — the OpenAI Codex CLI, +# docker, node, the agent-context file with its #80 guard — is rig's job: +# box auto-runs 'rig bootstrap codex' after mint (heavy-duty/rig#31). # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. -# BOX_USER must match the user user-data.yaml creates. -BOX_DESCRIPTION="OpenAI Codex CLI on Debian 13, creds-free" +# BOX_USER must match the user user-data.yaml creates (the duplication is +# deliberate and by hand) — and it is the tenant user the rig role converges +# (rig dies loudly if the seed did not create it). +BOX_DESCRIPTION="OpenAI Codex CLI on Debian 13, creds-free — box mints, rig converges" BOX_IMAGE="images:debian/13/cloud" BOX_USER="codex" BOX_CPU="4" BOX_MEMORY="8GiB" BOX_DISK="60GiB" +BOX_BOOTSTRAP_ROLE="codex" diff --git a/templates/codex/user-data.yaml b/templates/codex/user-data.yaml index 550e28a..7a443c1 100644 --- a/templates/codex/user-data.yaml +++ b/templates/codex/user-data.yaml @@ -1,60 +1,30 @@ #cloud-config +# A thin, creds-free seed (#81): the tenant user, tmux (#65), and rig — +# nothing that joins a tailnet or admits credentials, no agent CLI, no +# docker, no context-file heredoc. Everything this box becomes comes from +# 'rig bootstrap codex' (heavy-duty/rig#31), which box auto-runs after +# mint; the agent-context file — including the #80 guard (never run +# 'box setup-host' or the drill inside a box) — is rendered by that role, +# once, instead of being copy-pasted per template. users: - name: codex shell: /bin/bash sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: true -write_files: - - path: /home/codex/.codex/AGENTS.md - owner: "codex:codex" - permissions: '0644' - defer: true - content: | - # You are running inside a box (template: codex) - - A box is a trust-less, network-isolated, ephemeral VM created by the - `box` CLI. Keep this context in mind: - - - **Creds-free by default.** The box starts with no OpenAI and no git - credentials. If you need to authenticate Codex, the operator runs the - login flow (`codex`) interactively. For git, the operator adds their own - credentials (a PAT or `gh auth login`). Never assume credentials are - present; never ask for or store secrets on disk beyond what the operator - sets up. - - **Isolated.** The box reaches the public internet but nothing on the host - or local network. There is no inbound path. - - **Disposable.** Nothing here is backed up. State is discarded when the box - is removed; the operator persists work via git push and via `box snapshot`. - - **Bootstrap runbook.** If the repository you are working in contains a - `.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install - dependencies, start services, template environment files, seed data, and - smoke-test — and follow it. It is documentation for you, not a script the - host runs. package_update: true +# tmux: 'box tmux' runs 'tmux new-session' INSIDE the box (#65). +# curl + ca-certificates: the rig installer below rides them, and a bare +# cloud image is not guaranteed to ship either. packages: - - git - - gh + - tmux - curl - ca-certificates - - gnupg - - ripgrep - - jq - - tmux - - age - - unzip - - build-essential runcmd: - - curl -fsSL https://get.docker.com | sh - - usermod -aG docker codex - # Codex CLI is an npm global and needs Node 22+ (verified upstream: - # npmjs.com/package/@openai/codex — the SCOPED @openai/codex, not the - # unrelated 2012 'codex' package). - - curl -fsSL https://deb.nodesource.com/setup_22.x | bash - - - apt-get install -y nodejs - - npm install -g @openai/codex - # 'box exec -- codex …' runs a NON-interactive shell that reads no - # rc files — npm's global bin must be reachable from every shell. Symlink - # the installed binary into /usr/local/bin (the same fix the claude - # template needed for its own CLI). - - ln -sf "$(npm prefix -g)/bin/codex" /usr/local/bin/codex - - echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> /home/codex/.bashrc + # Preinstall rig so the box can converge — and re-converge — via + # 'rig bootstrap codex'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # box substitutes them at mint from the RIG_REPO/RIG_REF environment + # (default heavy-duty/rig @ main — unpinned, tracking main, the same + # honest edge as rig's own unpinned box install, until rig#32 ships a + # release flow). The pin covers both the installer fetched AND the tree + # it installs, so a branch under review is testable end to end. + - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash diff --git a/templates/grok/box.env b/templates/grok/box.env index 52912d3..d0a7dd9 100644 --- a/templates/grok/box.env +++ b/templates/grok/box.env @@ -1,11 +1,17 @@ -# The grok template — Debian 13 + xAI's Grok Build CLI, creds-free. +# The grok template — a thin, creds-free seed (#81): Debian 13, the +# 'grok' user, tmux and rig. What the box BECOMES — the xAI Grok CLI, +# docker, the agent-context file with its #80 guard — is rig's job: +# box auto-runs 'rig bootstrap grok' after mint (heavy-duty/rig#31). # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. -# BOX_USER must match the user user-data.yaml creates. -BOX_DESCRIPTION="xAI Grok CLI on Debian 13, creds-free" +# BOX_USER must match the user user-data.yaml creates (the duplication is +# deliberate and by hand) — and it is the tenant user the rig role converges +# (rig dies loudly if the seed did not create it). +BOX_DESCRIPTION="xAI Grok CLI on Debian 13, creds-free — box mints, rig converges" BOX_IMAGE="images:debian/13/cloud" BOX_USER="grok" BOX_CPU="4" BOX_MEMORY="8GiB" BOX_DISK="60GiB" +BOX_BOOTSTRAP_ROLE="grok" diff --git a/templates/grok/user-data.yaml b/templates/grok/user-data.yaml index 26dad6f..4f67b07 100644 --- a/templates/grok/user-data.yaml +++ b/templates/grok/user-data.yaml @@ -1,75 +1,30 @@ #cloud-config +# A thin, creds-free seed (#81): the tenant user, tmux (#65), and rig — +# nothing that joins a tailnet or admits credentials, no agent CLI, no +# docker, no context-file heredoc. Everything this box becomes comes from +# 'rig bootstrap grok' (heavy-duty/rig#31), which box auto-runs after +# mint; the agent-context file — including the #80 guard (never run +# 'box setup-host' or the drill inside a box) — is rendered by that role, +# once, instead of being copy-pasted per template. users: - name: grok shell: /bin/bash sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: true -write_files: - - path: /home/grok/.grok/AGENTS.md - owner: "grok:grok" - permissions: '0644' - defer: true - content: | - # You are running inside a box (template: grok) - - A box is a trust-less, network-isolated, ephemeral VM created by the - `box` CLI. Keep this context in mind: - - - **Creds-free by default.** The box starts with no xAI and no git - credentials. If you need to authenticate, the operator runs - `grok login` interactively (SuperGrok / X Premium+). For git, the - operator adds their own credentials (a PAT or `gh auth login`). Never - assume credentials are present; never ask for or store secrets on disk - beyond what the operator sets up. - - **Isolated.** The box reaches the public internet but nothing on the host - or local network. There is no inbound path. - - **Disposable.** Nothing here is backed up. State is discarded when the box - is removed; the operator persists work via git push and via `box snapshot`. - - **Bootstrap runbook.** If the repository you are working in contains a - `.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install - dependencies, start services, template environment files, seed data, and - smoke-test — and follow it. It is documentation for you, not a script the - host runs. package_update: true +# tmux: 'box tmux' runs 'tmux new-session' INSIDE the box (#65). +# curl + ca-certificates: the rig installer below rides them, and a bare +# cloud image is not guaranteed to ship either. packages: - - git - - gh + - tmux - curl - ca-certificates - - gnupg - - ripgrep - - jq - - tmux - - age - - unzip - - build-essential runcmd: - - curl -fsSL https://get.docker.com | sh - - usermod -aG docker grok - # The OFFICIAL installer, read at https://x.ai/cli/install.sh rather than - # guessed at. What it actually does: - # · installs the CLI as `grok` (with an `agent` alias) — NOT `grok-build` - # · BIN_DIR defaults to $HOME/.grok/bin, and what it puts there is a - # SYMLINK into its versioned download dir (so `find -type f` misses it) - # · GROK_BIN_DIR overrides that directory - # - # Run it AS grok, not root: the binary symlink points into the invoking - # user's download dir, and root's home is 0700 — a symlink into it would be - # unreadable to the grok user, giving a CLI that exists and cannot run. - - sudo -u grok bash -lc 'curl -fsSL https://x.ai/cli/install.sh | bash' - # 'box exec -- grok …' is a NON-interactive shell: it reads no rc files, - # so ~/.grok/bin is never on its PATH. Symlink onto the system PATH — the - # same fix the claude template needed (#15). Assert the result: a CLI that - # silently is not on PATH is what cost the last drill run. - - | - if [ -e /home/grok/.grok/bin/grok ]; then - ln -sf /home/grok/.grok/bin/grok /usr/local/bin/grok - echo "grok: linked /usr/local/bin/grok -> /home/grok/.grok/bin/grok" - /usr/local/bin/grok --version >/dev/null 2>&1 \ - && echo "grok: 'grok --version' answers from the system PATH" \ - || echo "grok: WARNING - linked, but 'grok --version' does not answer" >&2 - else - echo "grok: installer produced no ~/.grok/bin/grok - upstream layout changed?" >&2 - find /home/grok -maxdepth 4 \( -type f -o -type l \) -perm -u+x 2>/dev/null | head -20 >&2 - fi - - echo 'export PATH="$HOME/.grok/bin:$PATH"' >> /home/grok/.bashrc + # Preinstall rig so the box can converge — and re-converge — via + # 'rig bootstrap grok'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # box substitutes them at mint from the RIG_REPO/RIG_REF environment + # (default heavy-duty/rig @ main — unpinned, tracking main, the same + # honest edge as rig's own unpinned box install, until rig#32 ships a + # release flow). The pin covers both the installer fetched AND the tree + # it installs, so a branch under review is testable end to end. + - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash diff --git a/templates/staging/box.env b/templates/staging/box.env new file mode 100644 index 0000000..2996662 --- /dev/null +++ b/templates/staging/box.env @@ -0,0 +1,24 @@ +# The staging template — a thin, creds-free, server-class seed (#81, the +# re-cut of #69's layering): Debian 13, the 'ops' user, tmux and rig. The +# server posture — docker, sshd hardening — is rig's job: box auto-runs +# 'rig bootstrap staging' after mint (heavy-duty/rig#31). The tailnet +# workload join holds a key and therefore STAYS operator-run: +# box shell # then: sudo rig bootstrap workload --hostname +# KEY="value" only. Parsed against an allowlist, never sourced; there is no +# key for a network or a security flag, on purpose — the shared box-net +# profile is the placement contract and no template can weaken it. +# The two boot demands (#68): the VM is this box's trust boundary and its +# guest runs docker, so no container fallback (BOX_REQUIRE_VM); and a server +# must return from a host reboot without an operator (BOX_AUTOSTART). +# BOX_USER must match the user user-data.yaml creates (the duplication is +# deliberate and by hand) — and it is the tenant user the rig role converges +# (rig dies loudly if the seed did not create it). +BOX_DESCRIPTION="Server-class Debian 13, creds-free — box mints, rig converges, the join stays yours" +BOX_IMAGE="images:debian/13/cloud" +BOX_USER="ops" +BOX_CPU="4" +BOX_MEMORY="8GiB" +BOX_DISK="60GiB" +BOX_REQUIRE_VM="1" +BOX_AUTOSTART="1" +BOX_BOOTSTRAP_ROLE="staging" diff --git a/templates/staging/user-data.yaml b/templates/staging/user-data.yaml new file mode 100644 index 0000000..4cec701 --- /dev/null +++ b/templates/staging/user-data.yaml @@ -0,0 +1,31 @@ +#cloud-config +# A thin, creds-free, server-class seed (#81): the 'ops' user, tmux (#65), +# and rig — nothing that joins a tailnet or admits credentials, no docker, +# no sshd config, no keys. The server posture comes from +# 'rig bootstrap staging' (heavy-duty/rig#31), which box auto-runs after +# mint; the tailnet workload join holds a pre-auth key and stays +# operator-run ('box shell' → 'sudo rig bootstrap workload'), exactly as +# #69 designed it — box never sees the key. +users: + - name: ops + shell: /bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + lock_passwd: true +package_update: true +# tmux: 'box tmux' runs 'tmux new-session' INSIDE the box (#65) — and the +# operator babysits the workload join through it. +# curl + ca-certificates: the rig installer below rides them, and a bare +# cloud image is not guaranteed to ship either. +packages: + - tmux + - curl + - ca-certificates +runcmd: + # Preinstall rig so the box can converge — and re-converge — via + # 'rig bootstrap staging'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # box substitutes them at mint from the RIG_REPO/RIG_REF environment + # (default heavy-duty/rig @ main — unpinned, tracking main, the same + # honest edge as rig's own unpinned box install, until rig#32 ships a + # release flow). The pin covers both the installer fetched AND the tree + # it installs, so a branch under review is testable end to end. + - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash