feat(templates): thin creds-free seeds — box mints, rig converges (#81)
The tenant content that lived in claude/codex/grok's cloud-init — agent CLI installs, docker, node, the per-template agent-context heredocs — moves to rig's bootstrap roles (rig#31), where it is convergent, idempotent and testable end to end. What remains per template is a thin seed: the tenant user, tmux (#65), and rig preinstalled — nothing that joins a tailnet or admits credentials. - BOX_BOOTSTRAP_ROLE: a template names the creds-free rig role cmd_new auto-runs inside the guest after cloud-init settles. The value is a role NAME by allowlist — anything shell-shaped dies at parse time, on the host. A failed role leaves the box up and names the re-run. - render_userdata: the seed's ONE substitution. @RIG_REPO@/@RIG_REF@ resolve from the mint environment (default heavy-duty/rig @ main — unpinned, the honest rig#29 treatment, until rig#32's releases); values are whole-string-validated before touching the YAML, because they land inside a runcmd shell line. - templates/staging: the re-cut of #69's layering — user ops, BOX_REQUIRE_VM=1, BOX_AUTOSTART=1, role staging. The tailnet workload join holds a key and stays operator-run; cmd_new prints it as the next step and box never sees the key. - blank stays a box with nobody home: no rig, no role, nothing auto-runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a4d6388b02
commit
f4f25016d6
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
|
isolation is preserved, and the clone knows its template's user without
|
||||||
being told.
|
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.
|
--name <box> Required. The box's name.
|
||||||
--template <t> Template to mint from; 'box templates' lists them.
|
--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
|
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
|
never the network: every template gets the same
|
||||||
isolation.
|
isolation.
|
||||||
--from <src>[/<snap>] Clone src's live state, or its snapshot <snap>.
|
--from <src>[/<snap>] Clone src's live state, or its snapshot <snap>.
|
||||||
|
|
@ -267,10 +276,15 @@ EOF
|
||||||
templates) cat <<'EOF'
|
templates) cat <<'EOF'
|
||||||
List the templates this install can mint, with their descriptions. A template
|
List the templates this install can mint, with their descriptions. A template
|
||||||
is a directory under templates/: a box.env (image, user, resources, boot
|
is a directory under templates/: a box.env (image, user, resources, boot
|
||||||
demands — parsed against an allowlist, never sourced) and a user-data.yaml
|
demands, tenant role — parsed against an allowlist, never sourced) and a
|
||||||
(cloud-init, passed to Incus verbatim). Templates cannot touch the network
|
user-data.yaml (cloud-init, passed to Incus verbatim except the rig pin
|
||||||
or security flags — the shared box-net profile is the placement contract, so
|
tokens @RIG_REPO@/@RIG_REF@, resolved at mint from the environment).
|
||||||
every template gets the same isolation.
|
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 templates
|
||||||
box new --name scratch --template blank
|
box new --name scratch --template blank
|
||||||
|
|
@ -842,23 +856,26 @@ reset_identity() {
|
||||||
wait_agent "$i"
|
wait_agent "$i"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Templates set image, user, resources, boot demands and cloud-init — NOTHING
|
# Templates set image, user, resources, boot demands, a tenant role and
|
||||||
# else. The box.env file is parsed against this allowlist, never sourced:
|
# cloud-init — NOTHING else. The box.env file is parsed against this allowlist,
|
||||||
# sourcing would hand every template arbitrary bash execution on the HOST at
|
# never sourced: sourcing would hand every template arbitrary bash execution on
|
||||||
# mint time. And there is deliberately no key for a network or a security flag
|
# the HOST at mint time. And there is deliberately no key for a network or a
|
||||||
# — the shared box-net profile is the placement contract, so no template can
|
# security flag — the shared box-net profile is the placement contract, so no
|
||||||
# weaken isolation. 'blank' is a box with nobody home, not a box with the
|
# template can weaken isolation. 'blank' is a box with nobody home, not a box
|
||||||
# safety off. The two boot demands are for server-class templates (#68):
|
# with the safety off. The two boot demands are for server-class templates
|
||||||
# BOX_REQUIRE_VM=1 refuses the container fallback (the VM is the trust
|
# (#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
|
# 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.
|
# 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() {
|
load_template() {
|
||||||
local t="$1" dir line key val
|
local t="$1" dir line key val
|
||||||
dir="$root/templates/$t"
|
dir="$root/templates/$t"
|
||||||
[ -d "$dir" ] || die "no such template: $t (see 'box templates')"
|
[ -d "$dir" ] || die "no such template: $t (see 'box templates')"
|
||||||
[ -f "$dir/box.env" ] || die "template '$t' has no box.env"
|
[ -f "$dir/box.env" ] || die "template '$t' has no box.env"
|
||||||
T_DESC=""; T_IMAGE=""; T_USER=""; T_CPU=""; T_MEMORY=""; T_DISK=""
|
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
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
case "$line" in ''|\#*) continue ;; esac
|
case "$line" in ''|\#*) continue ;; esac
|
||||||
case "$line" in
|
case "$line" in
|
||||||
|
|
@ -882,7 +899,8 @@ load_template() {
|
||||||
BOX_DISK) T_DISK="$val" ;;
|
BOX_DISK) T_DISK="$val" ;;
|
||||||
BOX_REQUIRE_VM) T_REQUIRE_VM="$val" ;;
|
BOX_REQUIRE_VM) T_REQUIRE_VM="$val" ;;
|
||||||
BOX_AUTOSTART) T_AUTOSTART="$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
|
esac
|
||||||
done <"$dir/box.env"
|
done <"$dir/box.env"
|
||||||
# Not 'A && B || die': if T_IMAGE is set but T_USER is not, that idiom still
|
# 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
|
if [ -z "$T_IMAGE" ] || [ -z "$T_USER" ]; then
|
||||||
die "template '$t': BOX_IMAGE and BOX_USER are required"
|
die "template '$t': BOX_IMAGE and BOX_USER are required"
|
||||||
fi
|
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)
|
# Resolution, most specific wins: inline flag (--cpu/--memory/--disk, #57)
|
||||||
# > BOX_* environment (how a small host or the drill shrinks every box it
|
# > BOX_* environment (how a small host or the drill shrinks every box it
|
||||||
# mints) > the template's file > defaults. Values pass to Incus verbatim —
|
# 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}}}"
|
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() {
|
cmd_templates() {
|
||||||
local d t desc
|
local d t desc
|
||||||
echo "TEMPLATES"
|
echo "TEMPLATES"
|
||||||
|
|
@ -969,7 +1018,7 @@ cmd_new() {
|
||||||
--config user.box.user="$T_USER" \
|
--config user.box.user="$T_USER" \
|
||||||
--config limits.cpu="$T_CPU" \
|
--config limits.cpu="$T_CPU" \
|
||||||
--config limits.memory="$T_MEMORY" \
|
--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[@]}"
|
"${extra[@]}"
|
||||||
wait_agent "$instance"
|
wait_agent "$instance"
|
||||||
echo "box: waiting for phase-1 (cloud-init)..."
|
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
|
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"
|
die "cloud-init failed — the box is incomplete, so refusing to hand it over"
|
||||||
fi
|
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
|
fi
|
||||||
# The login hint belongs to the claude template — read the EFFECTIVE
|
# 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
|
# 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
|
[ -z "$eff" ] && [ "$(incus config get "$instance" user.claudebox 2>/dev/null || true)" = 1 ] && eff=claude
|
||||||
if [ "$eff" = claude ]; then
|
if [ "$eff" = claude ]; then
|
||||||
echo "box: ready — 'box shell $name'. Log into Claude inside: run 'claude' then /login."
|
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
|
else
|
||||||
echo "box: ready — 'box shell $name'."
|
echo "box: ready — 'box shell $name'."
|
||||||
fi
|
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="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
|
# 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.
|
# 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
|
# BOX_USER must match the user user-data.yaml creates (the duplication is
|
||||||
# passed to Incus verbatim, so the duplication is deliberate and by hand).
|
# deliberate and by hand) — and it is the tenant user the rig role converges
|
||||||
BOX_DESCRIPTION="Claude Code on Debian 13, creds-free — where this project started"
|
# (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_IMAGE="images:debian/13/cloud"
|
||||||
BOX_USER="claude"
|
BOX_USER="claude"
|
||||||
BOX_CPU="4"
|
BOX_CPU="4"
|
||||||
BOX_MEMORY="8GiB"
|
BOX_MEMORY="8GiB"
|
||||||
BOX_DISK="60GiB"
|
BOX_DISK="60GiB"
|
||||||
|
BOX_BOOTSTRAP_ROLE="claude"
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,30 @@
|
||||||
#cloud-config
|
#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:
|
users:
|
||||||
- name: claude
|
- name: claude
|
||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
lock_passwd: true
|
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
|
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:
|
packages:
|
||||||
- git
|
- tmux
|
||||||
- gh
|
|
||||||
- curl
|
- curl
|
||||||
- ca-certificates
|
- ca-certificates
|
||||||
- gnupg
|
|
||||||
- ripgrep
|
|
||||||
- jq
|
|
||||||
- tmux
|
|
||||||
- age
|
|
||||||
- unzip
|
|
||||||
- build-essential
|
|
||||||
- zsh
|
|
||||||
runcmd:
|
runcmd:
|
||||||
- curl -fsSL https://get.docker.com | sh
|
# Preinstall rig so the box can converge — and re-converge — via
|
||||||
- usermod -aG docker claude
|
# 'rig bootstrap claude'. @RIG_REPO@/@RIG_REF@ are the pin point (#81):
|
||||||
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
# box substitutes them at mint from the RIG_REPO/RIG_REF environment
|
||||||
- apt-get install -y nodejs
|
# (default heavy-duty/rig @ main — unpinned, tracking main, the same
|
||||||
- sudo -u claude bash -lc 'curl -fsSL https://claude.ai/install.sh | bash'
|
# honest edge as rig's own unpinned box install, until rig#32 ships a
|
||||||
# 'box exec <box> -- claude …' runs through a NON-interactive shell, which
|
# release flow). The pin covers both the installer fetched AND the tree
|
||||||
# reads no .bashrc/.zshrc — a PATH export there is invisible to it. The
|
# it installs, so a branch under review is testable end to end.
|
||||||
# symlink makes claude reachable from every shell, interactive or not.
|
- curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash
|
||||||
- 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
|
|
||||||
|
|
|
||||||
|
|
@ -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="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
|
# 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.
|
# profile is the placement contract and no template can weaken it.
|
||||||
# BOX_USER must match the user user-data.yaml creates.
|
# BOX_USER must match the user user-data.yaml creates (the duplication is
|
||||||
BOX_DESCRIPTION="OpenAI Codex CLI on Debian 13, creds-free"
|
# 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_IMAGE="images:debian/13/cloud"
|
||||||
BOX_USER="codex"
|
BOX_USER="codex"
|
||||||
BOX_CPU="4"
|
BOX_CPU="4"
|
||||||
BOX_MEMORY="8GiB"
|
BOX_MEMORY="8GiB"
|
||||||
BOX_DISK="60GiB"
|
BOX_DISK="60GiB"
|
||||||
|
BOX_BOOTSTRAP_ROLE="codex"
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,30 @@
|
||||||
#cloud-config
|
#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:
|
users:
|
||||||
- name: codex
|
- name: codex
|
||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
lock_passwd: true
|
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
|
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:
|
packages:
|
||||||
- git
|
- tmux
|
||||||
- gh
|
|
||||||
- curl
|
- curl
|
||||||
- ca-certificates
|
- ca-certificates
|
||||||
- gnupg
|
|
||||||
- ripgrep
|
|
||||||
- jq
|
|
||||||
- tmux
|
|
||||||
- age
|
|
||||||
- unzip
|
|
||||||
- build-essential
|
|
||||||
runcmd:
|
runcmd:
|
||||||
- curl -fsSL https://get.docker.com | sh
|
# Preinstall rig so the box can converge — and re-converge — via
|
||||||
- usermod -aG docker codex
|
# 'rig bootstrap codex'. @RIG_REPO@/@RIG_REF@ are the pin point (#81):
|
||||||
# Codex CLI is an npm global and needs Node 22+ (verified upstream:
|
# box substitutes them at mint from the RIG_REPO/RIG_REF environment
|
||||||
# npmjs.com/package/@openai/codex — the SCOPED @openai/codex, not the
|
# (default heavy-duty/rig @ main — unpinned, tracking main, the same
|
||||||
# unrelated 2012 'codex' package).
|
# honest edge as rig's own unpinned box install, until rig#32 ships a
|
||||||
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
# release flow). The pin covers both the installer fetched AND the tree
|
||||||
- apt-get install -y nodejs
|
# it installs, so a branch under review is testable end to end.
|
||||||
- npm install -g @openai/codex
|
- curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash
|
||||||
# '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
|
|
||||||
|
|
|
||||||
|
|
@ -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="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
|
# 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.
|
# profile is the placement contract and no template can weaken it.
|
||||||
# BOX_USER must match the user user-data.yaml creates.
|
# BOX_USER must match the user user-data.yaml creates (the duplication is
|
||||||
BOX_DESCRIPTION="xAI Grok CLI on Debian 13, creds-free"
|
# 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_IMAGE="images:debian/13/cloud"
|
||||||
BOX_USER="grok"
|
BOX_USER="grok"
|
||||||
BOX_CPU="4"
|
BOX_CPU="4"
|
||||||
BOX_MEMORY="8GiB"
|
BOX_MEMORY="8GiB"
|
||||||
BOX_DISK="60GiB"
|
BOX_DISK="60GiB"
|
||||||
|
BOX_BOOTSTRAP_ROLE="grok"
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,30 @@
|
||||||
#cloud-config
|
#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:
|
users:
|
||||||
- name: grok
|
- name: grok
|
||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
lock_passwd: true
|
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
|
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:
|
packages:
|
||||||
- git
|
- tmux
|
||||||
- gh
|
|
||||||
- curl
|
- curl
|
||||||
- ca-certificates
|
- ca-certificates
|
||||||
- gnupg
|
|
||||||
- ripgrep
|
|
||||||
- jq
|
|
||||||
- tmux
|
|
||||||
- age
|
|
||||||
- unzip
|
|
||||||
- build-essential
|
|
||||||
runcmd:
|
runcmd:
|
||||||
- curl -fsSL https://get.docker.com | sh
|
# Preinstall rig so the box can converge — and re-converge — via
|
||||||
- usermod -aG docker grok
|
# 'rig bootstrap grok'. @RIG_REPO@/@RIG_REF@ are the pin point (#81):
|
||||||
# The OFFICIAL installer, read at https://x.ai/cli/install.sh rather than
|
# box substitutes them at mint from the RIG_REPO/RIG_REF environment
|
||||||
# guessed at. What it actually does:
|
# (default heavy-duty/rig @ main — unpinned, tracking main, the same
|
||||||
# · installs the CLI as `grok` (with an `agent` alias) — NOT `grok-build`
|
# honest edge as rig's own unpinned box install, until rig#32 ships a
|
||||||
# · BIN_DIR defaults to $HOME/.grok/bin, and what it puts there is a
|
# release flow). The pin covers both the installer fetched AND the tree
|
||||||
# SYMLINK into its versioned download dir (so `find -type f` misses it)
|
# it installs, so a branch under review is testable end to end.
|
||||||
# · GROK_BIN_DIR overrides that directory
|
- curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
|
||||||
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