forked from heavy-duty/box
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:
parent
9b3522e4ee
commit
c6bb6cb0e0
6 changed files with 187 additions and 10 deletions
18
README.md
18
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
|
||||
|
|
|
|||
|
|
@ -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 >/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'
|
||||
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
|
||||
|
|
|
|||
11
templates/codex/box.env
Normal file
11
templates/codex/box.env
Normal 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"
|
||||
60
templates/codex/user-data.yaml
Normal file
60
templates/codex/user-data.yaml
Normal 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
11
templates/grok/box.env
Normal 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"
|
||||
59
templates/grok/user-data.yaml
Normal file
59
templates/grok/user-data.yaml
Normal 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
|
||||
Loading…
Reference in a new issue