feat: codex and grok templates — the mechanic's second and third tenants

Two coding-CLI templates mirroring claude's shape: a box.env + verbatim
cloud-init, inheriting the box-net placement contract structurally, no
new design.

- codex: OpenAI Codex CLI via 'npm i -g @openai/codex' (the SCOPED
  package; needs Node 22), symlinked onto the non-interactive exec PATH
  via 'npm prefix -g' — the same PATH fix the claude template needed.
- grok: xAI Grok Build via the official 'curl x.ai/cli/install.sh',
  run AS the grok user (the installer drops into $HOME); the binary is
  found and symlinked to /usr/local/bin.

Install commands verified upstream at implementation time, per the
issue's rule (npmjs.com/package/@openai/codex, x.ai/cli). Each gets an
AGENTS.md-style context file telling the agent it lives in a
disposable, isolated, creds-free box.

Drill: templates listing now expects four; a compact per-template smoke
(mint, '<cli> --version' via box exec, remove) validates each payload
installs and lands on the exec PATH — the generic mechanic is already
proven by blank+claude and not repeated.

Closes #54
This commit is contained in:
claude-hdb 2026-07-14 15:59:39 +00:00
parent 9b3522e4ee
commit c6bb6cb0e0
6 changed files with 187 additions and 10 deletions

View file

@ -75,10 +75,20 @@ claude # if the repo has .claudebox/, Claude reads it
## Templates ## Templates
The claude box is one template among several. A template is a directory under The claude box is one template among several. What ships today:
`templates/`: a `box.env` (image, user, resources — parsed against a strict
allowlist, never sourced) and a `user-data.yaml` (cloud-init, passed to Incus | Template | What's in it |
verbatim). | --- | --- |
| `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 ```sh
box templates # list what this install can mint box templates # list what this install can mint

View file

@ -159,7 +159,7 @@ This will, ON THIS HOST ($(hostname)):
· install Incus and a systemd unit · install Incus and a systemd unit
· create a network (boxnet), an ACL, and a profile · create a network (boxnet), an ACL, and a profile
· rewrite firewall rules (nft or UFW, and Docker's DOCKER-USER chain) · 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 · mutate the network and profile mid-run to rehearse the #16 hardening
Only do this on a machine you can format. Only do this on a machine you can format.
EOF EOF
@ -243,7 +243,7 @@ fi
inf "clearing anything a previous run left behind…" 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, # 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. # 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 timeout -k 5 60 incus delete -f "$n" >/dev/null 2>&1
done done
if incus network show boxnet >/dev/null 2>&1; then if incus network show boxnet >/dev/null 2>&1; then
@ -385,8 +385,12 @@ else
fi fi
# --- templates: the mint surface is itself a surface to test ---------------- # --- 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' \ tpl_missing=""
&& ok "templates: lists blank and claude" || no "templates listing is missing a shipped template" 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' \ 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" && 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 # 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' ' ')" no "blank mint FAILED — tail: $(tail -3 /tmp/mint-tpl.log | tr '\n' ' ')"
fi 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 >/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 2>/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' printf '\n minting a claude box (cold, ~10 min)…\n'
t0=$SECONDS t0=$SECONDS
if mint_box /tmp/mint-drill.log --name drill --template claude; then 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" inf "note: the D-phase mutations (dns.mode=none, NIC filtering) are still applied"
else else
# every name the drill can have left, whatever branch a partial run took # 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 # 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 # already embodies the discipline (only names the drill minted); demanding
# 'no boxes yet' here would flag any pre-existing operator box as a failure. # '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" \ [ -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' ' ')" || no "a drill box survived teardown: $(printf '%s' "$leftover" | awk '{print $1}' | tr '\n' ' ')"
fi fi

11
templates/codex/box.env Normal file
View file

@ -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"

View file

@ -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 <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

11
templates/grok/box.env Normal file
View file

@ -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"

View file

@ -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 <b> -- 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