feat: thin templates — box mints, rig converges (#81) #88
9 changed files with 226 additions and 190 deletions
104
bin/box
104
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 <role>', 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 <box> Required. The box's name.
|
||||
--template <t> 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 <src>[/<snap>] Clone src's live state, or its snapshot <snap>.
|
||||
|
|
@ -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 <role>'. 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 <role>' 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" </dev/null; then
|
||||
echo >&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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 <box> -- 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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 <b> -- 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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 <b> -- 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
|
||||
|
|
|
|||
24
templates/staging/box.env
Normal file
24
templates/staging/box.env
Normal file
|
|
@ -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 <name> # then: sudo rig bootstrap workload --hostname <name>
|
||||
# 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"
|
||||
31
templates/staging/user-data.yaml
Normal file
31
templates/staging/user-data.yaml
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue