feat(bootstrap)!: box tenant roles carry a -box suffix
The other half of #76. claude -> claude-box, codex -> codex-box, grok -> grok-box, staging -> staging-box, so a role name always says which family it belongs to: -server builds a fleet machine, -box converges a guest a box minted. With both halves in, the two families can no longer collide on a word the way `staging` did. The role carries the suffix; nothing inside the guest does. A tenant user is the account the box SEED created (BOX_USER) and each agent CLI reads its own dotdir, so claude-box still converges the `claude` user and still writes ~/.claude/CLAUDE.md. Every rename here is a $ROLE comparison or a case arm -- no CLI binary name, no dotdir path, and no account moved. README's tenant table now shows role and user in adjacent columns, because that distinction stopped being cosmetic the moment they differed. Hard cut, no aliases. The old names are refused as unknown at BOTH entrypoints -- `rig bootstrap <name>` and bootstrap-tenant.sh directly -- and the suite asserts each of the four at each, because bootstrap.sh keeps its own dispatch list and a name could survive in one and not the other. An alias left in for a single tenant is the shape that survives review: the taxonomy reads complete while one old name still quietly converges. The consequence is cross-repo. A seed carrying BOX_BOOTSTRAP_ROLE="claude" now fails its own mint-time bootstrap, so heavy-duty/box#123 updates the seeds and must land after this. Closes #76 (tenant half) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
00f6351e28
commit
94d9628766
7 changed files with 201 additions and 152 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -8,6 +8,29 @@ on the way to cutting its first release, and this file starts there.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING: the box tenant roles carry a `-box` suffix** (#76) — the other
|
||||||
|
half of the rename below. `claude` → `claude-box`, `codex` → `codex-box`,
|
||||||
|
`grok` → `grok-box`, `staging` → `staging-box`, so a role name always says
|
||||||
|
which family it belongs to: `-server` builds a fleet machine, `-box`
|
||||||
|
converges a guest a box minted.
|
||||||
|
|
||||||
|
**The role carries the suffix; nothing inside the guest does.** A tenant user
|
||||||
|
is the account the box *seed* created (`BOX_USER`) and each agent CLI reads
|
||||||
|
its own dotdir, so `claude-box` still converges the `claude` user and still
|
||||||
|
writes `~/.claude/CLAUDE.md`. The suffix is rig's word for "this is a guest",
|
||||||
|
not a rename of anything the guest contains — no path, no account, and no CLI
|
||||||
|
binary moved.
|
||||||
|
|
||||||
|
**Migration: hard cut, no aliases**, same as the machine roles. The old names
|
||||||
|
are refused as unknown tenant roles at both entrypoints — `rig bootstrap
|
||||||
|
<name>` and the tenant script directly — and the suite asserts each one at
|
||||||
|
both, because an alias left in for a single tenant is exactly the shape that
|
||||||
|
survives review: the taxonomy reads complete while one old name still quietly
|
||||||
|
converges. The practical consequence is cross-repo: a box seed carrying
|
||||||
|
`BOX_BOOTSTRAP_ROLE="claude"` now fails its own mint-time bootstrap, so
|
||||||
|
heavy-duty/box#125 (closing heavy-duty/box#123) updates the seeds and must
|
||||||
|
land after this.
|
||||||
|
|
||||||
- **BREAKING: machine roles carry a `-server` suffix, and the VM host gets its
|
- **BREAKING: machine roles carry a `-server` suffix, and the VM host gets its
|
||||||
name back** (#76) — rig builds two kinds of thing that sit on opposite sides
|
name back** (#76) — rig builds two kinds of thing that sit on opposite sides
|
||||||
of a trust boundary: tailnet **machines** it converges, and **guests** a box
|
of a trust boundary: tailnet **machines** it converges, and **guests** a box
|
||||||
|
|
|
||||||
49
README.md
49
README.md
|
|
@ -3,8 +3,9 @@
|
||||||
A CLI that turns a **pristine Debian server into a hardened, tailnet-joined
|
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
|
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
|
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, codex, grok,
|
guest, the same verb converges the **box tenants** — claude-box, codex-box,
|
||||||
staging — from thin, creds-free seeds (see *the box tenants* below).
|
grok-box, staging-box — from thin, creds-free seeds (see *the box tenants*
|
||||||
|
below).
|
||||||
|
|
||||||
Philosophy (shared with [box](https://github.com/heavy-duty/box)):
|
Philosophy (shared with [box](https://github.com/heavy-duty/box)):
|
||||||
**public tool, private state**. rig carries plumbing logic only — no
|
**public tool, private state**. rig carries plumbing logic only — no
|
||||||
|
|
@ -93,9 +94,9 @@ itself is untouched — what bootstrap converged stays converged.
|
||||||
### `rig bootstrap <control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom>`
|
### `rig bootstrap <control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom>`
|
||||||
|
|
||||||
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
|
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
|
||||||
second run changes nothing. (The box TENANT roles — `claude`, `codex`,
|
second run changes nothing. (The box TENANT roles — `claude-box`, `codex-box`,
|
||||||
`grok`, `staging` — share the verb but are their own family; see *the box
|
`grok-box`, `staging-box` — share the verb but are their own family; the
|
||||||
tenants* below.)
|
`-box` suffix says so. See *the box tenants* below.)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rig bootstrap control-plane-server --hostname my-coolify-box --users ./users
|
rig bootstrap control-plane-server --hostname my-coolify-box --users ./users
|
||||||
|
|
@ -167,8 +168,8 @@ its own account; the box CLI's own installer does that (see the `host`
|
||||||
trait), and every other way that step can fail lands in apply's existing
|
trait), and every other way that step can fail lands in apply's existing
|
||||||
refusal at the end.
|
refusal at the end.
|
||||||
|
|
||||||
`--users` does **not** reach the box TENANT roles (`claude`, `codex`, `grok`,
|
`--users` does **not** reach the box TENANT roles (`claude-box`, `codex-box`,
|
||||||
`staging`). A tenant is a box-minted *guest*: box auto-runs its bootstrap at
|
`grok-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
|
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
|
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
|
the **host's** `incus` grants, which the host's own users file already
|
||||||
|
|
@ -373,15 +374,15 @@ VM-host appliance) — and `workstation` is the machine at the keyboard end of
|
||||||
all the SSH connections: human-class, `join=login`, entering the tailnet as
|
all the SSH connections: human-class, `join=login`, entering the tailnet as
|
||||||
*your* device rather than the fleet's.
|
*your* device rather than the fleet's.
|
||||||
|
|
||||||
### `rig bootstrap <claude|codex|grok|staging>` — the box tenants
|
### `rig bootstrap <claude-box|codex-box|grok-box|staging-box>` — the box tenants
|
||||||
|
|
||||||
Run as root, **inside** a [box](https://github.com/heavy-duty/box)-minted
|
Run as root, **inside** a [box](https://github.com/heavy-duty/box)-minted
|
||||||
guest. Convergent — safe to re-run; a second run changes nothing.
|
guest. Convergent — safe to re-run; a second run changes nothing.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rig bootstrap claude # or codex, grok — the agent tenants
|
rig bootstrap claude-box # or codex-box, grok-box — the agent tenants
|
||||||
rig bootstrap staging # the server tenant (docker + sshd hardening)
|
rig bootstrap staging-box # the server tenant (docker + sshd hardening)
|
||||||
rig bootstrap claude --user dev # when the seed's BOX_USER differs
|
rig bootstrap claude-box --user dev # when the seed's BOX_USER differs
|
||||||
```
|
```
|
||||||
|
|
||||||
**The layering** (rig#31 ↔ box#81): a box template stops being where tenant
|
**The layering** (rig#31 ↔ box#81): a box template stops being where tenant
|
||||||
|
|
@ -397,12 +398,18 @@ the guests were the hole.
|
||||||
It is **one mechanism, parameterized per tenant** (`lib/tenant-config.sh`
|
It is **one mechanism, parameterized per tenant** (`lib/tenant-config.sh`
|
||||||
holds the whole per-tenant table), not four hand-maintained scripts:
|
holds the whole per-tenant table), not four hand-maintained scripts:
|
||||||
|
|
||||||
| tenant | user | what lands |
|
| tenant role | user | what lands |
|
||||||
|-----------|----------|------------|
|
|---------------|----------|------------|
|
||||||
| `claude` | `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` |
|
| `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` | `codex` | the toolbelt, docker, node 22, `@openai/codex` on the system PATH, and `~/.codex/AGENTS.md` |
|
| `codex-box` | `codex` | the toolbelt, docker, node 22, `@openai/codex` on the system PATH, and `~/.codex/AGENTS.md` |
|
||||||
| `grok` | `grok` | the toolbelt, docker, the grok CLI on the system PATH, and `~/.grok/AGENTS.md` |
|
| `grok-box` | `grok` | the toolbelt, docker, the grok CLI on the system PATH, and `~/.grok/AGENTS.md` |
|
||||||
| `staging` | `ops` | box#69's server posture: docker + the same sshd hardening the machine roles get (shared `lib/sshd.sh`, `class=server` acceptance) |
|
| `staging-box` | `ops` | box#69's server posture: docker + the same sshd hardening the machine roles get (shared `lib/sshd.sh`, `class=server` acceptance) |
|
||||||
|
|
||||||
|
**The role carries the suffix; the user does not.** A tenant user is the
|
||||||
|
account the box *seed* created (`BOX_USER`) and the agent CLI's own dotdir
|
||||||
|
hangs off it — `claude-box` converges the `claude` user and writes
|
||||||
|
`~/.claude/CLAUDE.md`. The suffix is rig's word for "this is a guest", not a
|
||||||
|
rename of anything inside the box, so nothing in the guest's filesystem moved.
|
||||||
|
|
||||||
Every install is **asserted on effective state**, not exit codes: the CLI must
|
Every install is **asserted on effective state**, not exit codes: the CLI must
|
||||||
*answer* (`--version`, run as the tenant user — a CLI that exists but cannot
|
*answer* (`--version`, run as the tenant user — a CLI that exists but cannot
|
||||||
|
|
@ -412,14 +419,14 @@ hardening. The CLI also lands on the **system** PATH (`/usr/local/bin`):
|
||||||
files, so a PATH export alone is invisible to it.
|
files, so a PATH export alone is invisible to it.
|
||||||
|
|
||||||
**Creds-free and non-interactive, by contract.** box auto-runs these at mint
|
**Creds-free and non-interactive, by contract.** box auto-runs these at mint
|
||||||
(`box exec … rig bootstrap claude`), so nothing here prompts, joins, or admits
|
(`box exec … rig bootstrap claude-box`), so nothing here prompts, joins, or admits
|
||||||
— no tailnet, no keys (the harness pins this by *absence*: no `tailscale`, no
|
— no tailnet, no keys (the harness pins this by *absence*: no `tailscale`, no
|
||||||
prompt, in the shipped script). The one creds-holding step a staging guest
|
prompt, in the shipped script). The one creds-holding step a staging guest
|
||||||
eventually needs — the tailnet workload join — stays **operator-run**, exactly
|
eventually needs — the tailnet workload join — stays **operator-run**, exactly
|
||||||
as box#69 designed it: `box shell` → `sudo rig bootstrap workload-server --hostname
|
as box#69 designed it: `box shell` → `sudo rig bootstrap workload-server --hostname
|
||||||
<name> --users <path>` (or `--no-users` — a guest's door is `box shell`, gated
|
<name> --users <path>` (or `--no-users` — a guest's door is `box shell`, gated
|
||||||
by the host's grants) with a single-use tagged pre-auth key. After that join, re-running
|
by the host's grants) with a single-use tagged pre-auth key. After that join, re-running
|
||||||
`rig bootstrap staging` still converges docker + hardening and leaves the
|
`rig bootstrap staging-box` still converges docker + hardening and leaves the
|
||||||
workload marker alone — the machine role is the truer statement of what the
|
workload marker alone — the machine role is the truer statement of what the
|
||||||
box became.
|
box became.
|
||||||
|
|
||||||
|
|
@ -1033,6 +1040,6 @@ the real converge belongs to the rehearsal. The end-to-end rehearsal is a
|
||||||
throwaway VM/container: pristine Debian → install → `bootstrap workload-server` with
|
throwaway VM/container: pristine Debian → install → `bootstrap workload-server` with
|
||||||
a real single-use key → assert the sshd drop-in, tailnet join, and a no-op
|
a real single-use key → assert the sshd drop-in, tailnet join, and a no-op
|
||||||
second run → destroy, remove the node from the tailnet. The tenant rehearsal
|
second run → destroy, remove the node from the tailnet. The tenant rehearsal
|
||||||
is the same shape, creds-free: container + seed user → `rig bootstrap claude`
|
is the same shape, creds-free: container + seed user → `rig bootstrap claude-box`
|
||||||
/ `staging` → assert the CLI answers, docker answers, `sshd -T`, the context
|
/ `staging-box` → assert the CLI answers, docker answers, `sshd -T`, the context
|
||||||
file — then re-run and watch it no-op.
|
file — then re-run and watch it no-op.
|
||||||
|
|
|
||||||
13
bin/rig
13
bin/rig
|
|
@ -33,13 +33,14 @@ commands:
|
||||||
tenant roles); custom and workstation take none. join=login
|
tenant roles); custom and workstation take none. join=login
|
||||||
(workstation) needs no key: interactive login, node must come up
|
(workstation) needs no key: interactive login, node must come up
|
||||||
untagged. Run as root.
|
untagged. Run as root.
|
||||||
bootstrap <claude|codex|grok|staging> [--user <name>]
|
bootstrap <claude-box|codex-box|grok-box|staging-box> [--user <name>]
|
||||||
The box TENANT roles: converge a box-minted guest. claude|codex|grok
|
The box TENANT roles: converge a box-minted guest. The '-box' suffix
|
||||||
land the agent toolbelt (git, gh, tmux, docker), the agent's CLI on
|
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
|
||||||
the system PATH, and the agent-context file (with the box#80 guard:
|
the system PATH, and the agent-context file (with the box#80 guard:
|
||||||
never run box setup-host inside a box). staging lands box#69's server
|
never run box setup-host inside a box). staging-box lands box#69's
|
||||||
posture — docker + sshd hardening; its tailnet join stays operator-run
|
server posture — docker + sshd hardening; its tailnet join stays
|
||||||
via 'rig bootstrap workload-server'. Creds-free and non-interactive: box
|
operator-run via 'rig bootstrap workload-server'. Creds-free and non-interactive: box
|
||||||
auto-runs these at mint. Run as root, inside the box.
|
auto-runs these at mint. Run as root, inside the box.
|
||||||
coolify install --version <pin>
|
coolify install --version <pin>
|
||||||
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# rig bootstrap <claude|codex|grok|staging> — the box TENANT roles: what a
|
# rig bootstrap <claude-box|codex-box|grok-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
|
# 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
|
# (base image, user, rig preinstalled — heavy-duty/box#81); rig converges the
|
||||||
# tenant content that used to live in the templates' cloud-init, idempotent and
|
# tenant content that used to live in the templates' cloud-init, idempotent and
|
||||||
|
|
@ -8,8 +9,8 @@
|
||||||
# lib/tenant-config.sh — never four hand-maintained copies.
|
# lib/tenant-config.sh — never four hand-maintained copies.
|
||||||
#
|
#
|
||||||
# Creds-free BY CONTRACT: box auto-runs these at mint ('box exec … rig
|
# Creds-free BY CONTRACT: box auto-runs these at mint ('box exec … rig
|
||||||
# bootstrap claude'), so every path here is non-interactive and nothing joins
|
# bootstrap claude-box'), so every path here is non-interactive and nothing joins
|
||||||
# or admits — no tailnet, no keys, no prompts. staging's tailnet join stays
|
# or admits — no tailnet, no keys, no prompts. staging-box's tailnet join stays
|
||||||
# operator-run ('rig bootstrap workload-server' through 'box shell'), exactly the
|
# operator-run ('rig bootstrap workload-server' through 'box shell'), exactly the
|
||||||
# creds split box#69 designed.
|
# creds split box#69 designed.
|
||||||
# Convergent: safe to re-run; a second run changes nothing.
|
# Convergent: safe to re-run; a second run changes nothing.
|
||||||
|
|
@ -21,7 +22,7 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||||
# shellcheck source=SCRIPTDIR/lib/users-config.sh
|
# shellcheck source=SCRIPTDIR/lib/users-config.sh
|
||||||
. "$HERE/lib/users-config.sh" # read_role_marker
|
. "$HERE/lib/users-config.sh" # read_role_marker
|
||||||
# shellcheck source=SCRIPTDIR/lib/sshd.sh
|
# shellcheck source=SCRIPTDIR/lib/sshd.sh
|
||||||
. "$HERE/lib/sshd.sh" # harden_sshd (the staging tenant)
|
. "$HERE/lib/sshd.sh" # harden_sshd (the staging-box tenant)
|
||||||
|
|
||||||
log() { printf 'rig-bootstrap: %s\n' "$*"; }
|
log() { printf 'rig-bootstrap: %s\n' "$*"; }
|
||||||
warn() { printf 'rig-bootstrap: WARNING: %s\n' "$*" >&2; }
|
warn() { printf 'rig-bootstrap: WARNING: %s\n' "$*" >&2; }
|
||||||
|
|
@ -29,27 +30,29 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: rig bootstrap <claude|codex|grok|staging> [--user <name>]
|
usage: rig bootstrap <claude-box|codex-box|grok-box|staging-box> [--user <name>]
|
||||||
|
|
||||||
Box TENANT roles — what a box-minted guest becomes. box mints the thin,
|
Box TENANT roles — what a box-minted guest becomes. box mints the thin,
|
||||||
creds-free seed (base image, user, rig preinstalled); this converges the
|
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.
|
tenant on top, and re-runs converge an existing box to a new spec.
|
||||||
|
|
||||||
claude|codex|grok the agent tenants: base tooling (git, gh, tmux, …),
|
claude-box|codex-box|grok-box
|
||||||
|
the agent tenants: base tooling (git, gh, tmux, …),
|
||||||
docker, the agent's CLI on the system PATH, and the
|
docker, the agent's CLI on the system PATH, and the
|
||||||
agent-context file — including the box#80 guard: never
|
agent-context file — including the box#80 guard: never
|
||||||
run `box setup-host` or the drill inside a box.
|
run `box setup-host` or the drill inside a box.
|
||||||
staging the server tenant (box#69's posture): docker + sshd
|
staging-box the server tenant (box#69's posture): docker + sshd
|
||||||
hardening. The tailnet workload join is deliberately
|
hardening. The tailnet workload join is deliberately
|
||||||
NOT here — it holds a credential, so it stays
|
NOT here — it holds a credential, so it stays
|
||||||
operator-run: `box shell` → `sudo rig bootstrap
|
operator-run: `box shell` → `sudo rig bootstrap
|
||||||
workload-server` with a tagged pre-auth key.
|
workload-server` with a tagged pre-auth key.
|
||||||
|
|
||||||
--user <name> the tenant user the box seed created (default: the
|
--user <name> the tenant user the box seed created (default: the
|
||||||
role's name; staging defaults to `ops`)
|
role's name minus the suffix; staging-box defaults to
|
||||||
|
`ops`)
|
||||||
|
|
||||||
Tenant roles are creds-free and non-interactive by contract — box auto-runs
|
Tenant roles are creds-free and non-interactive by contract — box auto-runs
|
||||||
them at mint (`box exec … rig bootstrap claude`). They take none of the
|
them at mint (`box exec … rig bootstrap claude-box`). They take none of the
|
||||||
machine-role traits (--hostname/--class/--host/--join): a tenant is a guest,
|
machine-role traits (--hostname/--class/--host/--join): a tenant is a guest,
|
||||||
not a tailnet machine. Run as root, inside the box.
|
not a tailnet machine. Run as root, inside the box.
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -58,10 +61,10 @@ EOF
|
||||||
# --- args (validated before the root check, so errors are testable) ---------
|
# --- args (validated before the root check, so errors are testable) ---------
|
||||||
ROLE="${1:-}"
|
ROLE="${1:-}"
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
claude|codex|grok|staging) shift ;;
|
claude-box|codex-box|grok-box|staging-box) shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
"") usage >&2; die "tenant role required (claude|codex|grok|staging)" 2 ;;
|
"") usage >&2; die "tenant role required (claude-box|codex-box|grok-box|staging-box)" 2 ;;
|
||||||
*) die "unknown tenant role: $ROLE (want claude|codex|grok|staging)" 2 ;;
|
*) die "unknown tenant role: $ROLE (want claude-box|codex-box|grok-box|staging-box)" 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
TENANT_USER="$(tenant_user "$ROLE")"
|
TENANT_USER="$(tenant_user "$ROLE")"
|
||||||
|
|
@ -74,13 +77,13 @@ while [ $# -gt 0 ]; do
|
||||||
--hostname|--class|--host|--join)
|
--hostname|--class|--host|--join)
|
||||||
# The machine-role traits, refused with a story rather than "unknown
|
# The machine-role traits, refused with a story rather than "unknown
|
||||||
# flag": a tenant is a guest, not a tailnet machine — its shape comes
|
# flag": a tenant is a guest, not a tailnet machine — its shape comes
|
||||||
# from the box seed, and the one trait-shaped thing a staging guest
|
# from the box seed, and the one trait-shaped thing a staging-box guest
|
||||||
# eventually does (join the tailnet as a workload) is deliberately not
|
# eventually does (join the tailnet as a workload) is deliberately not
|
||||||
# here: it holds a credential, so it stays operator-run.
|
# here: it holds a credential, so it stays operator-run.
|
||||||
die "tenant roles have no traits: $1 belongs to the machine roles (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom). A tenant box's shape comes from its seed; staging's tailnet join is operator-run via 'rig bootstrap workload-server'. The METAL that hosts these guests is 'rig bootstrap staging-server'." 2 ;;
|
die "tenant roles have no traits: $1 belongs to the machine roles (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom). A tenant box's shape comes from its seed; staging-box's tailnet join is operator-run via 'rig bootstrap workload-server'. The METAL that hosts these guests is 'rig bootstrap staging-server'." 2 ;;
|
||||||
--ts-tag)
|
--ts-tag)
|
||||||
[ $# -ge 2 ] && shift
|
[ $# -ge 2 ] && shift
|
||||||
die "--ts-tag is gone and tenant roles never join the tailnet anyway. staging's join is operator-run via 'rig bootstrap workload-server', where the tag comes from the pre-auth key." 2 ;;
|
die "--ts-tag is gone and tenant roles never join the tailnet anyway. staging-box's join is operator-run via 'rig bootstrap workload-server', where the tag comes from the pre-auth key." 2 ;;
|
||||||
*) die "unknown flag: $1" 2 ;;
|
*) die "unknown flag: $1" 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
@ -97,28 +100,28 @@ done
|
||||||
# markers (repo precedent: the coolify marker warning). Two refusals, one
|
# markers (repo precedent: the coolify marker warning). Two refusals, one
|
||||||
# tolerance:
|
# tolerance:
|
||||||
# - host=yes → refuse, every tenant: a VM HOST is the opposite of a guest.
|
# - host=yes → refuse, every tenant: a VM HOST is the opposite of a guest.
|
||||||
# Names the staging rename out loud — before #31, `staging` was the VM-host
|
# Names the staging PAIR out loud, because whoever lands here has the two
|
||||||
# PRESET; that shape is now spelled through the traits.
|
# halves confused: the metal is `staging-server`, the guest `staging-box`.
|
||||||
# - class= (agent tenants) → refuse: an agent box is never a tailnet machine.
|
# - class= (agent tenants) → refuse: an agent box is never a tailnet machine.
|
||||||
# - class=server with host=no (staging only) → PROCEED, and leave the marker
|
# - class=server with host=no (staging-box only) → PROCEED, and leave the
|
||||||
# alone: that is the staging guest AFTER its operator-run workload join, and
|
# marker alone: that is the guest AFTER its operator-run workload join, and
|
||||||
# re-converging docker+hardening on it is exactly what convergence is for.
|
# re-converging docker+hardening on it is exactly what convergence is for.
|
||||||
# ONLY that shape — any other class (say class=human, via `custom`) is a
|
# ONLY that shape — any other class (say class=human, via `custom`) is a
|
||||||
# machine rig built on purpose, and staging hardening it with server rules
|
# machine rig built on purpose, and staging-box hardening it with server rules
|
||||||
# would die with server-specific messaging on a box that was never one.
|
# would die with server-specific messaging on a box that was never one.
|
||||||
MARKER_PATH="${RIG_ROLE_MARKER:-/etc/rig/role}"
|
MARKER_PATH="${RIG_ROLE_MARKER:-/etc/rig/role}"
|
||||||
EXISTING_MARKER="$(read_role_marker "$MARKER_PATH")"
|
EXISTING_MARKER="$(read_role_marker "$MARKER_PATH")"
|
||||||
case "$EXISTING_MARKER" in
|
case "$EXISTING_MARKER" in
|
||||||
*host=yes*)
|
*host=yes*)
|
||||||
die "this box hosts VMs (${EXISTING_MARKER}) — a tenant role converges box GUESTS, never the host under them. Note: before rig#31, 'staging' was the VM-host preset; that shape is now 'rig bootstrap custom --class server --host yes --join authkey' (or 'dev --class server')." ;;
|
die "this box hosts VMs (${EXISTING_MARKER}) — a tenant role converges box GUESTS, never the host under them. You want the other half of the pair: the metal is 'rig bootstrap staging-server', and the guests it mints are 'staging-box'." ;;
|
||||||
*class=*)
|
*class=*)
|
||||||
if [ "$ROLE" != "staging" ]; then
|
if [ "$ROLE" != "staging-box" ]; then
|
||||||
die "this box already carries a machine role (${EXISTING_MARKER}) — the agent tenants converge box guests, never tailnet machines. If this really is a guest, remove ${MARKER_PATH} and re-run."
|
die "this box already carries a machine role (${EXISTING_MARKER}) — the agent tenants converge box guests, never tailnet machines. If this really is a guest, remove ${MARKER_PATH} and re-run."
|
||||||
fi
|
fi
|
||||||
case "$EXISTING_MARKER" in
|
case "$EXISTING_MARKER" in
|
||||||
*class=server*) ;;
|
*class=server*) ;;
|
||||||
*)
|
*)
|
||||||
die "this box carries a non-server machine role (${EXISTING_MARKER}) — staging tolerates only the workload-joined guest (class=server host=no). If this really is a staging guest, remove ${MARKER_PATH} and re-run." ;;
|
die "this box carries a non-server machine role (${EXISTING_MARKER}) — staging-box tolerates only the workload-joined guest (class=server host=no). If this really is a staging-box guest, remove ${MARKER_PATH} and re-run." ;;
|
||||||
esac ;;
|
esac ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|
@ -161,13 +164,13 @@ export DEBIAN_FRONTEND=noninteractive
|
||||||
log "installing base packages (tenant ${ROLE})"
|
log "installing base packages (tenant ${ROLE})"
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
claude)
|
claude-box)
|
||||||
# The claude tenant keeps zsh (its shell UX ships with the box); the
|
# The claude-box tenant keeps zsh (its shell UX ships with the box); the
|
||||||
# remaining list is the shared agent toolbelt the templates carried.
|
# 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 ;;
|
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential zsh ;;
|
||||||
codex|grok)
|
codex-box|grok-box)
|
||||||
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential ;;
|
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential ;;
|
||||||
staging)
|
staging-box)
|
||||||
# openssh-server: the hardening drop-in below targets /etc/ssh/sshd_config.d/,
|
# openssh-server: the hardening drop-in below targets /etc/ssh/sshd_config.d/,
|
||||||
# which only exists once the package is installed — pristine container/VM
|
# which only exists once the package is installed — pristine container/VM
|
||||||
# images (and thin seeds) do not ship it.
|
# images (and thin seeds) do not ship it.
|
||||||
|
|
@ -177,13 +180,13 @@ esac
|
||||||
# contract ('box tmux' runs tmux new-session inside every box) and gh is how
|
# contract ('box tmux' runs tmux new-session inside every box) and gh is how
|
||||||
# the operator's git credential lands.
|
# the operator's git credential lands.
|
||||||
command -v tmux >/dev/null 2>&1 || die "tmux missing after package install — 'box tmux' (box#65) needs it"
|
command -v tmux >/dev/null 2>&1 || die "tmux missing after package install — 'box tmux' (box#65) needs it"
|
||||||
if [ "$ROLE" != "staging" ]; then
|
if [ "$ROLE" != "staging-box" ]; then
|
||||||
command -v gh >/dev/null 2>&1 || die "gh missing after package install"
|
command -v gh >/dev/null 2>&1 || die "gh missing after package install"
|
||||||
command -v git >/dev/null 2>&1 || die "git missing after package install"
|
command -v git >/dev/null 2>&1 || die "git missing after package install"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- docker ------------------------------------------------------------------
|
# --- docker ------------------------------------------------------------------
|
||||||
# Every tenant gets docker (the templates all carried it; staging's guests run
|
# Every tenant gets docker (the templates all carried it; staging-box's workloads run
|
||||||
# their workloads in it). Docker's own installer, convergence-guarded — its
|
# their workloads in it). Docker's own installer, convergence-guarded — its
|
||||||
# script is not a no-op when docker exists, so rig supplies the guard.
|
# script is not a no-op when docker exists, so rig supplies the guard.
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
|
@ -214,9 +217,9 @@ else
|
||||||
warn "no docker group after install — skipping the ${TENANT_USER} group add; check docker's install"
|
warn "no docker group after install — skipping the ${TENANT_USER} group add; check docker's install"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- node (claude, codex) ----------------------------------------------------
|
# --- node (claude-box, codex-box) ----------------------------------------------------
|
||||||
# Codex is an npm global needing Node 22+ (the SCOPED @openai/codex — verified
|
# Codex is an npm global needing Node 22+ (the SCOPED @openai/codex — verified
|
||||||
# upstream when the template was written); the claude tenant ships node as part
|
# upstream when the template was written); the claude-box tenant ships node as part
|
||||||
# of its toolbelt, same pin. grok's CLI is a self-contained binary: no node.
|
# of its toolbelt, same pin. grok's CLI is a self-contained binary: no node.
|
||||||
node_ok() {
|
node_ok() {
|
||||||
command -v node >/dev/null 2>&1 || return 1
|
command -v node >/dev/null 2>&1 || return 1
|
||||||
|
|
@ -224,7 +227,7 @@ node_ok() {
|
||||||
major="$(node --version 2>/dev/null | sed -E 's/^v([0-9]+)\..*$/\1/')"
|
major="$(node --version 2>/dev/null | sed -E 's/^v([0-9]+)\..*$/\1/')"
|
||||||
[ "${major:-0}" -ge 22 ] 2>/dev/null
|
[ "${major:-0}" -ge 22 ] 2>/dev/null
|
||||||
}
|
}
|
||||||
if [ "$ROLE" = "claude" ] || [ "$ROLE" = "codex" ]; then
|
if [ "$ROLE" = "claude-box" ] || [ "$ROLE" = "codex-box" ]; then
|
||||||
if node_ok; then
|
if node_ok; then
|
||||||
log "node $(node --version) already present"
|
log "node $(node --version) already present"
|
||||||
else
|
else
|
||||||
|
|
@ -241,10 +244,10 @@ fi
|
||||||
# 'box exec <box> -- <cli> …' runs a NON-interactive shell that reads no rc
|
# 'box exec <box> -- <cli> …' runs a NON-interactive shell that reads no rc
|
||||||
# files, so a PATH export alone is invisible to it (the #15 lesson) — and
|
# files, so a PATH export alone is invisible to it (the #15 lesson) — and
|
||||||
# assert it ANSWERS as the tenant user: a CLI that exists but cannot run is
|
# assert it ANSWERS as the tenant user: a CLI that exists but cannot run is
|
||||||
# what cost the last drill (the grok template's scar).
|
# what cost the last drill (the grok-box template's scar).
|
||||||
CLI="" CLI_SRC=""
|
CLI="" CLI_SRC=""
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
claude)
|
claude-box)
|
||||||
CLI=claude CLI_SRC="$TENANT_HOME/.local/bin/claude"
|
CLI=claude CLI_SRC="$TENANT_HOME/.local/bin/claude"
|
||||||
if [ ! -e "$CLI_SRC" ]; then
|
if [ ! -e "$CLI_SRC" ]; then
|
||||||
log "installing the Claude Code CLI as ${TENANT_USER}"
|
log "installing the Claude Code CLI as ${TENANT_USER}"
|
||||||
|
|
@ -252,7 +255,7 @@ case "$ROLE" in
|
||||||
else
|
else
|
||||||
log "claude CLI already installed"
|
log "claude CLI already installed"
|
||||||
fi ;;
|
fi ;;
|
||||||
codex)
|
codex-box)
|
||||||
CLI=codex
|
CLI=codex
|
||||||
if ! command -v codex >/dev/null 2>&1; then
|
if ! command -v codex >/dev/null 2>&1; then
|
||||||
log "installing the Codex CLI (npm global)"
|
log "installing the Codex CLI (npm global)"
|
||||||
|
|
@ -261,7 +264,7 @@ case "$ROLE" in
|
||||||
log "codex CLI already installed"
|
log "codex CLI already installed"
|
||||||
fi
|
fi
|
||||||
CLI_SRC="$(npm prefix -g)/bin/codex" ;;
|
CLI_SRC="$(npm prefix -g)/bin/codex" ;;
|
||||||
grok)
|
grok-box)
|
||||||
# The OFFICIAL installer (x.ai/cli/install.sh): installs the CLI as `grok`,
|
# The OFFICIAL installer (x.ai/cli/install.sh): installs the CLI as `grok`,
|
||||||
# a SYMLINK under $HOME/.grok/bin pointing into its versioned download dir.
|
# a SYMLINK under $HOME/.grok/bin pointing into its versioned download dir.
|
||||||
# Run it AS the tenant user, never root: a symlink into root's 0700 home
|
# Run it AS the tenant user, never root: a symlink into root's 0700 home
|
||||||
|
|
@ -273,7 +276,7 @@ case "$ROLE" in
|
||||||
else
|
else
|
||||||
log "grok CLI already installed"
|
log "grok CLI already installed"
|
||||||
fi ;;
|
fi ;;
|
||||||
staging) ;; # no agent lives on the staging tenant
|
staging-box) ;; # no agent lives on the staging-box tenant
|
||||||
esac
|
esac
|
||||||
if [ -n "$CLI" ]; then
|
if [ -n "$CLI" ]; then
|
||||||
[ -e "$CLI_SRC" ] || die "the ${CLI} installer produced no ${CLI_SRC} — upstream layout changed?"
|
[ -e "$CLI_SRC" ] || die "the ${CLI} installer produced no ${CLI_SRC} — upstream layout changed?"
|
||||||
|
|
@ -289,11 +292,11 @@ if [ -n "$CLI" ]; then
|
||||||
# point: the line must expand in the USER's shell, not here.
|
# point: the line must expand in the USER's shell, not here.
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
claude)
|
claude-box)
|
||||||
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.local/bin:$PATH"' ;;
|
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.local/bin:$PATH"' ;;
|
||||||
codex)
|
codex-box)
|
||||||
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$(npm prefix -g)/bin:$PATH"' ;;
|
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$(npm prefix -g)/bin:$PATH"' ;;
|
||||||
grok)
|
grok-box)
|
||||||
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.grok/bin:$PATH"' ;;
|
append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.grok/bin:$PATH"' ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
@ -324,11 +327,11 @@ if CTX_PATH="$(tenant_context_path "$ROLE" "$TENANT_HOME")"; then
|
||||||
rm -f "$CTX_TMP"
|
rm -f "$CTX_TMP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- claude shell niceties ---------------------------------------------------
|
# --- claude-box shell niceties ---------------------------------------------------
|
||||||
# The claude template shipped zsh + oh-my-zsh + tmux mouse mode; they move with
|
# The claude-box template shipped zsh + oh-my-zsh + tmux mouse mode; they move with
|
||||||
# the tenant. oh-my-zsh is a cosmetic EXTRA: its failure warns, never aborts a
|
# the tenant. oh-my-zsh is a cosmetic EXTRA: its failure warns, never aborts a
|
||||||
# bootstrap whose real work (CLI, context, docker) already converged.
|
# bootstrap whose real work (CLI, context, docker) already converged.
|
||||||
if [ "$ROLE" = "claude" ]; then
|
if [ "$ROLE" = "claude-box" ]; then
|
||||||
if [ "$(getent passwd "$TENANT_USER" | cut -d: -f7)" != "/usr/bin/zsh" ]; then
|
if [ "$(getent passwd "$TENANT_USER" | cut -d: -f7)" != "/usr/bin/zsh" ]; then
|
||||||
chsh -s /usr/bin/zsh "$TENANT_USER"
|
chsh -s /usr/bin/zsh "$TENANT_USER"
|
||||||
log "login shell set to zsh for ${TENANT_USER}"
|
log "login shell set to zsh for ${TENANT_USER}"
|
||||||
|
|
@ -350,20 +353,20 @@ if [ "$ROLE" = "claude" ]; then
|
||||||
append_line_once "$TENANT_HOME/.tmux.conf" 'set -g mouse on'
|
append_line_once "$TENANT_HOME/.tmux.conf" 'set -g mouse on'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- staging server posture --------------------------------------------------
|
# --- staging-box server posture --------------------------------------------------
|
||||||
# box#69's posture, minus the join: docker (above) + sshd hardening, through
|
# box#69's posture, minus the join: docker (above) + sshd hardening, through
|
||||||
# the SAME code the machine roles use (lib/sshd.sh) — the staging guest is a
|
# the SAME code the machine roles use (lib/sshd.sh) — the staging-box guest is a
|
||||||
# workload server in waiting, and its door must never be password-open even
|
# workload server in waiting, and its door must never be password-open even
|
||||||
# before the operator joins it. class=server: root SSH stays the control
|
# before the operator joins it. class=server: root SSH stays the control
|
||||||
# plane's future automation door.
|
# plane's future automation door.
|
||||||
if [ "$ROLE" = "staging" ]; then
|
if [ "$ROLE" = "staging-box" ]; then
|
||||||
harden_sshd server
|
harden_sshd server
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- role marker --------------------------------------------------------------
|
# --- role marker --------------------------------------------------------------
|
||||||
# Same ground truth the machine roles write, tenant-shaped: no class= (a tenant
|
# Same ground truth the machine roles write, tenant-shaped: no class= (a tenant
|
||||||
# has no root-door policy of its own — close-root fails closed on it), and
|
# has no root-door policy of its own — close-root fails closed on it), and
|
||||||
# host=no so `rig users apply` box-role gating keeps working. staging SKIPS the
|
# host=no so `rig users apply` box-role gating keeps working. staging-box SKIPS the
|
||||||
# write when a machine marker is already present: after the operator-run
|
# write when a machine marker is already present: after the operator-run
|
||||||
# workload join, the workload marker is the truer statement and rig never
|
# workload join, the workload marker is the truer statement and rig never
|
||||||
# clobbers state a joined box earned.
|
# clobbers state a joined box earned.
|
||||||
|
|
@ -383,7 +386,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "done — tenant ${ROLE}, user ${TENANT_USER}"
|
log "done — tenant ${ROLE}, user ${TENANT_USER}"
|
||||||
if [ "$ROLE" = "staging" ]; then
|
if [ "$ROLE" = "staging-box" ]; then
|
||||||
log "next (operator-run, holds a credential): box shell → sudo rig bootstrap workload-server --hostname <name> with a tagged pre-auth key"
|
log "next (operator-run, holds a credential): box shell → sudo rig bootstrap workload-server --hostname <name> with a tagged pre-auth key"
|
||||||
else
|
else
|
||||||
log "next: creds stay with the operator — ${CLI} authenticates through its own interactive login when a human decides"
|
log "next: creds stay with the operator — ${CLI} authenticates through its own interactive login when a human decides"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ usage: rig bootstrap <control-plane-server|workload-server|runner-server|
|
||||||
(--users <path> | --no-users)
|
(--users <path> | --no-users)
|
||||||
[--hostname <name>] [--class <human|server>]
|
[--hostname <name>] [--class <human|server>]
|
||||||
[--host <yes|no>] [--join <authkey|login>]
|
[--host <yes|no>] [--join <authkey|login>]
|
||||||
rig bootstrap <claude|codex|grok|staging> [--user <name>]
|
rig bootstrap <claude-box|codex-box|grok-box|staging-box> [--user <name>]
|
||||||
(the box TENANT roles — see their own --help; they take
|
(the box TENANT roles — see their own --help; they take
|
||||||
no --users, see below)
|
no --users, see below)
|
||||||
|
|
||||||
|
|
@ -51,7 +51,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
|
in. So the complete path is the default path and skipping it is a deliberate
|
||||||
--no-users, not an omission.
|
--no-users, not an omission.
|
||||||
|
|
||||||
--users does NOT reach the box TENANT roles (claude|codex|grok|staging). A
|
--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 mint,
|
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
|
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
|
and has no SSH door of its own — entry is `box shell`, gated by the HOST's
|
||||||
|
|
@ -109,15 +109,15 @@ EOF
|
||||||
ROLE="${1:-}"
|
ROLE="${1:-}"
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom) shift ;;
|
control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom) shift ;;
|
||||||
claude|codex|grok|staging)
|
claude-box|codex-box|grok-box|staging-box)
|
||||||
# The box TENANT roles (#31) are a different family — guests a box mints,
|
# The box TENANT roles (#31) are a different family — guests a box mints,
|
||||||
# never tailnet machines — and live in their own mechanism, one script
|
# never tailnet machines — and live in their own mechanism, one script
|
||||||
# parameterized per tenant. Dispatched here so `rig bootstrap <role>`
|
# parameterized per tenant. Dispatched here so `rig bootstrap <role>`
|
||||||
# stays the single entrypoint for both families.
|
# stays the single entrypoint for both families.
|
||||||
exec "$HERE/bootstrap-tenant.sh" "$@" ;;
|
exec "$HERE/bootstrap-tenant.sh" "$@" ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-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|codex|grok|staging)" 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|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|codex|grok|staging)" 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 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Role→traits map — the single place a role's shape is declared (issue #26).
|
# Role→traits map — the single place a role's shape is declared (issue #26).
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,34 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Shared parameters for the box TENANT roles (claude, codex, grok, staging) —
|
# 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
|
||||||
|
# '-server' machine roles rig converges); see #76.
|
||||||
# sourced by bootstrap-tenant.sh and by the test harness. Pure text→text, no
|
# 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
|
# side effects: the per-tenant differences live HERE, in one table, so the
|
||||||
# mechanism stays one script parameterized per tenant instead of four
|
# mechanism stays one script parameterized per tenant instead of four
|
||||||
# hand-maintained copies (repo precedent: parse_users_file, runner-config).
|
# hand-maintained copies (repo precedent: parse_users_file, runner-config).
|
||||||
|
|
||||||
# tenant_user <role> — the user the box seed creates (box.env BOX_USER). The
|
# tenant_user <role> — the user the box seed creates (box.env BOX_USER). The
|
||||||
# agent tenants are named after their agent; staging keeps box#69's `ops`.
|
# agent tenants are named after their agent (minus the suffix — the USER is not
|
||||||
|
# the role); staging-box keeps box#69's `ops`.
|
||||||
tenant_user() {
|
tenant_user() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
claude) printf 'claude' ;;
|
claude-box) printf 'claude' ;;
|
||||||
codex) printf 'codex' ;;
|
codex-box) printf 'codex' ;;
|
||||||
grok) printf 'grok' ;;
|
grok-box) printf 'grok' ;;
|
||||||
staging) printf 'ops' ;;
|
staging-box) printf 'ops' ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# tenant_context_path <role> <home> — where the agent-context file lands. Each
|
# tenant_context_path <role> <home> — where the agent-context file lands. Each
|
||||||
# agent CLI reads its own instructions file from its own dotdir; staging has no
|
# agent CLI reads its own instructions file from its own dotdir (named for the
|
||||||
# agent and no context file (return 1).
|
# agent, not the role — the dotdir is the CLI's, and the suffix is rig's);
|
||||||
|
# staging-box has no agent and no context file (return 1).
|
||||||
tenant_context_path() {
|
tenant_context_path() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
claude) printf '%s/.claude/CLAUDE.md' "$2" ;;
|
claude-box) printf '%s/.claude/CLAUDE.md' "$2" ;;
|
||||||
codex) printf '%s/.codex/AGENTS.md' "$2" ;;
|
codex-box) printf '%s/.codex/AGENTS.md' "$2" ;;
|
||||||
grok) printf '%s/.grok/AGENTS.md' "$2" ;;
|
grok-box) printf '%s/.grok/AGENTS.md' "$2" ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -32,27 +36,27 @@ tenant_context_path() {
|
||||||
# render_tenant_context <role> — the agent-context file's content, on stdout.
|
# 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 three agents: only the creds paragraph is per-vendor,
|
||||||
# and the box#80 guard note lives HERE once — never copy-pasted per template.
|
# and the box#80 guard note lives HERE once — never copy-pasted per template.
|
||||||
# staging renders nothing (return 1): no agent lives there.
|
# staging-box renders nothing (return 1): no agent lives there.
|
||||||
render_tenant_context() {
|
render_tenant_context() {
|
||||||
local role="$1" creds
|
local role="$1" creds
|
||||||
# The single-quoted markdown below carries literal `$`-free backtick prose;
|
# The single-quoted markdown below carries literal `$`-free backtick prose;
|
||||||
# single quotes are deliberate — nothing in it may expand here.
|
# single quotes are deliberate — nothing in it may expand here.
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
case "$role" in
|
case "$role" in
|
||||||
claude)
|
claude-box)
|
||||||
creds='- **Creds-free by default.** The box starts with no Claude and no git
|
creds='- **Creds-free by default.** The box starts with no Claude and no git
|
||||||
credentials. If you need to authenticate Claude, the operator runs `/login`
|
credentials. If you need to authenticate Claude, the operator runs `/login`
|
||||||
interactively. For git, the operator adds their own credentials (a PAT or
|
interactively. For git, the operator adds their own credentials (a PAT or
|
||||||
`gh auth login`). Never assume credentials are present; never ask for or
|
`gh auth login`). Never assume credentials are present; never ask for or
|
||||||
store secrets on disk beyond what the operator sets up.' ;;
|
store secrets on disk beyond what the operator sets up.' ;;
|
||||||
codex)
|
codex-box)
|
||||||
creds='- **Creds-free by default.** The box starts with no OpenAI and no git
|
creds='- **Creds-free by default.** The box starts with no OpenAI and no git
|
||||||
credentials. If you need to authenticate Codex, the operator runs the
|
credentials. If you need to authenticate Codex, the operator runs the
|
||||||
login flow (`codex`) interactively. For git, the operator adds their own
|
login flow (`codex`) interactively. For git, the operator adds their own
|
||||||
credentials (a PAT or `gh auth login`). Never assume credentials are
|
credentials (a PAT or `gh auth login`). Never assume credentials are
|
||||||
present; never ask for or store secrets on disk beyond what the operator
|
present; never ask for or store secrets on disk beyond what the operator
|
||||||
sets up.' ;;
|
sets up.' ;;
|
||||||
grok)
|
grok-box)
|
||||||
creds='- **Creds-free by default.** The box starts with no xAI and no git
|
creds='- **Creds-free by default.** The box starts with no xAI and no git
|
||||||
credentials. If you need to authenticate, the operator runs
|
credentials. If you need to authenticate, the operator runs
|
||||||
`grok login` interactively (SuperGrok / X Premium+). For git, the
|
`grok login` interactively (SuperGrok / X Premium+). For git, the
|
||||||
|
|
|
||||||
125
test/cli.sh
125
test/cli.sh
|
|
@ -47,12 +47,12 @@ check "bootstrap: --ts-tag is removed (with value), exit 2" 2 "comes from the pr
|
||||||
"$ROOT/commands/bootstrap.sh" runner-server --ts-tag tag:server
|
"$ROOT/commands/bootstrap.sh" runner-server --ts-tag tag:server
|
||||||
check "bootstrap: --ts-tag is removed (no value), exit 2" 2 "comes from the pre-auth key" \
|
check "bootstrap: --ts-tag is removed (no value), exit 2" 2 "comes from the pre-auth key" \
|
||||||
"$ROOT/commands/bootstrap.sh" runner-server --ts-tag
|
"$ROOT/commands/bootstrap.sh" runner-server --ts-tag
|
||||||
# staging is a box TENANT role since #31 (the guest, not the VM host), and it
|
# staging-box is a box TENANT role (the guest, not the VM host), and it
|
||||||
# never joins the tailnet — but --ts-tag on it must still die with a story,
|
# never joins the tailnet — but --ts-tag on it must still die with a story,
|
||||||
# not an "unknown flag": scripts from its trait-preset life may pass it, and
|
# not an "unknown flag": scripts from its trait-preset life may pass it, and
|
||||||
# the message must say where both the tag AND the join went.
|
# the message must say where both the tag AND the join went.
|
||||||
check "bootstrap: staging + removed --ts-tag exits 2" 2 "never join the tailnet" \
|
check "bootstrap: staging-box + removed --ts-tag exits 2" 2 "never join the tailnet" \
|
||||||
"$ROOT/commands/bootstrap.sh" staging --ts-tag tag:server
|
"$ROOT/commands/bootstrap.sh" staging-box --ts-tag tag:server
|
||||||
# The VM-HOST shape has a named role again (staging-server, #76), but it is
|
# The VM-HOST shape has a named role again (staging-server, #76), but it is
|
||||||
# still not one of the two the control plane manages, so the catch-all
|
# still not one of the two the control plane manages, so the catch-all
|
||||||
# tag:server refusal must own it. Grep-pinned so a deleted guard cannot ship
|
# tag:server refusal must own it. Grep-pinned so a deleted guard cannot ship
|
||||||
|
|
@ -80,8 +80,8 @@ fi
|
||||||
# UNKNOWN rather than quietly resolving to anything. Asserted per name because
|
# UNKNOWN rather than quietly resolving to anything. Asserted per name because
|
||||||
# an alias accidentally left in for one role is exactly the shape that survives
|
# an alias accidentally left in for one role is exactly the shape that survives
|
||||||
# review — the taxonomy reads as complete while one old name still works.
|
# review — the taxonomy reads as complete while one old name still works.
|
||||||
# 'staging' is deliberately absent: it still routes to the TENANT mechanism at
|
# 'staging' is deliberately absent HERE: it is a TENANT name, and its own hard
|
||||||
# this point in the stack, and the tenant rename lands in its own change.
|
# cut is asserted in the tenant section below, at both entrypoints.
|
||||||
for r in control-plane workload runner dev; do
|
for r in control-plane workload runner dev; do
|
||||||
check "bootstrap: the pre-#76 name '$r' is gone (hard cut)" 2 "unknown role" \
|
check "bootstrap: the pre-#76 name '$r' is gone (hard cut)" 2 "unknown role" \
|
||||||
"$ROOT/commands/bootstrap.sh" "$r"
|
"$ROOT/commands/bootstrap.sh" "$r"
|
||||||
|
|
@ -416,7 +416,7 @@ check "rig usage documents the bootstrap users flags" 0 "(--users <path> | --no-
|
||||||
# decision (a box-minted guest has no SSH door of its own; entry is `box shell`,
|
# decision (a box-minted guest has no SSH door of its own; entry is `box shell`,
|
||||||
# gated by the HOST's incus grants) is documented in usage and the README.
|
# gated by the HOST's incus grants) is documented in usage and the README.
|
||||||
check "bootstrap: --users does not reach the tenant roles" 2 "unknown flag" \
|
check "bootstrap: --users does not reach the tenant roles" 2 "unknown flag" \
|
||||||
"$ROOT/commands/bootstrap.sh" claude --users "$BOOT_USERS/ok"
|
"$ROOT/commands/bootstrap.sh" claude-box --users "$BOOT_USERS/ok"
|
||||||
check "bootstrap: usage explains why tenants take no --users" 0 "box-minted GUEST" \
|
check "bootstrap: usage explains why tenants take no --users" 0 "box-minted GUEST" \
|
||||||
"$ROOT/commands/bootstrap.sh" --help
|
"$ROOT/commands/bootstrap.sh" --help
|
||||||
# --- README: the box rename (#12) --------------------------------------------
|
# --- README: the box rename (#12) --------------------------------------------
|
||||||
|
|
@ -451,13 +451,13 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||||
check "bootstrap: --users file reaches the root check" 1 "must run as root" \
|
check "bootstrap: --users file reaches the root check" 1 "must run as root" \
|
||||||
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/ok"
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/ok"
|
||||||
check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner-server --no-users
|
check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner-server --no-users
|
||||||
# staging dispatches to the tenant mechanism now; reaching ITS root check
|
# staging-box dispatches to the tenant mechanism; reaching ITS root check
|
||||||
# through bootstrap.sh proves the dispatch and the tenant arg pass in one go.
|
# through bootstrap.sh proves the dispatch and the tenant arg pass in one go.
|
||||||
# RIG_ROLE_MARKER points at an absent fixture: the tenant marker guard runs
|
# RIG_ROLE_MARKER points at an absent fixture: the tenant marker guard runs
|
||||||
# before the root check, and the machine running this harness may well have
|
# before the root check, and the machine running this harness may well have
|
||||||
# a real /etc/rig/role of its own.
|
# a real /etc/rig/role of its own.
|
||||||
check "bootstrap: staging dispatches to the tenant mechanism, refuses non-root" 1 "must run as root" \
|
check "bootstrap: staging-box dispatches to the tenant mechanism, refuses non-root" 1 "must run as root" \
|
||||||
env RIG_ROLE_MARKER=/nonexistent/rig-role "$ROOT/commands/bootstrap.sh" staging
|
env RIG_ROLE_MARKER=/nonexistent/rig-role "$ROOT/commands/bootstrap.sh" staging-box
|
||||||
check "bootstrap: dev role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" dev-server --no-users
|
check "bootstrap: dev role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" dev-server --no-users
|
||||||
check "bootstrap: workstation parses, refuses non-root" 1 "must run as root" env -u TS_AUTHKEY "$ROOT/commands/bootstrap.sh" workstation --no-users
|
check "bootstrap: workstation parses, refuses non-root" 1 "must run as root" env -u TS_AUTHKEY "$ROOT/commands/bootstrap.sh" workstation --no-users
|
||||||
check "bootstrap: custom parses, refuses non-root" 1 "must run as root" \
|
check "bootstrap: custom parses, refuses non-root" 1 "must run as root" \
|
||||||
|
|
@ -466,7 +466,7 @@ else
|
||||||
echo "skip: bootstrap non-root refusals (running as root)"
|
echo "skip: bootstrap non-root refusals (running as root)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- box tenant roles (#31): claude|codex|grok|staging ------------------------
|
# --- box tenant roles (#31/#76): claude-box|codex-box|grok-box|staging-box ---
|
||||||
# What a box-minted guest becomes — ONE mechanism (bootstrap-tenant.sh),
|
# What a box-minted guest becomes — ONE mechanism (bootstrap-tenant.sh),
|
||||||
# parameterized per tenant through lib/tenant-config.sh, dispatched from
|
# parameterized per tenant through lib/tenant-config.sh, dispatched from
|
||||||
# bootstrap.sh so `rig bootstrap <role>` stays the single entrypoint. The real
|
# bootstrap.sh so `rig bootstrap <role>` stays the single entrypoint. The real
|
||||||
|
|
@ -474,12 +474,23 @@ fi
|
||||||
# rehearsal's job — so the harness proves what it can non-root: the whole
|
# rehearsal's job — so the harness proves what it can non-root: the whole
|
||||||
# arg/refusal surface, the pure parameter table, the rendered agent-context
|
# arg/refusal surface, the pure parameter table, the rendered agent-context
|
||||||
# file (guard note included), and grep-pins on the shipped script.
|
# file (guard note included), and grep-pins on the shipped script.
|
||||||
|
# THE HARD CUT, tenant half (#76). The pre-rename names are gone and must fail
|
||||||
|
# as UNKNOWN — asserted per name, because an alias left in for one tenant is the
|
||||||
|
# shape that survives review: the taxonomy reads complete while one old name
|
||||||
|
# still quietly converges. Checked at BOTH entrypoints, since bootstrap.sh has
|
||||||
|
# its own dispatch list and a name could survive in one and not the other.
|
||||||
|
for r in claude codex grok staging; do
|
||||||
|
check "tenant: the pre-#76 name '$r' is gone (tenant entrypoint)" 2 "unknown tenant role" \
|
||||||
|
"$ROOT/commands/bootstrap-tenant.sh" "$r"
|
||||||
|
check "tenant: the pre-#76 name '$r' is gone (bootstrap dispatch)" 2 "unknown role" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" "$r"
|
||||||
|
done
|
||||||
check "tenant: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap-tenant.sh" --help
|
check "tenant: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap-tenant.sh" --help
|
||||||
check "tenant: role required, exit 2" 2 "tenant role required" "$ROOT/commands/bootstrap-tenant.sh"
|
check "tenant: role required, exit 2" 2 "tenant role required" "$ROOT/commands/bootstrap-tenant.sh"
|
||||||
check "tenant: unknown role exits 2" 2 "unknown tenant role" "$ROOT/commands/bootstrap-tenant.sh" potato
|
check "tenant: unknown role exits 2" 2 "unknown tenant role" "$ROOT/commands/bootstrap-tenant.sh" potato
|
||||||
check "tenant: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap-tenant.sh" claude --nope
|
check "tenant: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap-tenant.sh" claude-box --nope
|
||||||
check "tenant: --user needs value" 2 "needs a value" "$ROOT/commands/bootstrap-tenant.sh" claude --user
|
check "tenant: --user needs value" 2 "needs a value" "$ROOT/commands/bootstrap-tenant.sh" claude-box --user
|
||||||
check "tenant: bad --user charset exits 2" 2 "invalid user" "$ROOT/commands/bootstrap-tenant.sh" claude --user 'fo|o'
|
check "tenant: bad --user charset exits 2" 2 "invalid user" "$ROOT/commands/bootstrap-tenant.sh" claude-box --user 'fo|o'
|
||||||
# The docker converge asserts the DAEMON answers, not just the client binary —
|
# The docker converge asserts the DAEMON answers, not just the client binary —
|
||||||
# a dead dockerd passing `docker --version` is the "linked but cannot run"
|
# a dead dockerd passing `docker --version` is the "linked but cannot run"
|
||||||
# scar in daemon form. Grep-pinned so the assert cannot ship deleted.
|
# scar in daemon form. Grep-pinned so the assert cannot ship deleted.
|
||||||
|
|
@ -488,19 +499,19 @@ check "tenant: dockerd effective-state assert is present" 0 "" \
|
||||||
# The machine-role traits die with the tenant story, never "unknown flag" — an
|
# The machine-role traits die with the tenant story, never "unknown flag" — an
|
||||||
# operator reaching for --hostname must learn where the trait family went.
|
# operator reaching for --hostname must learn where the trait family went.
|
||||||
check "tenant: trait flags die with the tenant story" 2 "have no traits" \
|
check "tenant: trait flags die with the tenant story" 2 "have no traits" \
|
||||||
"$ROOT/commands/bootstrap-tenant.sh" claude --class human
|
"$ROOT/commands/bootstrap-tenant.sh" claude-box --class human
|
||||||
check "tenant: --hostname dies the same way" 2 "have no traits" \
|
check "tenant: --hostname dies the same way" 2 "have no traits" \
|
||||||
"$ROOT/commands/bootstrap-tenant.sh" staging --hostname my-guest
|
"$ROOT/commands/bootstrap-tenant.sh" staging-box --hostname my-guest
|
||||||
# Dispatch: the machine-role entrypoint hands tenant roles to the tenant
|
# Dispatch: the machine-role entrypoint hands tenant roles to the tenant
|
||||||
# mechanism with args intact (--help reaching the TENANT usage proves both).
|
# mechanism with args intact (--help reaching the TENANT usage proves both).
|
||||||
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude|codex|grok|staging" \
|
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude-box|codex-box|grok-box|staging-box" \
|
||||||
"$ROOT/commands/bootstrap.sh" claude --help
|
"$ROOT/commands/bootstrap.sh" claude-box --help
|
||||||
# The marker guard fires BEFORE the root check (repo precedent: the coolify
|
# The marker guard fires BEFORE the root check (repo precedent: the coolify
|
||||||
# marker warning), so the refusals are provable here off fixture markers. A
|
# marker warning), so the refusals are provable here off fixture markers. A
|
||||||
# VM host (host=yes) refuses for every tenant — and names the staging rename,
|
# VM host (host=yes) refuses for every tenant — and names the staging PAIR,
|
||||||
# because a pre-#31 staging HOST re-running its old command is exactly who
|
# because whoever lands here has the two halves confused and wants the metal
|
||||||
# lands here. An agent tenant refuses ANY machine-role box; staging tolerates
|
# (staging-server). An agent tenant refuses ANY machine-role box; staging-box
|
||||||
# ONLY class=server with host=no — that is the staging guest after its
|
# tolerates ONLY class=server with host=no — that is the guest after its
|
||||||
# operator-run workload join, and re-converging it is what convergence is for.
|
# operator-run workload join, and re-converging it is what convergence is for.
|
||||||
# A non-server machine (class=human via custom) is NOT that guest, and server
|
# A non-server machine (class=human via custom) is NOT that guest, and server
|
||||||
# hardening would die at it with server-specific messaging — refuse instead.
|
# hardening would die at it with server-specific messaging — refuse instead.
|
||||||
|
|
@ -508,28 +519,28 @@ TEN_FIX="$(mktemp -d)"
|
||||||
printf 'role=dev-server class=human host=yes join=authkey\n' > "$TEN_FIX/host"
|
printf 'role=dev-server class=human host=yes join=authkey\n' > "$TEN_FIX/host"
|
||||||
printf 'role=workload-server class=server host=no join=authkey\n' > "$TEN_FIX/machine"
|
printf 'role=workload-server class=server host=no join=authkey\n' > "$TEN_FIX/machine"
|
||||||
printf 'role=custom class=human host=no join=login\n' > "$TEN_FIX/human"
|
printf 'role=custom class=human host=no join=login\n' > "$TEN_FIX/human"
|
||||||
printf 'role=claude tenant=yes host=no\n' > "$TEN_FIX/tenant"
|
printf 'role=claude-box tenant=yes host=no\n' > "$TEN_FIX/tenant"
|
||||||
check "tenant: staging refuses a non-server machine box" 1 "non-server machine role" \
|
check "tenant: staging-box refuses a non-server machine box" 1 "non-server machine role" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/human" "$ROOT/commands/bootstrap-tenant.sh" staging
|
env RIG_ROLE_MARKER="$TEN_FIX/human" "$ROOT/commands/bootstrap-tenant.sh" staging-box
|
||||||
check "tenant: refuses a host=yes box (a VM host is never a guest)" 1 "hosts VMs" \
|
check "tenant: refuses a host=yes box (a VM host is never a guest)" 1 "hosts VMs" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" claude
|
env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" claude-box
|
||||||
check "tenant: the host refusal names the old staging preset's new spelling" 1 "custom --class server --host yes" \
|
check "tenant: the host refusal sends you to the metal half of the pair" 1 "staging-server" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" staging
|
env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" staging-box
|
||||||
check "tenant: an agent role refuses a machine-role box" 1 "never tailnet machines" \
|
check "tenant: an agent role refuses a machine-role box" 1 "never tailnet machines" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" claude
|
env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" claude-box
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
# RIG_ROLE_MARKER pinned to the absent fixture: the marker guard runs before
|
# RIG_ROLE_MARKER pinned to the absent fixture: the marker guard runs before
|
||||||
# the root check, and the harness machine may carry a real /etc/rig/role.
|
# the root check, and the harness machine may carry a real /etc/rig/role.
|
||||||
check "tenant: claude parses, refuses non-root" 1 "must run as root" \
|
check "tenant: claude-box parses, refuses non-root" 1 "must run as root" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" claude
|
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" claude-box
|
||||||
check "tenant: codex parses, refuses non-root" 1 "must run as root" \
|
check "tenant: codex-box parses, refuses non-root" 1 "must run as root" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" codex
|
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" codex-box
|
||||||
check "tenant: grok parses, refuses non-root" 1 "must run as root" \
|
check "tenant: grok-box parses, refuses non-root" 1 "must run as root" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" grok
|
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" grok-box
|
||||||
check "tenant: staging tolerates a workload-joined guest's marker" 1 "must run as root" \
|
check "tenant: staging-box tolerates a workload-joined guest's marker" 1 "must run as root" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" staging
|
env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" staging-box
|
||||||
check "tenant: a tenant marker re-runs fine (convergence)" 1 "must run as root" \
|
check "tenant: a tenant marker re-runs fine (convergence)" 1 "must run as root" \
|
||||||
env RIG_ROLE_MARKER="$TEN_FIX/tenant" "$ROOT/commands/bootstrap-tenant.sh" claude
|
env RIG_ROLE_MARKER="$TEN_FIX/tenant" "$ROOT/commands/bootstrap-tenant.sh" claude-box
|
||||||
else
|
else
|
||||||
echo "skip: tenant non-root refusals (running as root)"
|
echo "skip: tenant non-root refusals (running as root)"
|
||||||
fi
|
fi
|
||||||
|
|
@ -545,24 +556,24 @@ tpath() { bash -c 'set -euo pipefail
|
||||||
. "$1/commands/lib/tenant-config.sh"; tenant_context_path "$2" "$3"' _ "$ROOT" "$1" "$2"; }
|
. "$1/commands/lib/tenant-config.sh"; tenant_context_path "$2" "$3"' _ "$ROOT" "$1" "$2"; }
|
||||||
tctx() { bash -c 'set -euo pipefail
|
tctx() { bash -c 'set -euo pipefail
|
||||||
. "$1/commands/lib/tenant-config.sh"; render_tenant_context "$2"' _ "$ROOT" "$1"; }
|
. "$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
|
check "tenant params: agent users are named after their agent" 0 "claude" tuser claude-box
|
||||||
check "tenant params: staging's user is box#69's ops" 0 "ops" tuser staging
|
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 /home/claude
|
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 /home/codex
|
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 /home/grok
|
check "tenant params: grok context lands in ~/.grok/AGENTS.md" 0 "/home/grok/.grok/AGENTS.md" tpath grok-box /home/grok
|
||||||
check "tenant params: staging has no context file" 1 "" tpath staging /home/ops
|
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
|
# 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.
|
# 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
|
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
|
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
|
check "tenant context: grok carries the box#80 guard" 0 "box setup-host" tctx grok-box
|
||||||
check "tenant context: the guard says whose host this is not" 0 "not a host you own" tctx claude
|
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
|
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
|
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
|
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
|
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
|
check "tenant context: grok names its login flow" 0 "grok login" tctx grok-box
|
||||||
check "tenant context: staging renders nothing (no agent lives there)" 1 "" tctx staging
|
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
|
# Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal
|
||||||
# idiom): nothing in the tenant mechanism touches the tailnet, prompts, or
|
# idiom): nothing in the tenant mechanism touches the tailnet, prompts, or
|
||||||
# apt-installs incus. A grep that finds nothing (exit 1) is the pass.
|
# apt-installs incus. A grep that finds nothing (exit 1) is the pass.
|
||||||
|
|
@ -572,15 +583,15 @@ check "tenant: non-interactive — nothing prompts" 1 "" \
|
||||||
grep -nE '\bread -r' "$ROOT/commands/bootstrap-tenant.sh"
|
grep -nE '\bread -r' "$ROOT/commands/bootstrap-tenant.sh"
|
||||||
check "tenant: never apt-installs incus (box owns the daemon)" 1 "" \
|
check "tenant: never apt-installs incus (box owns the daemon)" 1 "" \
|
||||||
grep -nE 'apt-get install.* incus' "$ROOT/commands/bootstrap-tenant.sh"
|
grep -nE 'apt-get install.* incus' "$ROOT/commands/bootstrap-tenant.sh"
|
||||||
# staging's posture rides the SAME hardening code as the machine roles — the
|
# staging-box's posture rides the SAME hardening code as the machine roles — the
|
||||||
# shared lib call is the anti-drift property, so pin the call, not the words.
|
# shared lib call is the anti-drift property, so pin the call, not the words.
|
||||||
check "tenant: staging hardens through the shared sshd lib" 0 "" \
|
check "tenant: staging-box hardens through the shared sshd lib" 0 "" \
|
||||||
grep -qE '^[[:space:]]*harden_sshd server$' "$ROOT/commands/bootstrap-tenant.sh"
|
grep -qE '^[[:space:]]*harden_sshd server$' "$ROOT/commands/bootstrap-tenant.sh"
|
||||||
check "tenant: docker lands via docker's own installer" 0 "" \
|
check "tenant: docker lands via docker's own installer" 0 "" \
|
||||||
grep -q "get.docker.com" "$ROOT/commands/bootstrap-tenant.sh"
|
grep -q "get.docker.com" "$ROOT/commands/bootstrap-tenant.sh"
|
||||||
# The #15 lesson pinned: 'box exec' shells read no rc files, so the CLI must
|
# The #15 lesson pinned: 'box exec' shells read no rc files, so the CLI must
|
||||||
# land on the SYSTEM path — and a claimed install is verified, not trusted:
|
# land on the SYSTEM path — and a claimed install is verified, not trusted:
|
||||||
# it must ANSWER as the tenant user (the grok template's scar: linked but
|
# it must ANSWER as the tenant user (the grok-box template's scar: linked but
|
||||||
# cannot run). The $CLI/$TENANT_USER are literals we grep for in the script.
|
# cannot run). The $CLI/$TENANT_USER are literals we grep for in the script.
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
check "tenant: the agent CLI lands on the system PATH" 0 "" \
|
check "tenant: the agent CLI lands on the system PATH" 0 "" \
|
||||||
|
|
@ -1473,7 +1484,7 @@ fi
|
||||||
# post-close-root state, strictly harder than what bootstrap installs. Byte-grep
|
# post-close-root state, strictly harder than what bootstrap installs. Byte-grep
|
||||||
# the widened assertion so a revert cannot ship green. The hardening block
|
# the widened assertion so a revert cannot ship green. The hardening block
|
||||||
# lives in lib/sshd.sh since #31 — ONE converger shared by the machine roles
|
# lives in lib/sshd.sh since #31 — ONE converger shared by the machine roles
|
||||||
# and the staging tenant — so the greps pin the lib, and a call-site grep pins
|
# and the staging-box tenant — so the greps pin the lib, and a call-site grep pins
|
||||||
# that bootstrap actually runs it (a function nobody calls is not hardening).
|
# that bootstrap actually runs it (a function nobody calls is not hardening).
|
||||||
check "sshd lib: permitrootlogin assertion accepts the closed state" 0 "" \
|
check "sshd lib: permitrootlogin assertion accepts the closed state" 0 "" \
|
||||||
grep -qF "permitrootlogin (no|prohibit-password|without-password)" "$ROOT/commands/lib/sshd.sh"
|
grep -qF "permitrootlogin (no|prohibit-password|without-password)" "$ROOT/commands/lib/sshd.sh"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue