diff --git a/README.md b/README.md index 821d798..5dbf81e 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,20 @@ claude # if the repo has .claudebox/, Claude reads it ## Templates -The claude box is one template among several. A template is a directory under -`templates/`: a `box.env` (image, user, resources — parsed against a strict -allowlist, never sourced) and a `user-data.yaml` (cloud-init, passed to Incus -verbatim). +The claude box is one template among several. What ships today: + +| Template | What's in it | +| --- | --- | +| `blank` | Bare Debian 13 — same isolation, no tooling. The default. | +| `claude` | Claude Code, creds-free (the original claudebox) | +| `codex` | OpenAI Codex CLI, creds-free | +| `grok` | xAI Grok Build CLI, creds-free | + +A template is a directory under `templates/`: a `box.env` (image, user, +resources — parsed against a strict allowlist, never sourced) and a +`user-data.yaml` (cloud-init, passed to Incus verbatim). The coding-CLI +templates are all the same shape — install the CLI, put it on PATH, drop an +agent-context file; none of them carry credentials. ```sh box templates # list what this install can mint diff --git a/drill/drill.sh b/drill/drill.sh index 7a8c13e..cf8961b 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -159,7 +159,7 @@ This will, ON THIS HOST ($(hostname)): · install Incus and a systemd unit · create a network (boxnet), an ACL, and a profile · rewrite firewall rules (nft or UFW, and Docker's DOCKER-USER chain) - · create and destroy instances named: drill, clone, archive, peer, payroll, cbprobe, cbcopy, tpl + · create and destroy instances named: drill, clone, archive, peer, payroll, cbprobe, cbcopy, tpl, codex, grok · mutate the network and profile mid-run to rehearse the #16 hardening Only do this on a machine you can format. EOF @@ -243,7 +243,7 @@ fi inf "clearing anything a previous run left behind…" # One name at a time — 'incus delete -f a b c' aborts at the first MISSING name, # which is how run 2 inherited run 1's boxes and cascaded five false FAILs. -for n in drill clone archive peer payroll cbprobe cbcopy cbnotours tpl; do +for n in drill clone archive peer payroll cbprobe cbcopy cbnotours tpl codex grok; do timeout -k 5 60 incus delete -f "$n" >/dev/null 2>&1 done if incus network show boxnet >/dev/null 2>&1; then @@ -385,8 +385,12 @@ else fi # --- templates: the mint surface is itself a surface to test ---------------- -box templates 2>/dev/null | grep -q '^ blank' && box templates 2>/dev/null | grep -q '^ claude' \ - && ok "templates: lists blank and claude" || no "templates listing is missing a shipped template" +tpl_missing="" +for t in blank claude codex grok; do + box templates 2>/dev/null | grep -q "^ $t" || tpl_missing="$tpl_missing $t" +done +[ -z "$tpl_missing" ] && ok "templates: lists blank, claude, codex, grok" \ + || no "templates listing is missing:$tpl_missing" box new --name tpl --template nosuch 2>&1 | grep -q 'no such template' \ && ok "unknown template refused, points at 'box templates'" || no "an unknown template was not refused" # The one rule that keeps templates honest: no key can name a network. Plant a @@ -427,6 +431,28 @@ else no "blank mint FAILED — tail: $(tail -3 /tmp/mint-tpl.log | tr '\n' ' ')" fi +# The generic mechanic (metadata, placement, user, isolation parity) is proven +# once by blank+claude and needs no per-template repeat. What a NEW template +# still has to prove is its own payload: the CLI installs, lands on the +# non-interactive exec PATH, and answers --version. One mint each. +for t in codex grok; do + case "$t" in codex) bin=codex; user=codex ;; grok) bin=grok-build; user=grok ;; esac + printf '\n minting a %s box (cold — validates the template install)…\n' "$t" + if mint_box "/tmp/mint-$t.log" --name "$t" --template "$t"; then + [ "$(incus config get "$t" user.box.user 2>/dev/null)" = "$user" ] \ + && ok "$t: template user stamped ($user)" || no "$t: user.box.user not $user" + if timeout -k 5 30 box exec "$t" -- "$bin" --version /dev/null 2>&1; then + ok "$t: '$bin --version' answers via box exec — installed and on the non-interactive PATH" + else + no "$t: '$bin --version' FAILED via exec — not installed, or not on exec's PATH (the claude template's #15 bug)" + inf "PATH as exec sees it: $(timeout -k 5 20 box exec "$t" -- printenv PATH /dev/null)" + fi + box rm "$t" --force >/dev/null 2>&1 && ok "$t box removed" || no "$t: could not remove" + else + no "$t mint FAILED — tail: $(tail -3 "/tmp/mint-$t.log" | tr '\n' ' ')" + fi +done + printf '\n minting a claude box (cold, ~10 min)…\n' t0=$SECONDS if mint_box /tmp/mint-drill.log --name drill --template claude; then @@ -678,11 +704,11 @@ if [ "$KEEP" = 1 ]; then inf "note: the D-phase mutations (dns.mode=none, NIC filtering) are still applied" else # every name the drill can have left, whatever branch a partial run took - for n in drill clone archive peer tpl; do box rm "$n" --force >/dev/null 2>&1; done + for n in drill clone archive peer tpl codex grok; do box rm "$n" --force >/dev/null 2>&1; done # Assert OUR boxes are gone — not that the host is empty. The rm loop above # already embodies the discipline (only names the drill minted); demanding # 'no boxes yet' here would flag any pre-existing operator box as a failure. - leftover="$(box list 2>/dev/null | grep -E '^(drill|clone|archive|peer|tpl)([[:space:]]|$)' || true)" + leftover="$(box list 2>/dev/null | grep -E '^(drill|clone|archive|peer|tpl|codex|grok)([[:space:]]|$)' || true)" [ -z "$leftover" ] && ok "teardown: every box the drill minted is gone" \ || no "a drill box survived teardown: $(printf '%s' "$leftover" | awk '{print $1}' | tr '\n' ' ')" fi diff --git a/templates/codex/box.env b/templates/codex/box.env new file mode 100644 index 0000000..08d5e58 --- /dev/null +++ b/templates/codex/box.env @@ -0,0 +1,11 @@ +# The codex template — Debian 13 + OpenAI Codex CLI, creds-free. +# 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_IMAGE="images:debian/13/cloud" +BOX_USER="codex" +BOX_CPU="4" +BOX_MEMORY="8GiB" +BOX_DISK="60GiB" diff --git a/templates/codex/user-data.yaml b/templates/codex/user-data.yaml new file mode 100644 index 0000000..1aa115c --- /dev/null +++ b/templates/codex/user-data.yaml @@ -0,0 +1,60 @@ +#cloud-config +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 + `.claudebox/` folder, 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 +packages: + - git + - gh + - 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 diff --git a/templates/grok/box.env b/templates/grok/box.env new file mode 100644 index 0000000..be5b569 --- /dev/null +++ b/templates/grok/box.env @@ -0,0 +1,11 @@ +# The grok template — Debian 13 + xAI's Grok Build CLI, creds-free. +# 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 Build CLI on Debian 13, creds-free" +BOX_IMAGE="images:debian/13/cloud" +BOX_USER="grok" +BOX_CPU="4" +BOX_MEMORY="8GiB" +BOX_DISK="60GiB" diff --git a/templates/grok/user-data.yaml b/templates/grok/user-data.yaml new file mode 100644 index 0000000..1de6646 --- /dev/null +++ b/templates/grok/user-data.yaml @@ -0,0 +1,59 @@ +#cloud-config +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-build 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 + `.claudebox/` folder, 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 +packages: + - git + - gh + - curl + - ca-certificates + - gnupg + - ripgrep + - jq + - tmux + - age + - unzip + - build-essential +runcmd: + - curl -fsSL https://get.docker.com | sh + - usermod -aG docker grok + # Grok Build's official installer (verified upstream: x.ai/cli). It drops the + # binary under the invoking user's home, so run it AS grok, not root. + - sudo -u grok bash -lc 'curl -fsSL https://x.ai/cli/install.sh | bash' + # 'box exec -- grok-build …' runs a NON-interactive shell that reads no + # rc files. Find whatever the installer dropped and symlink it onto the + # system PATH — the same fix the claude template needed. If the installer's + # binary name changes upstream, this glob is the one line to update. + - | + bin="$(find /home/grok -maxdepth 4 -type f -name 'grok-build' 2>/dev/null | head -1)" + [ -n "$bin" ] && ln -sf "$bin" /usr/local/bin/grok-build || echo "grok-build binary not found post-install — check x.ai/cli install layout" >&2 + - echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/grok/.bashrc