feat: kimi-box joins the box tenant roles — the Kimi CLI agent guest
The fourth agent tenant, through the one parameterized mechanism (lib/tenant-config.sh + bootstrap-tenant.sh) — never a fifth hand-maintained copy. The official installer (code.kimi.com/install.sh) is uv-managed and lands kimi in ~/.local/bin, run as the tenant user (grok's lesson: a root-owned install under a 0700 home is a CLI that exists and cannot run); no node, uv brings its own Python. The context file lands at ~/.kimi/AGENTS.md — the <dotdir>/AGENTS.md convention — with an honest note that kimi documents only project-level AGENTS.md today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f8a0e0885
commit
5bf0acd940
7 changed files with 62 additions and 23 deletions
|
|
@ -6,6 +6,10 @@ on the way to cutting its first release, and this file starts there.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109)
|
||||
|
||||
## 0.3.0 — 2026-07-21
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -4,7 +4,7 @@ A CLI that turns a **pristine Debian server into a hardened, tailnet-joined
|
|||
node** — one curl, one command. A second command installs a version-pinned
|
||||
Coolify on a control-plane box. And inside a [box](https://github.com/heavy-duty/box)-minted
|
||||
guest, the same verb converges the **box tenants** — claude-box, codex-box,
|
||||
grok-box, staging-box — from thin, creds-free seeds (see *the box tenants*
|
||||
grok-box, kimi-box, staging-box — from thin, creds-free seeds (see *the box tenants*
|
||||
below).
|
||||
|
||||
Philosophy (shared with [box](https://github.com/heavy-duty/box)):
|
||||
|
|
@ -91,7 +91,7 @@ itself is untouched — what bootstrap converged stays converged.
|
|||
|
||||
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
|
||||
second run changes nothing. (The box TENANT roles — `claude-box`, `codex-box`,
|
||||
`grok-box`, `staging-box` — share the verb but are their own family; the
|
||||
`grok-box`, `kimi-box`, `staging-box` — share the verb but are their own family; the
|
||||
`-box` suffix says so. See *the box tenants* below.)
|
||||
|
||||
```sh
|
||||
|
|
@ -167,7 +167,7 @@ trait), and every other way that step can fail lands in apply's existing
|
|||
refusal at the end.
|
||||
|
||||
`--users` does **not** reach the box TENANT roles (`claude-box`, `codex-box`,
|
||||
`grok-box`, `staging-box`). A tenant is a box-minted *guest*: box auto-runs its bootstrap at
|
||||
`grok-box`, `kimi-box`, `staging-box`). A tenant is a box-minted *guest*: box auto-runs its bootstrap at
|
||||
mint, non-interactively, with no file to hand it; the guest never joins the
|
||||
tailnet and has no SSH door of its own — you enter with `box shell`, gated by
|
||||
the **host's** `incus` grants, which the host's own users file already
|
||||
|
|
@ -382,13 +382,13 @@ unattended VM-host appliance) — and `workstation` is the machine at the keyboa
|
|||
end of all the SSH connections: `root-door=closed`, `join=login`, entering the
|
||||
tailnet as *your* device rather than the fleet's.
|
||||
|
||||
### `rig bootstrap <claude-box|codex-box|grok-box|staging-box>` — the box tenants
|
||||
### `rig bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box>` — the box tenants
|
||||
|
||||
Run as root, **inside** a [box](https://github.com/heavy-duty/box)-minted
|
||||
guest. Convergent — safe to re-run; a second run changes nothing.
|
||||
|
||||
```sh
|
||||
rig bootstrap claude-box # or codex-box, grok-box — the agent tenants
|
||||
rig bootstrap claude-box # or codex-box, grok-box, kimi-box — the agent tenants
|
||||
rig bootstrap staging-box # the server tenant (docker + sshd hardening)
|
||||
rig bootstrap claude-box --user dev # when the seed's BOX_USER differs
|
||||
```
|
||||
|
|
@ -411,6 +411,7 @@ holds the whole per-tenant table), not four hand-maintained scripts:
|
|||
| `claude-box` | `claude` | the agent toolbelt (git, gh, tmux, ripgrep, jq, age, unzip, build-essential), docker, node 22, the Claude Code CLI on the system PATH, zsh + oh-my-zsh, and `~/.claude/CLAUDE.md` |
|
||||
| `codex-box` | `codex` | the toolbelt, docker, node 22, `@openai/codex` on the system PATH, and `~/.codex/AGENTS.md` |
|
||||
| `grok-box` | `grok` | the toolbelt, docker, the grok CLI on the system PATH, and `~/.grok/AGENTS.md` |
|
||||
| `kimi-box` | `kimi` | the toolbelt, docker, the kimi CLI (uv-managed) on the system PATH, and `~/.kimi/AGENTS.md` |
|
||||
| `staging-box` | `ops` | box#69's server posture: docker + the same sshd hardening the machine roles get (shared `lib/sshd.sh`, `root-door=open` acceptance) |
|
||||
|
||||
**The role carries the suffix; the user does not.** A tenant user is the
|
||||
|
|
|
|||
4
bin/rig
4
bin/rig
|
|
@ -33,10 +33,10 @@ commands:
|
|||
tenant roles); custom and workstation take none. join=login
|
||||
(workstation) needs no key: interactive login, node must come up
|
||||
untagged. Run as root.
|
||||
bootstrap <claude-box|codex-box|grok-box|staging-box> [--user <name>]
|
||||
bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>]
|
||||
The box TENANT roles: converge a box-minted guest. The '-box' suffix
|
||||
names the family (a guest, vs the '-server' machine roles above).
|
||||
claude-box|codex-box|grok-box land the agent toolbelt (git, gh, tmux, docker), the agent's CLI on
|
||||
claude-box|codex-box|grok-box|kimi-box land the agent toolbelt (git, gh, tmux, docker), the agent's CLI on
|
||||
the system PATH, and the agent-context file (with the box#80 guard:
|
||||
never run box setup-host inside a box). staging-box lands box#69's
|
||||
server posture — docker + sshd hardening; its tailnet join stays
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# rig bootstrap <claude-box|codex-box|grok-box|staging-box> — the box TENANT
|
||||
# rig bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> — the box TENANT
|
||||
# roles ('-box' names the family: a guest, vs the '-server' machine roles): what a
|
||||
# box-minted guest becomes (issue #31). box mints the thin, creds-free seed
|
||||
# (base image, user, rig preinstalled — heavy-duty/box#81); rig converges the
|
||||
|
|
@ -32,13 +32,13 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
usage: rig bootstrap <claude-box|codex-box|grok-box|staging-box> [--user <name>]
|
||||
usage: rig bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>]
|
||||
|
||||
Box TENANT roles — what a box-minted guest becomes. box mints the thin,
|
||||
creds-free seed (base image, user, rig preinstalled); this converges the
|
||||
tenant on top, and re-runs converge an existing box to a new spec.
|
||||
|
||||
claude-box|codex-box|grok-box
|
||||
claude-box|codex-box|grok-box|kimi-box
|
||||
the agent tenants: base tooling (git, gh, tmux, …),
|
||||
docker, the agent's CLI on the system PATH, and the
|
||||
agent-context file — including the box#80 guard: never
|
||||
|
|
@ -63,10 +63,10 @@ EOF
|
|||
# --- args (validated before the root check, so errors are testable) ---------
|
||||
ROLE="${1:-}"
|
||||
case "$ROLE" in
|
||||
claude-box|codex-box|grok-box|staging-box) shift ;;
|
||||
claude-box|codex-box|grok-box|kimi-box|staging-box) shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
"") usage >&2; die "tenant role required (claude-box|codex-box|grok-box|staging-box)" 2 ;;
|
||||
*) die "unknown tenant role: $ROLE (want claude-box|codex-box|grok-box|staging-box)" 2 ;;
|
||||
"") usage >&2; die "tenant role required (claude-box|codex-box|grok-box|kimi-box|staging-box)" 2 ;;
|
||||
*) die "unknown tenant role: $ROLE (want claude-box|codex-box|grok-box|kimi-box|staging-box)" 2 ;;
|
||||
esac
|
||||
|
||||
TENANT_USER="$(tenant_user "$ROLE")"
|
||||
|
|
@ -183,7 +183,7 @@ case "$ROLE" in
|
|||
# The claude-box tenant keeps zsh (its shell UX ships with the box); the
|
||||
# remaining list is the shared agent toolbelt the templates carried.
|
||||
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential zsh ;;
|
||||
codex-box|grok-box)
|
||||
codex-box|grok-box|kimi-box)
|
||||
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential ;;
|
||||
staging-box)
|
||||
# openssh-server: the hardening drop-in below targets /etc/ssh/sshd_config.d/,
|
||||
|
|
@ -291,6 +291,20 @@ case "$ROLE" in
|
|||
else
|
||||
log "grok CLI already installed"
|
||||
fi ;;
|
||||
kimi-box)
|
||||
# The OFFICIAL installer (code.kimi.com/install.sh): a uv-managed Python
|
||||
# tool (kimi-cli), landing `kimi` in ~/.local/bin — uv's tool bin — with
|
||||
# uv bringing its own managed CPython, so no apt python pin here (the
|
||||
# node section above stays claude/codex-only for the same reason). Run AS
|
||||
# the tenant user, never root: grok's lesson — a root-owned install under
|
||||
# a 0700 home is a CLI that exists and cannot run.
|
||||
CLI=kimi CLI_SRC="$TENANT_HOME/.local/bin/kimi"
|
||||
if [ ! -e "$CLI_SRC" ]; then
|
||||
log "installing the Kimi CLI as ${TENANT_USER}"
|
||||
runuser -l "$TENANT_USER" -c 'curl -LsSf https://code.kimi.com/install.sh | bash'
|
||||
else
|
||||
log "kimi CLI already installed"
|
||||
fi ;;
|
||||
staging-box) ;; # no agent lives on the staging-box tenant
|
||||
esac
|
||||
if [ -n "$CLI" ]; then
|
||||
|
|
@ -313,6 +327,8 @@ if [ -n "$CLI" ]; then
|
|||
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$(npm prefix -g)/bin:$PATH"' ;;
|
||||
grok-box)
|
||||
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.grok/bin:$PATH"' ;;
|
||||
kimi-box)
|
||||
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.local/bin:$PATH"' ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ usage: rig bootstrap <control-plane-server|workload-server|runner-server|
|
|||
(--users <path> | --no-users)
|
||||
[--hostname <name>] [--root-door <closed|open>]
|
||||
[--host <yes|no>] [--join <authkey|login>]
|
||||
rig bootstrap <claude-box|codex-box|grok-box|staging-box> [--user <name>]
|
||||
rig bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>]
|
||||
(the box TENANT roles — see their own --help; they take
|
||||
no --users, see below)
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ and per-human accounts keep attribution intact for the times someone does go
|
|||
in. So the complete path is the default path and skipping it is a deliberate
|
||||
--no-users, not an omission.
|
||||
|
||||
--users does NOT reach the box TENANT roles (claude-box|codex-box|grok-box|staging-box). A
|
||||
--users does NOT reach the box TENANT roles (claude-box|codex-box|grok-box|kimi-box|staging-box). A
|
||||
tenant is a box-minted GUEST: box auto-runs its bootstrap at mint,
|
||||
non-interactively, with no file to hand it; the guest never joins the tailnet
|
||||
and has no SSH door of its own — entry is `box shell`, gated by the HOST's
|
||||
|
|
@ -115,15 +115,15 @@ EOF
|
|||
ROLE="${1:-}"
|
||||
case "$ROLE" in
|
||||
control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom) shift ;;
|
||||
claude-box|codex-box|grok-box|staging-box)
|
||||
claude-box|codex-box|grok-box|kimi-box|staging-box)
|
||||
# The box TENANT roles (#31) are a different family — guests a box mints,
|
||||
# never tailnet machines — and live in their own mechanism, one script
|
||||
# parameterized per tenant. Dispatched here so `rig bootstrap <role>`
|
||||
# stays the single entrypoint for both families.
|
||||
exec "$HERE/bootstrap-tenant.sh" "$@" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
"") usage >&2; die "role required (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude-box|codex-box|grok-box|staging-box)" 2 ;;
|
||||
*) die "unknown role: $ROLE (want control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude-box|codex-box|grok-box|staging-box)" 2 ;;
|
||||
"") usage >&2; die "role required (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude-box|codex-box|grok-box|kimi-box|staging-box)" 2 ;;
|
||||
*) die "unknown role: $ROLE (want control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude-box|codex-box|grok-box|kimi-box|staging-box)" 2 ;;
|
||||
esac
|
||||
|
||||
# Role→traits map — the single place a role's shape is declared (issue #26).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# Shared parameters for the box TENANT roles (claude-box, codex-box, grok-box,
|
||||
# staging-box) — the '-box' suffix names the FAMILY (a guest a box mints, vs the
|
||||
# kimi-box, staging-box) — the '-box' suffix names the FAMILY (a guest a box mints, vs the
|
||||
# '-server' machine roles rig converges); see #76.
|
||||
# sourced by bootstrap-tenant.sh and by the test harness. Pure text→text, no
|
||||
# side effects: the per-tenant differences live HERE, in one table, so the
|
||||
|
|
@ -15,6 +15,7 @@ tenant_user() {
|
|||
claude-box) printf 'claude' ;;
|
||||
codex-box) printf 'codex' ;;
|
||||
grok-box) printf 'grok' ;;
|
||||
kimi-box) printf 'kimi' ;;
|
||||
staging-box) printf 'ops' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
|
@ -29,12 +30,18 @@ tenant_context_path() {
|
|||
claude-box) printf '%s/.claude/CLAUDE.md' "$2" ;;
|
||||
codex-box) printf '%s/.codex/AGENTS.md' "$2" ;;
|
||||
grok-box) printf '%s/.grok/AGENTS.md' "$2" ;;
|
||||
# kimi documents only PROJECT-level AGENTS.md today (no global file); its
|
||||
# dotdir is ~/.kimi (config.toml, sessions/, credentials/). The context
|
||||
# file lands at the <dotdir>/AGENTS.md convention the other CLIs converged
|
||||
# on, so it is where an operator (or a future global-read) will look — an
|
||||
# honest placement, not a claim that the CLI auto-loads it.
|
||||
kimi-box) printf '%s/.kimi/AGENTS.md' "$2" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# render_tenant_context <role> — the agent-context file's content, on stdout.
|
||||
# One renderer for all three agents: only the creds paragraph is per-vendor,
|
||||
# One renderer for all four agents: only the creds paragraph is per-vendor,
|
||||
# and the box#80 guard note lives HERE once — never copy-pasted per template.
|
||||
# staging-box renders nothing (return 1): no agent lives there.
|
||||
render_tenant_context() {
|
||||
|
|
@ -63,6 +70,13 @@ render_tenant_context() {
|
|||
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.' ;;
|
||||
kimi-box)
|
||||
creds='- **Creds-free by default.** The box starts with no Moonshot and no git
|
||||
credentials. If you need to authenticate, the operator runs `kimi` and
|
||||
its `/login` flow interactively (Kimi Code OAuth, or an API key). 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.' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
cat <<EOF
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ else
|
|||
echo "skip: bootstrap non-root refusals (running as root)"
|
||||
fi
|
||||
|
||||
# --- box tenant roles (#31/#76): claude-box|codex-box|grok-box|staging-box ---
|
||||
# --- box tenant roles (#31/#76): claude-box|codex-box|grok-box|kimi-box|staging-box ---
|
||||
# What a box-minted guest becomes — ONE mechanism (bootstrap-tenant.sh),
|
||||
# parameterized per tenant through lib/tenant-config.sh, dispatched from
|
||||
# bootstrap.sh so `rig bootstrap <role>` stays the single entrypoint. The real
|
||||
|
|
@ -530,7 +530,7 @@ check "tenant: --hostname dies the same way" 2 "have no traits" \
|
|||
"$ROOT/commands/bootstrap-tenant.sh" staging-box --hostname my-guest
|
||||
# Dispatch: the machine-role entrypoint hands tenant roles to the tenant
|
||||
# mechanism with args intact (--help reaching the TENANT usage proves both).
|
||||
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude-box|codex-box|grok-box|staging-box" \
|
||||
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude-box|codex-box|grok-box|kimi-box|staging-box" \
|
||||
"$ROOT/commands/bootstrap.sh" claude-box --help
|
||||
# The marker guard fires BEFORE the root check (repo precedent: the coolify
|
||||
# marker warning), so the refusals are provable here off fixture markers. A
|
||||
|
|
@ -603,22 +603,26 @@ tpath() { bash -c 'set -euo pipefail
|
|||
tctx() { bash -c 'set -euo pipefail
|
||||
. "$1/commands/lib/tenant-config.sh"; render_tenant_context "$2"' _ "$ROOT" "$1"; }
|
||||
check "tenant params: agent users are named after their agent" 0 "claude" tuser claude-box
|
||||
check "tenant params: kimi's user drops the suffix too" 0 "kimi" tuser kimi-box
|
||||
check "tenant params: staging's user is box#69's ops" 0 "ops" tuser staging-box
|
||||
check "tenant params: claude context lands in ~/.claude/CLAUDE.md" 0 "/home/claude/.claude/CLAUDE.md" tpath claude-box /home/claude
|
||||
check "tenant params: codex context lands in ~/.codex/AGENTS.md" 0 "/home/codex/.codex/AGENTS.md" tpath codex-box /home/codex
|
||||
check "tenant params: grok context lands in ~/.grok/AGENTS.md" 0 "/home/grok/.grok/AGENTS.md" tpath grok-box /home/grok
|
||||
check "tenant params: kimi context lands in ~/.kimi/AGENTS.md" 0 "/home/kimi/.kimi/AGENTS.md" tpath kimi-box /home/kimi
|
||||
check "tenant params: staging has no context file" 1 "" tpath staging-box /home/ops
|
||||
# The box#80 guard note lives ONCE, in the renderer, and every agent's file
|
||||
# carries it — the layering decision's whole point: never per-template again.
|
||||
check "tenant context: claude carries the box#80 guard" 0 "box setup-host" tctx claude-box
|
||||
check "tenant context: codex carries the box#80 guard" 0 "box setup-host" tctx codex-box
|
||||
check "tenant context: grok carries the box#80 guard" 0 "box setup-host" tctx grok-box
|
||||
check "tenant context: kimi carries the box#80 guard" 0 "box setup-host" tctx kimi-box
|
||||
check "tenant context: the guard says whose host this is not" 0 "not a host you own" tctx claude-box
|
||||
check "tenant context: the guard cites box#80" 0 "box#80" tctx claude-box
|
||||
check "tenant context: the creds-free contract is stated" 0 "Creds-free by default" tctx claude-box
|
||||
check "tenant context: claude names /login as the operator's flow" 0 "/login" tctx claude-box
|
||||
check "tenant context: codex names its login flow" 0 "login flow (\`codex\`)" tctx codex-box
|
||||
check "tenant context: grok names its login flow" 0 "grok login" tctx grok-box
|
||||
check "tenant context: kimi names its login flow" 0 "Kimi Code OAuth" tctx kimi-box
|
||||
check "tenant context: staging renders nothing (no agent lives there)" 1 "" tctx staging-box
|
||||
# Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal
|
||||
# idiom): nothing in the tenant mechanism touches the tailnet, prompts, or
|
||||
|
|
|
|||
Loading…
Reference in a new issue