diff --git a/CHANGELOG.md b/CHANGELOG.md index 680c81c..ed2eded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,30 @@ which records not just what changed but what each drill run proved. ## Unreleased +### Added + +- **Global / root install** (#71) — run as root, box installs *once* to + `/opt/box` (world-readable) with the `box` symlink on `/usr/local/bin`, so + every operator on a shared host runs the same tree. Per-user installs are + unchanged (`$HOME/.local`); `BOX_HOME`/`BOX_BIN` still override. A per-user + tree under `/root` is `0700` and unreadable to everyone else — the whole fleet + got `command not found` — so the root branch lands in a system location and + `chmod -R a+rX`'s it (read for files, +search on dirs), guarded on root. This + unblocks "rig installs box" (rig#24's `box` role). +- **CI + a test suite** — `.github/workflows/ci.yml` (a `check` job: globstar + `shellcheck -x` over `bin/* **/*.sh`, then `bash test/cli.sh`) and `test/cli.sh`, + dependency-free and runnable by a non-root user with no Incus. It exercises the + `install.sh` DEST/BINDIR branch functionally (both tiers + `BOX_HOME`/`BOX_BIN` + overrides), the CLI contract, and grep-guards the daemon-gated invariants and + tmux in every template — the box was the repo with "no tests and no CI". + ### Fixed +- **`box tmux` works on every template** (#65) — `box tmux` runs + `tmux new-session` *inside* the box, but the templates did not install tmux, so + it failed with `tmux: command not found`. `tmux` is now in each template's + cloud-init package list (`blank`/`claude`/`codex`/`grok`). + - **`box setup-host` finishes in one run** (#63). When it had to add you to `incus-admin` it stopped there and told you to re-login and re-run — an `exit 0` that reported success having built none of the stack: no `boxnet`, diff --git a/README.md b/README.md index de9e3d1..8317fe0 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,25 @@ A version-aware upgrade that migrates boxes instead of asking you to is (CI, images), `BOX_YES=1` answers every prompt yes and `BOX_SKIP_SETUP_HOST=1` declines the host-setup step. +### Global vs per-user install + +Where box lands depends on **who runs the installer**, because on a shared host +box's tree is *executed by other users* — so it cannot hide in one user's home: + +- **As root → global.** The tree goes to `/opt/box` (world-readable) and the + `box` symlink to `/usr/local/bin` (already on every login `PATH`). One + install, every operator on the host runs the same `box`. This is the fleet + path: [rig](https://github.com/heavy-duty/rig)'s `box` role + ([rig#24](https://github.com/heavy-duty/rig/issues/24)) installs box once at + host bootstrap ([#71](https://github.com/heavy-duty/box/issues/71)). +- **As a normal user → per-user.** The tree goes to `~/.local/share/box` and + the symlink to `~/.local/bin` — the solo path, unchanged. Nobody else needs + to run your box. + +`BOX_HOME` / `BOX_BIN` override the destination on either path. A per-user +install under `/root` would be `0700` and unreadable to everyone else — which +is exactly the bug the root branch fixes. + ## One-time host setup (Ubuntu 24.04 / Debian 13) The installer already does this. Run it directly to set up a host you diff --git a/install.sh b/install.sh index 4eed235..a894f7d 100755 --- a/install.sh +++ b/install.sh @@ -10,8 +10,20 @@ set -euo pipefail REPO="${BOX_REPO:-heavy-duty/box}" REF="${BOX_REF:-main}" -DEST="${BOX_HOME:-$HOME/.local/share/box}" -BINDIR="${BOX_BIN:-$HOME/.local/bin}" +# Root installs GLOBALLY, non-root installs per-user. box's install tree is +# EXECUTED by other users (the multi-user host path: rig installs box once, every +# incus-group operator runs it) — unlike rig, which is root-only and can hide in +# /root. So a root install must land in a system location, not $HOME: /root is +# 0700, so a $HOME/.local tree there is unreadable to everyone else and the whole +# fleet gets 'command not found' (#71). /opt/box is the world-readable system +# tree; /usr/local/bin is already on every login PATH. BOX_HOME/BOX_BIN still win. +if [ "$(id -u)" -eq 0 ]; then + DEST="${BOX_HOME:-/opt/box}" + BINDIR="${BOX_BIN:-/usr/local/bin}" +else + DEST="${BOX_HOME:-$HOME/.local/share/box}" + BINDIR="${BOX_BIN:-$HOME/.local/bin}" +fi log() { printf 'box-install: %s\n' "$*"; } warn() { printf 'box-install: WARNING: %s\n' "$*" >&2; } @@ -96,6 +108,16 @@ mv "$EXTRACTED" "$DEST" chmod +x "$DEST/bin/box" +# A global (root) install is run by OTHER users, but mv preserves the tarball's +# root:root ownership and GitHub's archives carry no world bits on some paths — so +# without this, a non-root caller cannot even traverse into $DEST to reach bin/box. +# Root owns the tree, nobody else writes it, everybody reads it. a+rX: read on +# files, +search (x) on directories only. Guarded on root so the per-user install +# stays byte-identical to before. +if [ "$(id -u)" -eq 0 ]; then + chmod -R a+rX "$DEST" +fi + # --- put box on PATH ------------------------------------------------------- mkdir -p "$BINDIR" ln -sf "$DEST/bin/box" "$BINDIR/box" diff --git a/templates/blank/user-data.yaml b/templates/blank/user-data.yaml index efe8ddf..e482c90 100644 --- a/templates/blank/user-data.yaml +++ b/templates/blank/user-data.yaml @@ -7,3 +7,9 @@ users: sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: true package_update: false +# The one package a blank box still needs: 'box tmux' runs 'tmux new-session' +# INSIDE the box, and a bare Debian image ships no tmux — without this it fails +# with "tmux: command not found" (#65). cloud-init refreshes the apt lists on its +# own whenever 'packages' is non-empty, so package_update stays false here. +packages: + - tmux