From 5bf0acd940d953183669e58a4221091b907a9bfb Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Wed, 22 Jul 2026 15:02:49 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20kimi-box=20joins=20the=20box=20tenant?= =?UTF-8?q?=20roles=20=E2=80=94=20the=20Kimi=20CLI=20agent=20guest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /AGENTS.md convention — with an honest note that kimi documents only project-level AGENTS.md today. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++++ README.md | 11 ++++++----- bin/rig | 4 ++-- commands/bootstrap-tenant.sh | 30 +++++++++++++++++++++++------- commands/bootstrap.sh | 10 +++++----- commands/lib/tenant-config.sh | 18 ++++++++++++++++-- test/cli.sh | 8 ++++++-- 7 files changed, 62 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a967fe9..fdfa2b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 6c453fd..cd6c7c4 100644 --- a/README.md +++ b/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 ` — the box tenants +### `rig bootstrap ` — 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 diff --git a/bin/rig b/bin/rig index ec74693..8367638 100755 --- a/bin/rig +++ b/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 [--user ] + bootstrap [--user ] 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 diff --git a/commands/bootstrap-tenant.sh b/commands/bootstrap-tenant.sh index af78b2d..17a6ccb 100755 --- a/commands/bootstrap-tenant.sh +++ b/commands/bootstrap-tenant.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# rig bootstrap — the box TENANT +# rig bootstrap — 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 [--user ] +usage: rig bootstrap [--user ] 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 diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 92ce3af..b39554b 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -29,7 +29,7 @@ usage: rig bootstrap | --no-users) [--hostname ] [--root-door ] [--host ] [--join ] - rig bootstrap [--user ] + rig bootstrap [--user ] (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 ` # 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). diff --git a/commands/lib/tenant-config.sh b/commands/lib/tenant-config.sh index 7c6938c..2b3760b 100644 --- a/commands/lib/tenant-config.sh +++ b/commands/lib/tenant-config.sh @@ -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 /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 — 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 <` 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