feat(bootstrap)!: box tenant roles carry a -box suffix
#80
9 changed files with 520 additions and 287 deletions
82
CHANGELOG.md
82
CHANGELOG.md
|
|
@ -6,6 +6,88 @@ on the way to cutting its first release, and this file starts there.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### 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
|
||||||
|
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
|
||||||
|
mints. Both families lived in one flat namespace, and no role name said which
|
||||||
|
one you were asking for. `staging` is where that stopped being cosmetic — the
|
||||||
|
word names the metal that hosts guests *and* the guests on it, only one of
|
||||||
|
them could have the name, and #31 gave it to the guests. The VM-host shape
|
||||||
|
was left with no name at all, spelled `custom --class server --host yes
|
||||||
|
--join authkey`, which is what every refusal in the tree recited at an
|
||||||
|
operator who had confused the two.
|
||||||
|
|
||||||
|
So the suffix names the family: `control-plane-server`, `workload-server`,
|
||||||
|
`runner-server`, `dev-server`, and the restored `staging-server`
|
||||||
|
(`class=server host=yes join=authkey` — the preset #31 retired, back under a
|
||||||
|
name that cannot be mistaken for its own guests). `host=yes` already installs
|
||||||
|
the box CLI and runs box's `setup-host`, so `staging-server` is a table row
|
||||||
|
rather than new machinery, and it stays **out** of the `tag:server`
|
||||||
|
allow-list on purpose: a host is never managed by the control plane, its
|
||||||
|
guests are, so mint its key with `tag:local`.
|
||||||
|
|
||||||
|
**`custom` and `workstation` keep bare names**, and that is the rule rather
|
||||||
|
than an exception to it. `custom` presets nothing and can be any shape — a
|
||||||
|
guest included — so a family claim is one it cannot make. `workstation` is
|
||||||
|
somebody's own device rather than fleet infrastructure: it joins by
|
||||||
|
interactive login, comes up user-owned and untagged, and the tailnet never
|
||||||
|
manages it.
|
||||||
|
|
||||||
|
**Migration — this is a hard cut, with no aliases.** The old names are
|
||||||
|
refused as unknown roles; a box bootstrapped under one is re-bootstrapped
|
||||||
|
rather than migrated, which at this fleet size costs less than four
|
||||||
|
deprecation paths each quietly keeping an old name alive. Two consequences
|
||||||
|
worth knowing before you re-run anything. `TS_HOSTNAME` defaults to the role
|
||||||
|
name, so a box that took the default now comes up as `control-plane-server`
|
||||||
|
rather than `control-plane` — pass `--hostname` to hold a name steady, and
|
||||||
|
check anything pinning one (ACL entries, a `cast` `environments.yaml` server
|
||||||
|
name, host keys). And `rig coolify install` / `rig coolify backup install`
|
||||||
|
match the **role name** in `/etc/rig/role`, so they now look for
|
||||||
|
`role=control-plane-server`; a pre-rename control plane takes their warning
|
||||||
|
branch until it is re-bootstrapped. That check has always been advisory and
|
||||||
|
never a gate, so the run still proceeds and the warning names the repair.
|
||||||
|
|
||||||
|
The rename also reaches every string that *tells an operator to run a role*,
|
||||||
|
not just the code that accepts one — `bootstrap-tenant.sh` emits the staging
|
||||||
|
guest's tailnet-join next step (`sudo rig bootstrap workload-server`), and
|
||||||
|
two of its refusals recite the machine-role list. A stale next-step is worse
|
||||||
|
than a stale flag: it fails when someone copy-pastes it, on a different box,
|
||||||
|
minutes after the run that printed it reported success. `test/cli.sh` sweeps
|
||||||
|
every shipped script for pre-rename role names rather than pinning the known
|
||||||
|
sites, because the next instance of this will be somewhere else.
|
||||||
|
|
||||||
|
`dev-server` is `class=human`, which reads like a contradiction and is not:
|
||||||
|
the suffix names the family, the class names the root-SSH door policy, and
|
||||||
|
operators enter a dev box as themselves so `close-root` shuts its door. The
|
||||||
|
two axes genuinely share the word "server", which is a wart — #77 renames the
|
||||||
|
class trait to what it actually controls, and is kept separate because it
|
||||||
|
reaches markers on live machines that guard root SSH.
|
||||||
|
|
||||||
## 0.2.0 — 2026-07-19
|
## 0.2.0 — 2026-07-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
169
README.md
169
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
|
||||||
|
|
@ -90,18 +91,18 @@ itself is untouched — what bootstrap converged stays converged.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### `rig bootstrap <control-plane|workload|runner|dev|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 --hostname my-coolify-box --users ./users
|
rig bootstrap control-plane-server --hostname my-coolify-box --users ./users
|
||||||
rig bootstrap workload --hostname my-prod-box --users ./users
|
rig bootstrap workload-server --hostname my-prod-box --users ./users
|
||||||
rig bootstrap runner --hostname my-ci-box --users ./users
|
rig bootstrap runner-server --hostname my-ci-box --users ./users
|
||||||
rig bootstrap dev --hostname my-dev-box --users ./users
|
rig bootstrap dev-server --hostname my-dev-box --users ./users
|
||||||
rig bootstrap workstation --hostname my-laptop --users ./users
|
rig bootstrap workstation --hostname my-laptop --users ./users
|
||||||
rig bootstrap custom --hostname my-vm-host --class server --host yes --join authkey --users ./users
|
rig bootstrap custom --hostname my-vm-host --class server --host yes --join authkey --users ./users
|
||||||
```
|
```
|
||||||
|
|
@ -125,8 +126,8 @@ commands and the second one was easy to forget. Now it takes it, and
|
||||||
**requires** it:
|
**requires** it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rig bootstrap dev --hostname my-dev-box --users ./users # one command, people included
|
rig bootstrap dev-server --hostname my-dev-box --users ./users # one command, people included
|
||||||
rig bootstrap dev --hostname my-dev-box --no-users # deliberately root-only
|
rig bootstrap dev-server --hostname my-dev-box --no-users # deliberately root-only
|
||||||
```
|
```
|
||||||
|
|
||||||
`--users <path>` runs exactly what `rig users apply --file <path>` runs, as
|
`--users <path>` runs exactly what `rig users apply --file <path>` runs, as
|
||||||
|
|
@ -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
|
||||||
|
|
@ -187,27 +188,50 @@ presets nothing and requires `--hostname` plus all three traits.
|
||||||
| `host` | `yes`, `no` | whether the box exists to run VMs — the `/dev/kvm` advisory and, on `yes`, installing the `box` CLI + running box's `setup-host` |
|
| `host` | `yes`, `no` | whether the box exists to run VMs — the `/dev/kvm` advisory and, on `yes`, installing the `box` CLI + running box's `setup-host` |
|
||||||
| `join` | `authkey`, `login` | tagged pre-auth key (fleet identity) vs interactive browser login (user-owned device) |
|
| `join` | `authkey`, `login` | tagged pre-auth key (fleet identity) vs interactive browser login (user-owned device) |
|
||||||
|
|
||||||
| role | class | host | join | tailnet tag |
|
| role | class | host | join | tailnet tag |
|
||||||
|-----------------|--------|------|---------|-------------|
|
|------------------------|--------|------|---------|-------------|
|
||||||
| `control-plane` | server | no | authkey | `tag:server` |
|
| `control-plane-server` | server | no | authkey | `tag:server` |
|
||||||
| `workload` | server | no | authkey | `tag:server` |
|
| `workload-server` | server | no | authkey | `tag:server` |
|
||||||
| `runner` | server | no | authkey | `tag:ci` — refuses `tag:server` |
|
| `runner-server` | server | no | authkey | `tag:ci` — refuses `tag:server` |
|
||||||
| `dev` | human | yes | authkey | `tag:local` — refuses `tag:server` |
|
| `staging-server` | server | yes | authkey | `tag:local` — refuses `tag:server` |
|
||||||
| `workstation` | human | yes | login | untagged — any tag refused |
|
| `dev-server` | human | yes | authkey | `tag:local` — refuses `tag:server` |
|
||||||
|
| `workstation` | human | yes | login | untagged — any tag refused |
|
||||||
|
|
||||||
> **Where the `staging` preset went.** Before #31, `staging` was the VM-host
|
> **The suffix names the family, not the class** (#76). rig builds two kinds
|
||||||
> preset (`class=server host=yes join=authkey`). The name now belongs to the
|
> of thing on opposite sides of a trust boundary — tailnet **machines** it
|
||||||
> box TENANT family — the *guest*, not the host under it — because that is
|
> converges, and **guests** a box mints — and for a while nothing in a role
|
||||||
> what "staging" mostly names in practice (the box the control plane will
|
> name said which you were asking for. `staging` made that concrete: the word
|
||||||
> manage), and two meanings on one word was the worse bug. The host shape
|
> named both the metal that hosts guests and the guests on it, and only one of
|
||||||
> lost nothing: it is one flag away — `rig bootstrap dev --class server
|
> them could have it. So `-server` marks a fleet machine and `-box` marks a
|
||||||
> --hostname my-vm-host` — or fully spelled as `custom --class server --host
|
> box tenant, everywhere, and `staging-server` / `staging-box` are simply the
|
||||||
> yes --join authkey`. Tag policy is unchanged: mint the host's key with
|
> two halves spelled out. `staging-server` restores the VM-host preset #31
|
||||||
> `tag:local`; an effective `tag:server` is refused on every role but
|
> retired, under a name that cannot be confused with its own guests.
|
||||||
> `control-plane` and `workload`.
|
>
|
||||||
|
> Two roles take **no** suffix, on purpose. `custom` presets nothing and can
|
||||||
|
> be any shape — a guest included — so a family claim is one it cannot make.
|
||||||
|
> `workstation` is somebody's own device rather than fleet infrastructure: it
|
||||||
|
> joins by interactive login, comes up user-owned and untagged, and the
|
||||||
|
> tailnet never manages it.
|
||||||
|
>
|
||||||
|
> **`dev-server` is `class=human`, and that is not a contradiction** — though
|
||||||
|
> it is a wart. The suffix names the *family*; the class names the *root-SSH
|
||||||
|
> door policy*, and operators enter a dev box as themselves, so `close-root`
|
||||||
|
> shuts its door. Two orthogonal axes that happen to share the word "server".
|
||||||
|
> [#77](https://github.com/heavy-duty/rig/issues/77) renames the class trait
|
||||||
|
> to what it actually controls, which is the real fix; it touches markers on
|
||||||
|
> live machines, so it is deliberately not folded in here.
|
||||||
|
>
|
||||||
|
> **This was a hard cut — no aliases.** Old role names stop working, and a
|
||||||
|
> box bootstrapped under one is re-bootstrapped rather than migrated. Two
|
||||||
|
> things follow. The default tailnet hostname is the role name, so a box that
|
||||||
|
> took the default now comes up as `control-plane-server`; pass `--hostname`
|
||||||
|
> to hold a name steady. And `rig coolify install` / `rig coolify backup
|
||||||
|
> install` match `role=control-plane-server` in the marker, so a pre-rename
|
||||||
|
> control plane takes their (advisory, non-fatal) warning until it is
|
||||||
|
> re-bootstrapped.
|
||||||
|
|
||||||
The tag column is **derived policy, not a fourth trait**: `tag:server` means
|
The tag column is **derived policy, not a fourth trait**: `tag:server` means
|
||||||
"the control plane manages this box", and `control-plane` and `workload` are
|
"the control plane manages this box", and `control-plane-server` and `workload-server` are
|
||||||
the only shapes it manages — every other role refuses an effective
|
the only shapes it manages — every other role refuses an effective
|
||||||
`tag:server` after join, one rule instead of per-role exceptions.
|
`tag:server` after join, one rule instead of per-role exceptions.
|
||||||
|
|
||||||
|
|
@ -292,9 +316,9 @@ catches a box bootstrapped before this change, or retagged behind rig's back).
|
||||||
> tag flag, re-tagging needs a fresh key via `up --force-reauth` — so rig detects
|
> tag flag, re-tagging needs a fresh key via `up --force-reauth` — so rig detects
|
||||||
> and refuses, and never claims a convergence it cannot perform.
|
> and refuses, and never claims a convergence it cannot perform.
|
||||||
|
|
||||||
`control-plane` and `workload` are identical today except the default
|
`control-plane-server` and `workload-server` are identical today except the default
|
||||||
hostname; they exist because the boxes diverge over time, and because each
|
hostname; they exist because the boxes diverge over time, and because each
|
||||||
follow-up command applies to exactly one role. `runner` is the box a CI agent
|
follow-up command applies to exactly one role. `runner-server` is the box a CI agent
|
||||||
will live on, and it differs behaviorally: it **refuses `tag:server`**. That
|
will live on, and it differs behaviorally: it **refuses `tag:server`**. That
|
||||||
refusal moved onto the *effective* tag and is strictly stronger for it — it is
|
refusal moved onto the *effective* tag and is strictly stronger for it — it is
|
||||||
no longer "don't advertise `tag:server`" but "the key you actually used must not
|
no longer "don't advertise `tag:server`" but "the key you actually used must not
|
||||||
|
|
@ -305,13 +329,13 @@ hard, post-join error.
|
||||||
|
|
||||||
The VM-host shape — the box that *hosts* staging boxes: Incus VMs minted by
|
The VM-host shape — the box that *hosts* staging boxes: Incus VMs minted by
|
||||||
the [`box`](https://github.com/heavy-duty/box) CLI, each converged from inside
|
the [`box`](https://github.com/heavy-duty/box) CLI, each converged from inside
|
||||||
with the tenant roles and (for staging guests) `rig bootstrap workload` —
|
with the tenant roles and (for staging guests) `rig bootstrap workload-server` —
|
||||||
rides the traits since #31 (`--class server --host yes --join authkey`; see
|
is the `staging-server` role (`--class server --host yes --join authkey`; see
|
||||||
the note above). It is `class=server`: an unattended VM appliance — operators
|
the note above). It is `class=server`: an unattended VM appliance — operators
|
||||||
converge it and leave; nobody lives there. Mint its key with `tag:local`: the
|
converge it and leave; nobody lives there. Mint its key with `tag:local`: the
|
||||||
host and its guests sit on opposite sides of a trust boundary, and the *host*
|
host and its guests sit on opposite sides of a trust boundary, and the *host*
|
||||||
is never managed by the control plane — so an effective **`tag:server` is
|
is never managed by the control plane — so an effective **`tag:server` is
|
||||||
refused**, same mechanism as `runner`.
|
refused**, same mechanism as `runner-server`.
|
||||||
|
|
||||||
On a host-class box (`host=yes`), bootstrap finishes the job instead of leaving
|
On a host-class box (`host=yes`), bootstrap finishes the job instead of leaving
|
||||||
a to-do: after the role marker is written it **installs the `box` CLI globally
|
a to-do: after the role marker is written it **installs the `box` CLI globally
|
||||||
|
|
@ -344,21 +368,21 @@ merges box's root install lands in `/root`.)
|
||||||
> fork); `RIG_SKIP_BOX_INSTALL=1` opts out entirely for a host whose box you
|
> fork); `RIG_SKIP_BOX_INSTALL=1` opts out entirely for a host whose box you
|
||||||
> manage by hand.
|
> manage by hand.
|
||||||
|
|
||||||
`dev` is the human-class VM-hosting shape — `tag:local`, box CLI installed as
|
`dev-server` is the human-class VM-hosting shape — `tag:local`, box CLI installed as
|
||||||
above, a person living on it (`--class server` turns it into the unattended
|
above, a person living on it (`--class server` turns it into the unattended
|
||||||
VM-host appliance) — and `workstation` is the machine at the keyboard end of
|
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
|
||||||
|
|
@ -374,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
|
||||||
|
|
@ -389,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 --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.
|
||||||
|
|
||||||
|
|
@ -441,17 +471,18 @@ themself and elevates via sudo.
|
||||||
Per role, the whole identity picture at a glance — issue #25's class
|
Per role, the whole identity picture at a glance — issue #25's class
|
||||||
comparison, translated onto the traits that replaced the class binary:
|
comparison, translated onto the traits that replaced the class binary:
|
||||||
|
|
||||||
| role | class | host | join | who lives here | root SSH after `rig users apply` |
|
| role | class | host | join | who lives here | root SSH after `rig users apply` |
|
||||||
|-----------------|--------|------|---------|--------------------------------------|----------------------------------|
|
|------------------------|--------|------|---------|--------------------------------------|----------------------------------|
|
||||||
| `control-plane` | server | no | authkey | nobody — Coolify runs here | open — the automation door |
|
| `control-plane-server` | server | no | authkey | nobody — Coolify runs here | open — the automation door |
|
||||||
| `workload` | server | no | authkey | nobody — deployed services run here | open — the automation door |
|
| `workload-server` | server | no | authkey | nobody — deployed services run here | open — the automation door |
|
||||||
| `runner` | server | no | authkey | nobody — CI jobs as `github-runner` | open — the automation door |
|
| `runner-server` | server | no | authkey | nobody — CI jobs as `github-runner` | open — the automation door |
|
||||||
| `dev` | human | yes | authkey | operators, minting boxes | closed by `rig users close-root` |
|
| `staging-server` | server | yes | authkey | nobody — it mints and hosts guests | open — the automation door |
|
||||||
| `workstation` | human | yes | login | its owner | closed by `rig users close-root` |
|
| `dev-server` | human | yes | authkey | operators, minting boxes | closed by `rig users close-root` |
|
||||||
|
| `workstation` | human | yes | login | its owner | closed by `rig users close-root` |
|
||||||
|
|
||||||
(The unattended VM-host appliance — formerly the `staging` preset — is the
|
(`staging-server` is that unattended VM-host appliance, and the row above is
|
||||||
`class=server host=yes join=authkey` shape: nobody lives there, root SSH stays
|
the whole of it: nobody lives there, root SSH stays open as the automation
|
||||||
open as the automation door. The box TENANT roles sit outside this table on
|
door. The box TENANT roles sit outside this table on
|
||||||
purpose: a guest is not a tailnet machine, and its marker carries no `class=`,
|
purpose: a guest is not a tailnet machine, and its marker carries no `class=`,
|
||||||
so `rig users close-root` fails closed on it.)
|
so `rig users close-root` fails closed on it.)
|
||||||
|
|
||||||
|
|
@ -463,7 +494,7 @@ operators land via `rig users apply` on every class and elevate through sudo
|
||||||
as root. **Machine identities stay machine-shaped**: Coolify's automation
|
as root. **Machine identities stay machine-shaped**: Coolify's automation
|
||||||
SSHes in as root (that is what server-class root *is*), CI jobs run as the
|
SSHes in as root (that is what server-class root *is*), CI jobs run as the
|
||||||
unprivileged `github-runner`, and guest VMs are their own server-class boxes,
|
unprivileged `github-runner`, and guest VMs are their own server-class boxes,
|
||||||
converged from inside by `rig bootstrap workload`.
|
converged from inside by `rig bootstrap workload-server`.
|
||||||
|
|
||||||
**`class` decides root SSH's fate — after `rig users apply`, never before.**
|
**`class` decides root SSH's fate — after `rig users apply`, never before.**
|
||||||
On `class=human`, root SSH closes entirely (`rig users close-root`, below).
|
On `class=human`, root SSH closes entirely (`rig users close-root`, below).
|
||||||
|
|
@ -472,7 +503,7 @@ root there is the **automation** identity the control plane (Coolify) SSHes
|
||||||
in as. It is a machine door, never a human one.
|
in as. It is a machine door, never a human one.
|
||||||
|
|
||||||
**Where this diverges from #17's original table:** that table let the
|
**Where this diverges from #17's original table:** that table let the
|
||||||
`runner` role close root ("no Coolify involved"). The class model supersedes
|
`runner-server` role close root ("no Coolify involved"). The class model supersedes
|
||||||
the per-role call: runner is `class=server` — an automation identity, not a
|
the per-role call: runner is `class=server` — an automation identity, not a
|
||||||
person's box — and on every server-class machine root SSH is the management
|
person's box — and on every server-class machine root SSH is the management
|
||||||
plane rig itself converges through, so `close-root` refuses there
|
plane rig itself converges through, so `close-root` refuses there
|
||||||
|
|
@ -640,11 +671,11 @@ throwaway containers on every push.
|
||||||
|
|
||||||
### `rig runner install --repo <owner/repo>`
|
### `rig runner install --repo <owner/repo>`
|
||||||
|
|
||||||
Runner box only, run after `rig bootstrap runner` (the same two-step rhythm
|
Runner box only, run after `rig bootstrap runner-server` (the same two-step rhythm
|
||||||
as `bootstrap control-plane` → `coolify install`):
|
as `bootstrap control-plane-server` → `coolify install`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rig bootstrap runner --hostname my-ci-box --users ./users
|
rig bootstrap runner-server --hostname my-ci-box --users ./users
|
||||||
rig runner install --repo acme/widgets
|
rig runner install --repo acme/widgets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1006,9 +1037,9 @@ split: the harness proves the arg/refusal surface, the marker guards (off
|
||||||
fixture markers), the pure parameter table, and the rendered agent-context
|
fixture markers), the pure parameter table, and the rendered agent-context
|
||||||
file — guard note included — plus absence-greps for the creds-free contract;
|
file — guard note included — plus absence-greps for the creds-free contract;
|
||||||
the real converge belongs to the rehearsal. The end-to-end rehearsal is a
|
the real converge belongs to the rehearsal. The end-to-end rehearsal is a
|
||||||
throwaway VM/container: pristine Debian → install → `bootstrap workload` 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.
|
||||||
|
|
|
||||||
20
bin/rig
20
bin/rig
|
|
@ -13,7 +13,8 @@ usage() {
|
||||||
usage: rig <command> [args]
|
usage: rig <command> [args]
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
bootstrap <control-plane|workload|runner|dev|workstation|custom>
|
bootstrap <control-plane-server|workload-server|runner-server|staging-server|
|
||||||
|
dev-server|workstation|custom>
|
||||||
(--users <path> | --no-users)
|
(--users <path> | --no-users)
|
||||||
[--hostname <name>] [--class <human|server>] [--host <yes|no>]
|
[--hostname <name>] [--class <human|server>] [--host <yes|no>]
|
||||||
[--join <authkey|login>]
|
[--join <authkey|login>]
|
||||||
|
|
@ -27,16 +28,19 @@ commands:
|
||||||
overrides its trait, and custom states all of them. Prompts for a
|
overrides its trait, and custom states all of them. Prompts for a
|
||||||
single-use TAGGED tailnet pre-auth key (TS_AUTHKEY env overrides the
|
single-use TAGGED tailnet pre-auth key (TS_AUTHKEY env overrides the
|
||||||
prompt); the key's tags are the tailnet tag, verified after join —
|
prompt); the key's tags are the tailnet tag, verified after join —
|
||||||
only control-plane and workload may carry tag:server. join=login
|
only control-plane-server and workload-server may carry tag:server.
|
||||||
|
The '-server' suffix names the FAMILY (a fleet machine, vs the '-box'
|
||||||
|
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'. 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,9 +9,9 @@
|
||||||
# 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' 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.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -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` 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|workload|runner|dev|workstation|custom). A tenant box's shape comes from its seed; staging's tailnet join is operator-run via 'rig bootstrap workload'." 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', 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,8 +386,8 @@ 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 --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"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,12 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: rig bootstrap <control-plane|workload|runner|dev|workstation|custom>
|
usage: rig bootstrap <control-plane-server|workload-server|runner-server|
|
||||||
|
staging-server|dev-server|workstation|custom>
|
||||||
(--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)
|
||||||
|
|
||||||
|
|
@ -50,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
|
||||||
|
|
@ -60,23 +61,39 @@ operator file has nothing to converge in there.
|
||||||
Roles are presets over the three traits; any flag overrides its trait.
|
Roles are presets over the three traits; any flag overrides its trait.
|
||||||
custom presets nothing and requires --hostname plus all three traits.
|
custom presets nothing and requires --hostname plus all three traits.
|
||||||
|
|
||||||
role class host join
|
role class host join
|
||||||
control-plane server no authkey
|
control-plane-server server no authkey
|
||||||
workload server no authkey
|
workload-server server no authkey
|
||||||
runner server no authkey
|
runner-server server no authkey
|
||||||
dev human yes authkey
|
staging-server server yes authkey
|
||||||
workstation human yes login
|
dev-server human yes authkey
|
||||||
|
workstation human yes login
|
||||||
|
|
||||||
The former staging VM-host preset is now spelled through the traits:
|
THE SUFFIX NAMES THE FAMILY, not the class. '-server' means this role builds a
|
||||||
'custom --class server --host yes --join authkey' (or 'dev --class server').
|
fleet MACHINE — a tailnet node rig converges; '-box' (the tenant roles) means a
|
||||||
'staging' names the box TENANT role today — the guest, not the host.
|
GUEST a box mints. Two families lived in one flat namespace and nothing in a
|
||||||
|
name said which you were asking for; 'staging' made that concrete by naming
|
||||||
|
both the metal and the guests on it.
|
||||||
|
|
||||||
|
custom no suffix: it presets nothing and can be any shape, a guest
|
||||||
|
included, so a family claim would be one it cannot make.
|
||||||
|
workstation no suffix: somebody's own device, not fleet infrastructure —
|
||||||
|
it joins by interactive login and comes up user-owned and
|
||||||
|
untagged, and the tailnet never manages it.
|
||||||
|
|
||||||
|
'dev-server' is class=human, and that is not a contradiction: the suffix names
|
||||||
|
the family, the CLASS names the root-SSH door policy (operators enter a dev box
|
||||||
|
as themselves, so 'users close-root' shuts its door). The two axes share the
|
||||||
|
word 'server' and that is a genuine wart — tracked in #77, which renames the
|
||||||
|
class trait to what it actually controls.
|
||||||
|
|
||||||
The tailnet tag is NOT a rig argument. A pre-auth key is minted WITH its tags,
|
The tailnet tag is NOT a rig argument. A pre-auth key is minted WITH its tags,
|
||||||
so the key is the single source of truth: rig no longer requests a tag it might
|
so the key is the single source of truth: rig no longer requests a tag it might
|
||||||
disagree with. After the box joins, rig reads the tag control actually GRANTED
|
disagree with. After the box joins, rig reads the tag control actually GRANTED
|
||||||
(tailscale status .Self.Tags) and asserts on THAT — an untagged key is refused
|
(tailscale status .Self.Tags) and asserts on THAT — an untagged key is refused
|
||||||
outright, and only control-plane and workload may carry tag:server (they are
|
outright, and only control-plane-server and workload-server may carry
|
||||||
the only shapes the control plane manages). Mint a correctly-tagged key.
|
tag:server (they are the only shapes the control plane manages). Mint a
|
||||||
|
correctly-tagged key.
|
||||||
|
|
||||||
join=authkey: provide the single-use tailscale pre-auth key via the TS_AUTHKEY
|
join=authkey: provide the single-use tailscale pre-auth key via the TS_AUTHKEY
|
||||||
env var, or enter it at the interactive prompt. Used once, never written to disk.
|
env var, or enter it at the interactive prompt. Used once, never written to disk.
|
||||||
|
|
@ -91,16 +108,16 @@ 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
|
||||||
control-plane|workload|runner|dev|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|workload|runner|dev|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|workload|runner|dev|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).
|
||||||
|
|
@ -109,11 +126,16 @@ esac
|
||||||
# for the shape nobody foresaw — it declares nothing and must state all three.
|
# for the shape nobody foresaw — it declares nothing and must state all three.
|
||||||
CLASS="" HOST="" JOIN=""
|
CLASS="" HOST="" JOIN=""
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
control-plane) CLASS=server HOST=no JOIN=authkey ;;
|
control-plane-server) CLASS=server HOST=no JOIN=authkey ;;
|
||||||
workload) CLASS=server HOST=no JOIN=authkey ;;
|
workload-server) CLASS=server HOST=no JOIN=authkey ;;
|
||||||
runner) CLASS=server HOST=no JOIN=authkey ;;
|
runner-server) CLASS=server HOST=no JOIN=authkey ;;
|
||||||
dev) CLASS=human HOST=yes JOIN=authkey ;;
|
# The unattended VM host — the shape #31 retired when 'staging' moved to the
|
||||||
workstation) CLASS=human HOST=yes JOIN=login ;;
|
# tenant family, restored under a name that cannot be confused with its own
|
||||||
|
# guests. host=yes is the whole point: it is what installs the box CLI and
|
||||||
|
# runs box's setup-host further down, so this is a table row, not machinery.
|
||||||
|
staging-server) CLASS=server HOST=yes JOIN=authkey ;;
|
||||||
|
dev-server) CLASS=human HOST=yes JOIN=authkey ;;
|
||||||
|
workstation) CLASS=human HOST=yes JOIN=login ;;
|
||||||
custom) ;;
|
custom) ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|
@ -430,25 +452,26 @@ verify_effective_tag() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# tag:server policy is DERIVED, not a trait: it means "the control plane
|
# tag:server policy is DERIVED, not a trait: it means "the control plane
|
||||||
# manages this box", and only control-plane and workload are shapes the
|
# manages this box", and only control-plane-server and workload-server are shapes the
|
||||||
# control plane manages. Everything else refuses it on the EFFECTIVE tag —
|
# control plane manages. Everything else refuses it on the EFFECTIVE tag —
|
||||||
# strictly stronger than the old request-time check, which only guarded the
|
# strictly stronger than the old request-time check, which only guarded the
|
||||||
# tag rig HOPED for. The fleet has been bitten both ways: a runner carrying
|
# tag rig HOPED for. The fleet has been bitten both ways: a runner-server carrying
|
||||||
# tag:server extends every server grant to repo-controlled code, and a
|
# tag:server extends every server grant to repo-controlled code, and a
|
||||||
# staging host carrying it extends them to a box the control plane does not
|
# staging host carrying it extends them to a box the control plane does not
|
||||||
# even know. Refused, never warned; rig can DETECT this but cannot FIX it,
|
# even know. Refused, never warned; rig can DETECT this but cannot FIX it,
|
||||||
# so each refusal names its repair.
|
# so each refusal names its repair.
|
||||||
if printf '%s\n' "$tags" | grep -qx 'tag:server'; then
|
if printf '%s\n' "$tags" | grep -qx 'tag:server'; then
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
control-plane|workload) ;;
|
control-plane-server|workload-server) ;;
|
||||||
runner)
|
runner-server)
|
||||||
die "role runner joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). The key you used grants tag:server to repo-controlled code; that must never happen. Re-run bootstrap with a key minted for a CI tag (e.g. tag:ci)." ;;
|
die "role runner-server joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). The key you used grants tag:server to repo-controlled code; that must never happen. Re-run bootstrap with a key minted for a CI tag (e.g. tag:ci)." ;;
|
||||||
*)
|
*)
|
||||||
# This arm now also owns the VM-host shape the old staging preset
|
# This arm owns the VM-host shape too — 'staging-server' by name now,
|
||||||
# covered (custom/dev --class server): a host is never managed by the
|
# plus custom/dev-server --class server: a host is never managed by the
|
||||||
# control plane — its guest VMs are — so tag:server is refused there
|
# control plane — its guest VMs are — so tag:server is refused there
|
||||||
# like everywhere else outside control-plane|workload.
|
# like everywhere else outside the two control-plane-managed shapes.
|
||||||
die "role $ROLE joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). Only control-plane and workload are managed by the control plane; tag:server on this box extends every server grant to it. Re-run bootstrap with a key minted for a non-server tag (e.g. tag:local)." ;;
|
# Mint the metal's key with tag:local.
|
||||||
|
die "role $ROLE joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). Only control-plane-server and workload-server are managed by the control plane; tag:server on this box extends every server grant to it. Re-run bootstrap with a key minted for a non-server tag (e.g. tag:local)." ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -703,9 +726,9 @@ if [ -n "$USERS_FILE" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
|
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
|
||||||
if [ "$ROLE" = "control-plane" ]; then
|
if [ "$ROLE" = "control-plane-server" ]; then
|
||||||
log "next: rig coolify install --version <pin>"
|
log "next: rig coolify install --version <pin>"
|
||||||
elif [ "$ROLE" = "runner" ]; then
|
elif [ "$ROLE" = "runner-server" ]; then
|
||||||
log "next: rig runner install --repo <owner/repo> --version <pin>"
|
log "next: rig runner install --repo <owner/repo> --version <pin>"
|
||||||
fi
|
fi
|
||||||
# Every class gets operators: humans always enter as themselves and elevate via
|
# Every class gets operators: humans always enter as themselves and elevate via
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,13 @@ done
|
||||||
# certainly means the wrong SSH session — but the marker is advisory and may be
|
# certainly means the wrong SSH session — but the marker is advisory and may be
|
||||||
# absent, so WARN, never die, and warn before the root check so the harness can
|
# absent, so WARN, never die, and warn before the root check so the harness can
|
||||||
# prove it non-root (RIG_ROLE_MARKER points it at fixtures, repo precedent).
|
# prove it non-root (RIG_ROLE_MARKER points it at fixtures, repo precedent).
|
||||||
|
# Matches the ROLE NAME, so #76's rename reaches it the same way it reaches
|
||||||
|
# `coolify install` — a pre-rename marker takes the warning branch, which is
|
||||||
|
# the hard cut behaving as designed rather than a regression.
|
||||||
MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")"
|
MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")"
|
||||||
case "$MARKER_LINE" in
|
case "$MARKER_LINE" in
|
||||||
""|"role=control-plane"|"role=control-plane "*) ;;
|
""|"role=control-plane-server"|"role=control-plane-server "*) ;;
|
||||||
*) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. The nightly dump targets Coolify's own database, which lives on role control-plane; if this is the wrong box, stop here and re-check your SSH session." ;;
|
*) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. The nightly dump targets Coolify's own database, which lives on role control-plane-server; if this is the wrong box, stop here and re-check your SSH session." ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# --- guards ----------------------------------------------------------------
|
# --- guards ----------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,17 @@ fi
|
||||||
# are: the harness proves it non-root, and reading a 0644 file needs no
|
# are: the harness proves it non-root, and reading a 0644 file needs no
|
||||||
# privilege. RIG_ROLE_MARKER overrides the path so tests point it at fixtures
|
# privilege. RIG_ROLE_MARKER overrides the path so tests point it at fixtures
|
||||||
# (repo precedent: users-apply, users-close-root).
|
# (repo precedent: users-apply, users-close-root).
|
||||||
|
#
|
||||||
|
# This match is on the ROLE NAME, which #76's rename therefore reaches: a box
|
||||||
|
# bootstrapped before the rename carries 'role=control-plane' and now takes the
|
||||||
|
# warning branch. That is the hard cut behaving as designed — the marker is
|
||||||
|
# advisory, the run still proceeds, and the warning names the re-bootstrap that
|
||||||
|
# makes the marker true again. Nothing here is load-bearing enough to justify
|
||||||
|
# carrying the old name forever.
|
||||||
MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")"
|
MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")"
|
||||||
case "$MARKER_LINE" in
|
case "$MARKER_LINE" in
|
||||||
""|"role=control-plane"|"role=control-plane "*) ;;
|
""|"role=control-plane-server"|"role=control-plane-server "*) ;;
|
||||||
*) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. Coolify belongs on role control-plane; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane' first so the marker tells the truth." ;;
|
*) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. Coolify belongs on role control-plane-server; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane-server' first so the marker tells the truth." ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
288
test/cli.sh
288
test/cli.sh
|
|
@ -34,8 +34,8 @@ check "bare coolify shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" coolify
|
||||||
check "bootstrap: role required, exit 2" 2 "role required" "$ROOT/commands/bootstrap.sh"
|
check "bootstrap: role required, exit 2" 2 "role required" "$ROOT/commands/bootstrap.sh"
|
||||||
check "bootstrap: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap.sh" --help
|
check "bootstrap: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap.sh" --help
|
||||||
check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
||||||
check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope
|
check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload-server --nope
|
||||||
check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname
|
check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload-server --hostname
|
||||||
# --ts-tag is REMOVED, not demoted: the tag now comes from the pre-auth key and
|
# --ts-tag is REMOVED, not demoted: the tag now comes from the pre-auth key and
|
||||||
# rig verifies the GRANTED tag after join. The old runner-refuses-tag:server test
|
# rig verifies the GRANTED tag after join. The old runner-refuses-tag:server test
|
||||||
# asserted the request-time refusal THROUGH this flag; that policy now lives on
|
# asserted the request-time refusal THROUGH this flag; that policy now lives on
|
||||||
|
|
@ -44,25 +44,73 @@ check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bo
|
||||||
# at the key (exit 2, a usage error), rather than an "unknown flag" that would
|
# at the key (exit 2, a usage error), rather than an "unknown flag" that would
|
||||||
# leave an operator guessing where the tag went — value present or absent.
|
# leave an operator guessing where the tag went — value present or absent.
|
||||||
check "bootstrap: --ts-tag is removed (with value), exit 2" 2 "comes from the pre-auth key" \
|
check "bootstrap: --ts-tag is removed (with value), exit 2" 2 "comes from the pre-auth key" \
|
||||||
"$ROOT/commands/bootstrap.sh" runner --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 --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 old staging effective-tag refusal guarded the VM-HOST shape, which now
|
# The VM-HOST shape has a named role again (staging-server, #76), but it is
|
||||||
# rides the traits (custom/dev --class server) — the catch-all tag:server
|
# still not one of the two the control plane manages, so the catch-all
|
||||||
# refusal must still own that shape, so grep the general die instead.
|
# tag:server refusal must own it. Grep-pinned so a deleted guard cannot ship
|
||||||
|
# green (repo precedent: the login-path refusal below).
|
||||||
check "bootstrap: the catch-all tag:server refusal is present" 0 "" \
|
check "bootstrap: the catch-all tag:server refusal is present" 0 "" \
|
||||||
grep -q "Only control-plane and workload are managed by the control plane" "$ROOT/commands/bootstrap.sh"
|
grep -q "Only control-plane-server and workload-server are managed by the control plane" "$ROOT/commands/bootstrap.sh"
|
||||||
|
# ...and staging-server must NOT have slipped into the allow-list arm beside
|
||||||
|
# control-plane-server|workload-server. A new preset silently landing there
|
||||||
|
# would extend every server grant to a VM host, which is the exact shape the
|
||||||
|
# refusal exists to prevent — and nothing else in the suite would notice.
|
||||||
|
check "bootstrap: staging-server is not in the tag:server allow-list" 1 "" \
|
||||||
|
grep -qE '^ *control-plane-server\|workload-server\)[^#]*staging-server' "$ROOT/commands/bootstrap.sh"
|
||||||
|
|
||||||
|
# --- the role taxonomy (#76): -server names the family, and it was a hard cut -
|
||||||
|
# Every machine role carries the suffix; custom and workstation deliberately do
|
||||||
|
# not. Proven by reaching the ROOT CHECK, which is the last thing before the
|
||||||
|
# converge and therefore proof the name resolved to a preset.
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
for r in control-plane-server workload-server runner-server staging-server dev-server; do
|
||||||
|
check "bootstrap: role $r resolves" 1 "must run as root" \
|
||||||
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" "$r" --no-users
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
# THE HARD CUT. No aliases: the pre-#76 names are gone, and must fail as
|
||||||
|
# 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
|
||||||
|
# review — the taxonomy reads as complete while one old name still works.
|
||||||
|
# 'staging' is deliberately absent HERE: it is a TENANT name, and its own hard
|
||||||
|
# cut is asserted in the tenant section below, at both entrypoints.
|
||||||
|
for r in control-plane workload runner dev; do
|
||||||
|
check "bootstrap: the pre-#76 name '$r' is gone (hard cut)" 2 "unknown role" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" "$r"
|
||||||
|
done
|
||||||
|
# ...but the two roles that legitimately carry no suffix must NOT have been
|
||||||
|
# swept up in the rename. This is the inverse error and it fails silently: a
|
||||||
|
# workstation that stopped resolving would only surface at someone's laptop.
|
||||||
|
check "bootstrap: workstation keeps its bare name" 2 "unset TS_AUTHKEY" \
|
||||||
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workstation
|
||||||
|
check "bootstrap: custom keeps its bare name" 2 "--hostname" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" custom --class server --host no --join authkey
|
||||||
|
# NOTHING may still TELL an operator to run a pre-#76 role. The rename is a
|
||||||
|
# hard cut, so a next-step string, a usage line or a refusal that still recites
|
||||||
|
# a bare role name is a command that fails when someone copy-pastes it — and it
|
||||||
|
# fails later and further from the cause than a broken flag would, because it
|
||||||
|
# fails on a different box, minutes after this run reported success. The tenant
|
||||||
|
# script is the one that emits the staging guest's workload-join next step, so
|
||||||
|
# it is where this bites first (caught in review on #80, fixed here where the
|
||||||
|
# rename actually happens). Swept across every shipped script rather than
|
||||||
|
# asserted at the one known site: the next instance of this will be somewhere
|
||||||
|
# else, and a site-specific check would not see it.
|
||||||
|
check "roles: no shipped script tells an operator to run a pre-#76 role name" 1 "" \
|
||||||
|
grep -rnE "rig bootstrap (control-plane|workload|runner|dev)( |'|\"|$)" \
|
||||||
|
"$ROOT/bin/rig" "$ROOT/commands/"
|
||||||
# --- traits: roles are presets, every trait individually settable (#26) -----
|
# --- traits: roles are presets, every trait individually settable (#26) -----
|
||||||
check "bootstrap: unknown role still exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
check "bootstrap: unknown role still exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
||||||
check "bootstrap: bad --class value exits 2" 2 "human|server" "$ROOT/commands/bootstrap.sh" workload --class potato
|
check "bootstrap: bad --class value exits 2" 2 "human|server" "$ROOT/commands/bootstrap.sh" workload-server --class potato
|
||||||
check "bootstrap: bad --host value exits 2" 2 "yes|no" "$ROOT/commands/bootstrap.sh" workload --host maybe
|
check "bootstrap: bad --host value exits 2" 2 "yes|no" "$ROOT/commands/bootstrap.sh" workload-server --host maybe
|
||||||
check "bootstrap: bad --join value exits 2" 2 "authkey|login" "$ROOT/commands/bootstrap.sh" workload --join carrier-pigeon
|
check "bootstrap: bad --join value exits 2" 2 "authkey|login" "$ROOT/commands/bootstrap.sh" workload-server --join carrier-pigeon
|
||||||
check "bootstrap: custom without --hostname exits 2" 2 "--hostname" \
|
check "bootstrap: custom without --hostname exits 2" 2 "--hostname" \
|
||||||
"$ROOT/commands/bootstrap.sh" custom --class server --host no --join authkey
|
"$ROOT/commands/bootstrap.sh" custom --class server --host no --join authkey
|
||||||
check "bootstrap: custom without traits exits 2" 2 "--class" "$ROOT/commands/bootstrap.sh" custom --hostname box1
|
check "bootstrap: custom without traits exits 2" 2 "--class" "$ROOT/commands/bootstrap.sh" custom --hostname box1
|
||||||
|
|
@ -75,7 +123,7 @@ check "bootstrap: workstation + TS_AUTHKEY exits 2" 2 "unset TS_AUTHKEY" \
|
||||||
# join=authkey (TS_AUTHKEY fine → falls through to the root check), but
|
# join=authkey (TS_AUTHKEY fine → falls through to the root check), but
|
||||||
# --join login flips it into the TS_AUTHKEY refusal.
|
# --join login flips it into the TS_AUTHKEY refusal.
|
||||||
check "bootstrap: dev --join login + TS_AUTHKEY exits 2" 2 "unset TS_AUTHKEY" \
|
check "bootstrap: dev --join login + TS_AUTHKEY exits 2" 2 "unset TS_AUTHKEY" \
|
||||||
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" dev --join login
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" dev-server --join login
|
||||||
# The login-path inverted assertion needs a real tailnet; grep the refusal so a
|
# The login-path inverted assertion needs a real tailnet; grep the refusal so a
|
||||||
# deleted guard cannot ship green (repo precedent: staging/runner tag greps).
|
# deleted guard cannot ship green (repo precedent: staging/runner tag greps).
|
||||||
check "bootstrap: login-path tagged refusal is present" 0 "" \
|
check "bootstrap: login-path tagged refusal is present" 0 "" \
|
||||||
|
|
@ -186,33 +234,33 @@ printf '%s\n' 'maria ops ssh-ed25519 AAAA maria@mac' > "$BOOT_USER
|
||||||
# to skip it type the identical command — the error is the only place rig can
|
# to skip it type the identical command — the error is the only place rig can
|
||||||
# tell them apart.
|
# tell them apart.
|
||||||
check "bootstrap: omitting --users and --no-users exits 2" 2 "one of --users <path> or --no-users is required" \
|
check "bootstrap: omitting --users and --no-users exits 2" 2 "one of --users <path> or --no-users is required" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload
|
"$ROOT/commands/bootstrap.sh" workload-server
|
||||||
check "bootstrap: the requirement names --no-users as the way out" 2 "--no-users to leave it root-only" \
|
check "bootstrap: the requirement names --no-users as the way out" 2 "--no-users to leave it root-only" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname b
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname b
|
||||||
check "bootstrap: the requirement holds on class=server too" 2 "one of --users" \
|
check "bootstrap: the requirement holds on class=server too" 2 "one of --users" \
|
||||||
"$ROOT/commands/bootstrap.sh" control-plane --hostname cp
|
"$ROOT/commands/bootstrap.sh" control-plane-server --hostname cp
|
||||||
check "bootstrap: --users needs a value" 2 "needs a value" \
|
check "bootstrap: --users needs a value" 2 "needs a value" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users
|
"$ROOT/commands/bootstrap.sh" workload-server --users
|
||||||
# MUTUAL EXCLUSION, both orders: rig refuses to pick a winner rather than let a
|
# MUTUAL EXCLUSION, both orders: rig refuses to pick a winner rather than let a
|
||||||
# precedence rule decide who may enter the box. Both orders, because a
|
# precedence rule decide who may enter the box. Both orders, because a
|
||||||
# "last flag wins" implementation would pass one of them silently.
|
# "last flag wins" implementation would pass one of them silently.
|
||||||
check "bootstrap: --users with --no-users exits 2" 2 "contradictory" \
|
check "bootstrap: --users with --no-users exits 2" 2 "contradictory" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/ok" --no-users
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/ok" --no-users
|
||||||
check "bootstrap: --no-users with --users exits 2 (either order)" 2 "contradictory" \
|
check "bootstrap: --no-users with --users exits 2 (either order)" 2 "contradictory" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --no-users --users "$BOOT_USERS/ok"
|
"$ROOT/commands/bootstrap.sh" workload-server --no-users --users "$BOOT_USERS/ok"
|
||||||
# Pre-flight: an unreadable or invalid file dies at the top of the run, exit 2,
|
# Pre-flight: an unreadable or invalid file dies at the top of the run, exit 2,
|
||||||
# before the root check — the same contract every other flag here has.
|
# before the root check — the same contract every other flag here has.
|
||||||
check "bootstrap: an unreadable users file exits 2" 2 "cannot read users file" \
|
check "bootstrap: an unreadable users file exits 2" 2 "cannot read users file" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/nope"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/nope"
|
||||||
check "bootstrap: an invalid users file exits 2 with the parser's errors" 2 "invalid users file" \
|
check "bootstrap: an invalid users file exits 2 with the parser's errors" 2 "invalid users file" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/bad"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/bad"
|
||||||
check "bootstrap: the invalid-file refusal carries the parser's own line error" 2 "valid roles: admin rig box" \
|
check "bootstrap: the invalid-file refusal carries the parser's own line error" 2 "valid roles: admin rig box" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/bad"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/bad"
|
||||||
# '-' is apply's stdin convenience and cannot survive the trip through
|
# '-' is apply's stdin convenience and cannot survive the trip through
|
||||||
# bootstrap: stdin here is the pre-auth key prompt's. Refused, with the split
|
# bootstrap: stdin here is the pre-auth key prompt's. Refused, with the split
|
||||||
# ('--no-users' then apply by hand) named.
|
# ('--no-users' then apply by hand) named.
|
||||||
check "bootstrap: --users - is refused, naming the pre-auth key prompt" 2 "pre-auth key prompt" \
|
check "bootstrap: --users - is refused, naming the pre-auth key prompt" 2 "pre-auth key prompt" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users -
|
"$ROOT/commands/bootstrap.sh" workload-server --users -
|
||||||
# A file that parses to ZERO users (#57). Not a parse error — the parser is
|
# A file that parses to ZERO users (#57). Not a parse error — the parser is
|
||||||
# right to accept empty, comments-only and whitespace-only files — but it walks
|
# right to accept empty, comments-only and whitespace-only files — but it walks
|
||||||
# straight through #51's requirement: `--users ./empty` and `--no-users`
|
# straight through #51's requirement: `--users ./empty` and `--no-users`
|
||||||
|
|
@ -227,23 +275,23 @@ cat > "$BOOT_USERS/comments" <<'USERS'
|
||||||
USERS
|
USERS
|
||||||
printf ' \n\t\n\n' > "$BOOT_USERS/blank"
|
printf ' \n\t\n\n' > "$BOOT_USERS/blank"
|
||||||
check "bootstrap: an empty users file exits 2" 2 "names no users" \
|
check "bootstrap: an empty users file exits 2" 2 "names no users" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/empty"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/empty"
|
||||||
check "bootstrap: a comments-only users file exits 2" 2 "names no users" \
|
check "bootstrap: a comments-only users file exits 2" 2 "names no users" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/comments"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/comments"
|
||||||
check "bootstrap: a whitespace-only users file exits 2" 2 "names no users" \
|
check "bootstrap: a whitespace-only users file exits 2" 2 "names no users" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/blank"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/blank"
|
||||||
# The refusal must name --no-users, for the same reason the missing-flag one
|
# The refusal must name --no-users, for the same reason the missing-flag one
|
||||||
# does: the root-only box IS reachable, it just has to be said out loud. An
|
# does: the root-only box IS reachable, it just has to be said out loud. An
|
||||||
# error that only reported "no users" would leave the operator who genuinely
|
# error that only reported "no users" would leave the operator who genuinely
|
||||||
# wants root-only with no named way to ask for it.
|
# wants root-only with no named way to ask for it.
|
||||||
check "bootstrap: the zero-user refusal names --no-users as the way to say it" 2 "pass --no-users to leave this box root-only" \
|
check "bootstrap: the zero-user refusal names --no-users as the way to say it" 2 "pass --no-users to leave this box root-only" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/empty"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/empty"
|
||||||
# It must NOT over-refuse: a file that names even one operator passes pre-flight
|
# It must NOT over-refuse: a file that names even one operator passes pre-flight
|
||||||
# untouched. Reaching the root check (exit 1) is the proof — same idiom as the
|
# untouched. Reaching the root check (exit 1) is the proof — same idiom as the
|
||||||
# incus precondition's negative cases below.
|
# incus precondition's negative cases below.
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
check "bootstrap: a users file naming operators still passes pre-flight" 1 "must run as root" \
|
check "bootstrap: a users file naming operators still passes pre-flight" 1 "must run as root" \
|
||||||
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/ok"
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/ok"
|
||||||
fi
|
fi
|
||||||
# Scope guard (#57): the refusal is BOOTSTRAP's contract, not the parser's and
|
# Scope guard (#57): the refusal is BOOTSTRAP's contract, not the parser's and
|
||||||
# not apply's. A standalone `rig users apply` against an emptied file is a real
|
# not apply's. A standalone `rig users apply` against an emptied file is a real
|
||||||
|
|
@ -295,19 +343,19 @@ chmod +x "$INCUS_SHIM_NO/getent" "$INCUS_SHIM_YES/getent" \
|
||||||
"$BOXLESS_SHIM/getent" "$INCUS_SHIM_YES/box"
|
"$BOXLESS_SHIM/getent" "$INCUS_SHIM_YES/box"
|
||||||
check "bootstrap: host=yes + box role + no incus + skipped box install exits 2" 2 "group incus is absent" \
|
check "bootstrap: host=yes + box role + no incus + skipped box install exits 2" 2 "group incus is absent" \
|
||||||
env RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
env RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/box"
|
||||||
check "bootstrap: that refusal points at box setup-host, not at rig" 2 "rig never installs Incus" \
|
check "bootstrap: that refusal points at box setup-host, not at rig" 2 "rig never installs Incus" \
|
||||||
env RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
env RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/box"
|
||||||
# The group can be there while the CLI is not — #49's die owns that shape, and
|
# The group can be there while the CLI is not — #49's die owns that shape, and
|
||||||
# under the skip it is just as final and just as knowable now. PATH is built
|
# under the skip it is just as final and just as knowable now. PATH is built
|
||||||
# WITHOUT the real one so the absence is the test's, not the machine's.
|
# WITHOUT the real one so the absence is the test's, not the machine's.
|
||||||
check "bootstrap: host=yes + box role + incus group + no box CLI + skip exits 2" 2 "box CLI is not on PATH" \
|
check "bootstrap: host=yes + box role + incus group + no box CLI + skip exits 2" 2 "box CLI is not on PATH" \
|
||||||
env RIG_SKIP_BOX_INSTALL=1 PATH="$BOXLESS_SHIM:/usr/bin:/bin" \
|
env RIG_SKIP_BOX_INSTALL=1 PATH="$BOXLESS_SHIM:/usr/bin:/bin" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/box"
|
||||||
check "bootstrap: that refusal names the tier, not just the socket" 2 "the restricted tier is 'box grant'" \
|
check "bootstrap: that refusal names the tier, not just the socket" 2 "the restricted tier is 'box grant'" \
|
||||||
env RIG_SKIP_BOX_INSTALL=1 PATH="$BOXLESS_SHIM:/usr/bin:/bin" \
|
env RIG_SKIP_BOX_INSTALL=1 PATH="$BOXLESS_SHIM:/usr/bin:/bin" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/box"
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
# It must NOT fire in the three shapes that are not doomed. A users file with
|
# It must NOT fire in the three shapes that are not doomed. A users file with
|
||||||
# no box-role user converges fine on a host that never saw Incus (refusing it
|
# no box-role user converges fine on a host that never saw Incus (refusing it
|
||||||
|
|
@ -318,18 +366,18 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||||
# root check (exit 1) is the proof each passed the precondition.
|
# root check (exit 1) is the proof each passed the precondition.
|
||||||
check "bootstrap: no box-role user means no incus precondition" 1 "must run as root" \
|
check "bootstrap: no box-role user means no incus precondition" 1 "must run as root" \
|
||||||
env TS_AUTHKEY=x RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
env TS_AUTHKEY=x RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/ok"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/ok"
|
||||||
check "bootstrap: an existing incus group satisfies the precondition" 1 "must run as root" \
|
check "bootstrap: an existing incus group satisfies the precondition" 1 "must run as root" \
|
||||||
env TS_AUTHKEY=x RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_YES:$PATH" \
|
env TS_AUTHKEY=x RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_YES:$PATH" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/box"
|
||||||
check "bootstrap: without the skip, the box install is left to create the group" 1 "must run as root" \
|
check "bootstrap: without the skip, the box install is left to create the group" 1 "must run as root" \
|
||||||
env TS_AUTHKEY=x PATH="$INCUS_SHIM_NO:$PATH" \
|
env TS_AUTHKEY=x PATH="$INCUS_SHIM_NO:$PATH" \
|
||||||
"$ROOT/commands/bootstrap.sh" dev --hostname h --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" dev-server --hostname h --users "$BOOT_USERS/box"
|
||||||
# host=no is the other side of apply's host= rule — the box role is skipped
|
# host=no is the other side of apply's host= rule — the box role is skipped
|
||||||
# with a warning there, never refused, so bootstrap must not refuse it either.
|
# with a warning there, never refused, so bootstrap must not refuse it either.
|
||||||
check "bootstrap: host=no never gets the incus precondition" 1 "must run as root" \
|
check "bootstrap: host=no never gets the incus precondition" 1 "must run as root" \
|
||||||
env TS_AUTHKEY=x RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
env TS_AUTHKEY=x RIG_SKIP_BOX_INSTALL=1 PATH="$INCUS_SHIM_NO:$PATH" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/box"
|
"$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/box"
|
||||||
fi
|
fi
|
||||||
# rig does NOT resolve the open "should rig install box" question here: the
|
# rig does NOT resolve the open "should rig install box" question here: the
|
||||||
# precondition refuses, it never calls setup-host itself. A grep that finds
|
# precondition refuses, it never calls setup-host itself. A grep that finds
|
||||||
|
|
@ -368,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) --------------------------------------------
|
||||||
|
|
@ -399,18 +447,18 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||||
# required (#51), so reaching the root check at all proves it was accepted.
|
# required (#51), so reaching the root check at all proves it was accepted.
|
||||||
# --no-users here keeps these asserts about the ROOT CHECK; the --users path
|
# --no-users here keeps these asserts about the ROOT CHECK; the --users path
|
||||||
# gets its own root-check assert below, against a valid fixture.
|
# gets its own root-check assert below, against a valid fixture.
|
||||||
check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload --no-users
|
check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload-server --no-users
|
||||||
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 --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 --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 --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" \
|
||||||
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" custom --hostname b --class server --host no --join authkey --no-users
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" custom --hostname b --class server --host no --join authkey --no-users
|
||||||
|
|
@ -418,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
|
||||||
|
|
@ -426,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.
|
||||||
|
|
@ -440,48 +499,48 @@ 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.
|
||||||
TEN_FIX="$(mktemp -d)"
|
TEN_FIX="$(mktemp -d)"
|
||||||
printf 'role=dev 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 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
|
||||||
|
|
@ -497,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.
|
||||||
|
|
@ -524,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 "" \
|
||||||
|
|
@ -586,9 +645,15 @@ marker_warns() { # marker_warns <marker_path> <cmd...> — how many warnings fir
|
||||||
env RIG_ROLE_MARKER="$marker" "$@" 2>&1 | grep -c "not a control-plane box" || true
|
env RIG_ROLE_MARKER="$marker" "$@" 2>&1 | grep -c "not a control-plane box" || true
|
||||||
}
|
}
|
||||||
MARKER_FIX="$(mktemp -d)"
|
MARKER_FIX="$(mktemp -d)"
|
||||||
printf 'role=workload class=server host=no join=authkey\n' > "$MARKER_FIX/workload"
|
printf 'role=workload-server class=server host=no join=authkey\n' > "$MARKER_FIX/workload"
|
||||||
printf 'role=control-plane class=server host=no join=authkey\n' > "$MARKER_FIX/control-plane"
|
printf 'role=control-plane-server class=server host=no join=authkey\n' > "$MARKER_FIX/control-plane"
|
||||||
printf 'role=control-plane\n' > "$MARKER_FIX/bare-control-plane"
|
printf 'role=control-plane-server\n' > "$MARKER_FIX/bare-control-plane"
|
||||||
|
# A PRE-#76 marker, verbatim as a real box bootstrapped before the rename
|
||||||
|
# carries it. This is the one fixture that must keep its old spelling: the
|
||||||
|
# CHANGELOG promises such a box takes the warning branch and keeps working,
|
||||||
|
# and until this existed nothing asserted it — every other fixture here was
|
||||||
|
# renamed with the code, so the migration story was documented and untested.
|
||||||
|
printf 'role=control-plane class=server host=no join=authkey\n' > "$MARKER_FIX/pre-rename-cp"
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
check "coolify: warns on a non-control-plane marker" 0 "1" \
|
check "coolify: warns on a non-control-plane marker" 0 "1" \
|
||||||
marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
||||||
|
|
@ -596,10 +661,21 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||||
marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
||||||
# A bare marker line with no trailing traits must read the same as the full
|
# A bare marker line with no trailing traits must read the same as the full
|
||||||
# one — the guard must not couple to the marker's field formatting.
|
# one — the guard must not couple to the marker's field formatting.
|
||||||
check "coolify: a bare 'role=control-plane' line (no traits) stays silent" 0 "0" \
|
check "coolify: a bare 'role=control-plane-server' line (no traits) stays silent" 0 "0" \
|
||||||
marker_warns "$MARKER_FIX/bare-control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
marker_warns "$MARKER_FIX/bare-control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
||||||
check "coolify: absent marker stays silent (advisory, not a gate)" 0 "0" \
|
check "coolify: absent marker stays silent (advisory, not a gate)" 0 "0" \
|
||||||
marker_warns "$MARKER_FIX/absent" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
marker_warns "$MARKER_FIX/absent" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
||||||
|
# The migration story, pinned in both halves: a pre-#76 control plane WARNS
|
||||||
|
# (its marker no longer names a role that exists) but is never refused. Both
|
||||||
|
# halves matter — a rename that turned this into a refusal would break the
|
||||||
|
# exact boxes the CHANGELOG promises keep working, and it would do it on the
|
||||||
|
# command that installs the control plane.
|
||||||
|
check "coolify: a PRE-#76 'role=control-plane' marker warns (migration)" 0 "1" \
|
||||||
|
marker_warns "$MARKER_FIX/pre-rename-cp" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
||||||
|
check "coolify: ...and is still never refused" 1 "must run as root" \
|
||||||
|
env RIG_ROLE_MARKER="$MARKER_FIX/pre-rename-cp" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
||||||
|
check "coolify backup: a PRE-#76 'role=control-plane' marker warns (migration)" 0 "1" \
|
||||||
|
marker_warns "$MARKER_FIX/pre-rename-cp" "$ROOT/commands/coolify-backup-install.sh"
|
||||||
# The warning must stay a warning: the run proceeds past it and stops at the
|
# The warning must stay a warning: the run proceeds past it and stops at the
|
||||||
# root check (exit 1), never turned into a marker refusal.
|
# root check (exit 1), never turned into a marker refusal.
|
||||||
check "coolify: the marker warns but never refuses" 1 "must run as root" \
|
check "coolify: the marker warns but never refuses" 1 "must run as root" \
|
||||||
|
|
@ -992,11 +1068,11 @@ hostvm_gate() { # hostvm_gate <marker_path>
|
||||||
assert_marker_hosts_vms "$2"' _ "$ROOT" "$1"
|
assert_marker_hosts_vms "$2"' _ "$ROOT" "$1"
|
||||||
}
|
}
|
||||||
HOSTVM_FIX="$(mktemp -d)"
|
HOSTVM_FIX="$(mktemp -d)"
|
||||||
printf 'role=dev class=human host=yes join=authkey\n' > "$HOSTVM_FIX/yes"
|
printf 'role=dev-server class=human host=yes join=authkey\n' > "$HOSTVM_FIX/yes"
|
||||||
printf 'role=workload class=server host=no join=authkey\n' > "$HOSTVM_FIX/no"
|
printf 'role=workload-server class=server host=no join=authkey\n' > "$HOSTVM_FIX/no"
|
||||||
# A marker that predates the host= trait (or was hand-edited): present, but it
|
# A marker that predates the host= trait (or was hand-edited): present, but it
|
||||||
# names no host=. Distinct from an ABSENT marker and it must not read as yes.
|
# names no host=. Distinct from an ABSENT marker and it must not read as yes.
|
||||||
printf 'role=workload class=server join=authkey\n' > "$HOSTVM_FIX/traitless"
|
printf 'role=workload-server class=server join=authkey\n' > "$HOSTVM_FIX/traitless"
|
||||||
check "users apply: host=yes passes the box-role gate" \
|
check "users apply: host=yes passes the box-role gate" \
|
||||||
0 "" hostvm_gate "$HOSTVM_FIX/yes"
|
0 "" hostvm_gate "$HOSTVM_FIX/yes"
|
||||||
check "users apply: host=no fails the box-role gate" \
|
check "users apply: host=no fails the box-role gate" \
|
||||||
|
|
@ -1384,8 +1460,8 @@ marker_gate() { # marker_gate <marker_path>
|
||||||
assert_marker_human "$2"' _ "$ROOT" "$1"
|
assert_marker_human "$2"' _ "$ROOT" "$1"
|
||||||
}
|
}
|
||||||
MARKER_DIR="$(mktemp -d)"
|
MARKER_DIR="$(mktemp -d)"
|
||||||
printf 'role=workload class=server host=no join=authkey\n' > "$MARKER_DIR/server"
|
printf 'role=workload-server class=server host=no join=authkey\n' > "$MARKER_DIR/server"
|
||||||
printf 'role=dev class=human host=yes join=authkey\n' > "$MARKER_DIR/human"
|
printf 'role=dev-server class=human host=yes join=authkey\n' > "$MARKER_DIR/human"
|
||||||
check "users close-root: absent marker refuses, names bootstrap as the repair" \
|
check "users close-root: absent marker refuses, names bootstrap as the repair" \
|
||||||
1 "no /etc/rig/role marker" marker_gate "$MARKER_DIR/absent"
|
1 "no /etc/rig/role marker" marker_gate "$MARKER_DIR/absent"
|
||||||
check "users close-root: class=server refuses, names the control plane" \
|
check "users close-root: class=server refuses, names the control plane" \
|
||||||
|
|
@ -1408,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"
|
||||||
|
|
@ -1549,7 +1625,7 @@ check "install: ...and does not move the default" 0 "rig $VER" irig "$B1/rig" --
|
||||||
# host itself — /etc/rig/role. The deliberate decision: warn and proceed.
|
# host itself — /etc/rig/role. The deliberate decision: warn and proceed.
|
||||||
# Driven against a fixture marker; counting fires proves silence too.
|
# Driven against a fixture marker; counting fires proves silence too.
|
||||||
MARK="$WORK/role-marker"
|
MARK="$WORK/role-marker"
|
||||||
printf 'role=workload class=server host=no join=authkey\n' > "$MARK"
|
printf 'role=workload-server class=server host=no join=authkey\n' > "$MARK"
|
||||||
H2="$WORK/h2"; B2="$WORK/b2"
|
H2="$WORK/h2"; B2="$WORK/b2"
|
||||||
check "flip gate: baseline install" 0 "done" inst "$H2" "$B2"
|
check "flip gate: baseline install" 0 "done" inst "$H2" "$B2"
|
||||||
check "flip gate: an upgrade on a bootstrapped host WARNS" 0 "this host is bootstrapped" \
|
check "flip gate: an upgrade on a bootstrapped host WARNS" 0 "this host is bootstrapped" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue