Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,22 @@ jobs:
SQUAREBOX_WORKSPACE=$(mktemp -d)
export SQUAREBOX_WORKSPACE
cleanup() { docker compose down -v --remove-orphans >/dev/null 2>&1 || true; }
wait_for_compose_workspace() {
local _
for _ in {1..30}; do
if docker compose exec -T -u "$PUID:$PGID" squarebox sh -c \
'test "$(stat -c %u /proc/1)" = "$1" && test -w /workspace' _ "$PUID"; then
return 0
fi
sleep 1
done
docker compose logs squarebox >&2
return 1
}
trap cleanup EXIT
docker compose config >/dev/null
docker compose up -d
wait_for_compose_workspace
original_box=$(docker compose ps -q squarebox)
test -n "$original_box"
test "$(docker inspect --format '{{.Config.Image}}' "$original_box")" = "$SQUAREBOX_IMAGE_REF"
Expand All @@ -432,6 +445,7 @@ jobs:
test -f "$SQUAREBOX_WORKSPACE/from-compose"

docker compose up -d --force-recreate --no-deps squarebox
wait_for_compose_workspace
replacement_box=$(docker compose ps -q squarebox)
test -n "$replacement_box"
test "$replacement_box" != "$original_box"
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## v1.1.0 — unreleased
## v1.1.0 — 2026-07-16

### Added

Expand Down Expand Up @@ -57,6 +57,9 @@
state, and image-tier updates advertised without Candidate-authorized bytes.
- Fish derived aliases becoming stale after section-only setup reruns.
- CI reports claiming behavior that was not executed.
- Release-candidate failures caused by a Compose UID-remap startup race,
cross-UID Evidence file permissions, LazyGit's lowercase Linux asset name,
and Gum release binaries built with a vulnerable Go standard library.

### Removed

Expand Down
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ edit, fresh, Helix (`hx`), and Neovim/LazyVim.

## Tool registry and verified installation

`scripts/lib/tools.yaml` owns artifact metadata, Tool tier, verification policy,
and Docker ARG mapping. Observed version probes remain adapter functions in
`scripts/squarebox-update.sh`. The awk reader avoids requiring yq to install yq.
`scripts/lib/tools.yaml` owns release-asset metadata, Tool tier, verification
policy, and Docker ARG mapping. Observed version probes remain adapter functions
in `scripts/squarebox-update.sh`. The awk reader avoids requiring yq to install
yq. Gum is the sole OCI-sourced exception: Dockerfile pins its exact upstream
multi-architecture digest and asserts the reviewed version/commit, while
`tests/test-gum-image-policy.sh` rejects mutable or unreviewed references.

`scripts/lib/tool-lib.sh` is the deep artifact-install module. Its interface
must remain fail-closed across metadata validation, download, verification,
Expand Down
12 changes: 9 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,21 @@ Every registry entry declares its Tool tier and verification policy.

- Image-tier direct downloads use pinned versions and `sha256`; missing or
mismatched checksums must fail closed.
- Image-tier OCI inputs use an exact multi-architecture digest and assert their
reviewed binary identity during the image build. Gum currently uses this path
because its latest stable release tar embeds a vulnerable Go standard library;
`tests/test-gum-image-policy.sh` locks the reviewed exception.
- Managed-home GitHub release downloads use the SHA-256 digest on the exact
release asset. Missing, malformed, duplicate, or mismatched digests fail
before installation mutates the destination.
- Box-tier packages must be reconciled after Box replacement; a saved Selection
is not proof the package exists.

Use `scripts/update-versions.sh` for image-tier refreshes. It generates and
validates checksums and Dockerfile pins transactionally. Review upstream release
notes and the complete diff before accepting a refresh.
Use `scripts/update-versions.sh` for release-asset image-tier refreshes. It
generates and validates checksums and Dockerfile pins transactionally. Review
upstream release notes and the complete diff before accepting a refresh. Review
OCI-sourced exceptions by digest, per-architecture binary identity, and scan
result instead of adding a mutable tag to the release-asset registry.

Adding a tool requires updating every behavior represented by registry metadata
or adding metadata so derived inventories remain consistent. Add deterministic
Expand Down
18 changes: 14 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Gum v0.17.0's release binaries embed vulnerable Go 1.25.1. Upstream has not
# published a newer stable release, so copy the fixed development binary from
# its exact multi-architecture OCI manifest. The build below asserts the
# expected v0.17.1-devel commit identity before the binary enters the Box.
ARG GUM_IMAGE=ghcr.io/charmbracelet/gum@sha256:426c1e40739f11083e06d58ffaac910289eeace709a3d9bddcb8d4566140c93c
FROM ${GUM_IMAGE} AS gum-source

FROM ubuntu:24.04

# 1. Base Packages & Rust CLI Tools (consolidated, slimmed)
Expand Down Expand Up @@ -59,21 +66,23 @@ ARG YQ_VERSION=4.53.3
ARG XH_VERSION=0.26.1
ARG STARSHIP_VERSION=1.26.0
ARG GLOW_VERSION=2.1.2
ARG GUM_VERSION=0.17.0
ARG JUST_VERSION=1.56.0
ARG DIFFTASTIC_VERSION=0.69.0
ARG MISE_VERSION=2026.7.5
ARG GUM_EXPECTED_VERSION=v0.17.1-devel
ARG GUM_EXPECTED_COMMIT=591ded2

# Validate version ARGs are non-empty
RUN test -n "$DELTA_VERSION" || { echo "Error: DELTA_VERSION is empty" >&2; exit 1; } \
&& test -n "$YQ_VERSION" || { echo "Error: YQ_VERSION is empty" >&2; exit 1; } \
&& test -n "$XH_VERSION" || { echo "Error: XH_VERSION is empty" >&2; exit 1; } \
&& test -n "$STARSHIP_VERSION" || { echo "Error: STARSHIP_VERSION is empty" >&2; exit 1; } \
&& test -n "$GLOW_VERSION" || { echo "Error: GLOW_VERSION is empty" >&2; exit 1; } \
&& test -n "$GUM_VERSION" || { echo "Error: GUM_VERSION is empty" >&2; exit 1; } \
&& test -n "$JUST_VERSION" || { echo "Error: JUST_VERSION is empty" >&2; exit 1; } \
&& test -n "$DIFFTASTIC_VERSION" || { echo "Error: DIFFTASTIC_VERSION is empty" >&2; exit 1; } \
&& test -n "$MISE_VERSION" || { echo "Error: MISE_VERSION is empty" >&2; exit 1; }
&& test -n "$MISE_VERSION" || { echo "Error: MISE_VERSION is empty" >&2; exit 1; } \
&& test -n "$GUM_EXPECTED_VERSION" || { echo "Error: GUM_EXPECTED_VERSION is empty" >&2; exit 1; } \
&& test -n "$GUM_EXPECTED_COMMIT" || { echo "Error: GUM_EXPECTED_COMMIT is empty" >&2; exit 1; }

# Checksum verification infrastructure
COPY checksums.txt /tmp/checksums.txt
Expand Down Expand Up @@ -117,7 +126,8 @@ RUN . /tmp/sb-init.sh && sb_install delta "$DELTA_VERSION"
RUN . /tmp/sb-init.sh && sb_install yq "$YQ_VERSION"
RUN . /tmp/sb-init.sh && sb_install xh "$XH_VERSION"
RUN . /tmp/sb-init.sh && sb_install glow "$GLOW_VERSION"
RUN . /tmp/sb-init.sh && sb_install gum "$GUM_VERSION"
COPY --from=gum-source /usr/local/bin/gum /usr/local/bin/gum
RUN test "$(gum --version)" = "gum version ${GUM_EXPECTED_VERSION} (${GUM_EXPECTED_COMMIT})"
RUN . /tmp/sb-init.sh && sb_install starship "$STARSHIP_VERSION"
RUN . /tmp/sb-init.sh && sb_install just "$JUST_VERSION"
RUN . /tmp/sb-init.sh && sb_install difftastic "$DIFFTASTIC_VERSION"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Flags: `--build` (build from source), `--edge` (latest `main`), `--adopt`
|----------|---------|---------|
| `SQUAREBOX_DIR` | `~/squarebox` | Install location (repo + workspace). Point at durable storage on hosts where `$HOME` is volatile — e.g. Unraid `/mnt/user/appdata/squarebox`. |
| `SQUAREBOX_WORKSPACE` | `$SQUAREBOX_DIR/workspace` | Host path mounted as `/workspace`. |
| `SQUAREBOX_TAG` | latest published stable | Published Release to install (for example `v1.1.0-rc5`). Tags use `vMAJOR.MINOR.PATCH[-prerelease]`; build metadata is not published. |
| `SQUAREBOX_TAG` | latest published stable | Published Release to install (for example `v1.1.0`). Tags use `vMAJOR.MINOR.PATCH[-prerelease]`; build metadata is not published. |
| `SQUAREBOX_IMAGE` | value from `release.json` | Optional image-repository override for development/testing. |
| `SQUAREBOX_BUILD` | `0` | `1` is equivalent to `--build`. |
| `PUID` / `PGID` | invoking Linux user | Host uid/gid that should own bind-mounted files. Docker/rootful hosts may override these (Unraid/NAS: `99` / `100`); rootless Podman requires the invoking host identity. |
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- Complete the real-host matrix in `uat-checklist.md`, especially Windows,
macOS, Fedora rootless Podman/SELinux, Dev Containers/Codespaces, and
physical arm64 (tracked by GitHub issues #99–#105).
- Cut `v1.1.0-rc5` as the automatic publication rehearsal.
- `v1.1.0-rc5` completed the automatic publication rehearsal.
- Keep GitHub immutable Releases enabled, keep the no-bypass `v*` update and
deletion ruleset active, and verify the stable/prerelease environment
protections before creating a final tag.
Expand Down
3 changes: 0 additions & 3 deletions checksums.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ c411f07a0b204ca07858a67473d0f5c77e332226430d4d84d1d2afd351a425f2 xh-v0.26.1-x86
# Glow 2.1.2
6063d4f2af8a82a5f4bba0831e165de9381660aa8b41df4816d0106a265b07d5 glow_2.1.2_Linux_x86_64.tar.gz
cf63abebcb50b72909db965d78290e7cecbf17a900e84705dc84addbb6952099 glow_2.1.2_Linux_arm64.tar.gz
# Gum 0.17.0
69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb gum_0.17.0_Linux_x86_64.tar.gz
b0b9ed95cbf7c8b7073f17b9591811f5c001e33c7cfd066ca83ce8a07c576f9c gum_0.17.0_Linux_arm64.tar.gz
# Starship 1.26.0
b7c232b0e8249d8e55a40beb79c5c43a7d370f3f9408bd215deb0170daeaadf3 starship-x86_64-unknown-linux-musl.tar.gz
dc30189378d2f2e287384e8a692d3f95ad1df64cf0e8c36aa9201516028aed6b starship-aarch64-unknown-linux-musl.tar.gz
Expand Down
2 changes: 1 addition & 1 deletion demo/setup-demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ echo
printf '\e[1;38;5;208m'
toilet -f smblock --metal "squarebox"
printf '\e[0m'
printf '\e[38;5;208m 🟧📦 You'\''re in the box.\e[0m\e[38;5;245m (v1.1.0-rc5)\e[0m\n'
printf '\e[38;5;208m 🟧📦 You'\''re in the box.\e[0m\e[38;5;245m (v1.1.0)\e[0m\n'
printf '\e[38;5;172m %s\e[0m\n' "$(date '+%A, %B %d %Y %H:%M')"
printf '\e[38;5;245m Node 22.x ◆ Python 3.x\e[0m\n'
printf '\e[38;5;208m ✦ sqrbx-help\e[0m\e[38;5;245m — commands and keyboard shortcuts\e[0m\n'
Expand Down
19 changes: 10 additions & 9 deletions docs/releases/v1.1.0.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Squarebox v1.1.0 migration guide

v1.1 changes the lifecycle model as well as adding tools. Test an upgrade with
`v1.1.0-rc5` before promoting the Release used for important work.
v1.1 changes the lifecycle model as well as adding tools. Test the published
`v1.1.0` Release against a non-production installation before upgrading
important work.

## Before upgrading

Expand Down Expand Up @@ -68,15 +69,15 @@ Native PowerShell exposes `%USERPROFILE%\.ssh` read-only; it does not forward
| Image-tier binaries | Candidate image | Replaced by the published digest |
| Manually installed, unselected APT packages | Disposable Box filesystem | Lost |

## Release-candidate installation
## Version-pinned installation

Use the Candidate's published tag, which resolves matching source and image
identity from its Release assets:
Use the Release's published tag, which resolves matching source and image
identity from its assets:

```bash
(
set -euo pipefail
tag=v1.1.0-rc5
tag=v1.1.0
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
curl -fsSLo "$tmp/install.sh" \
Expand All @@ -94,9 +95,9 @@ identity from its Release assets:
)
```

This deliberately downloads the Candidate's installer instead of invoking the
old adapter already present in a v1.0 checkout. Do not combine an RC image with
a stable checkout. Verify `release.json`, the MOTD version, and
This deliberately downloads the Release's installer instead of invoking the
old adapter already present in a v1.0 checkout. Do not combine an image with a
checkout from a different Release. Verify `release.json`, the MOTD version, and
`/usr/local/lib/squarebox/VERSION` agree after installation.

## Rollback
Expand Down
4 changes: 4 additions & 0 deletions scripts/e2e-evidence.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ printf '%s\t%s\t%s\t%s\n' \
"$status" \
"$(clean_field "$description")" \
"$(clean_field "$detail")" > "$tmp"
# Evidence crosses the container/runner UID boundary before artifact upload.
# mktemp honours the container's restrictive umask, so normalize the completed
# file while it is still staged and preserve the atomic rename below.
chmod 0644 "$tmp"
mv -f "$tmp" "$evidence_dir/${safe_id}.evidence"
trap - EXIT
16 changes: 4 additions & 12 deletions scripts/lib/tools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,9 @@ tools:
verification: sha256
docker_arg: GLOW_VERSION

gum:
repo: charmbracelet/gum
version_prefix: v
artifact: gum_{version}_Linux_{larch}.tar.gz
method: tar.gz
binaries: gum
find_binary: true
dest: system
group: dockerfile
verification: sha256
docker_arg: GUM_VERSION
# Gum is copied from an immutable, multi-architecture upstream OCI manifest
# in Dockerfile. It is intentionally outside the release-tar updater because
# upstream's latest stable tar embeds a vulnerable Go standard library.

starship:
repo: starship/starship
Expand Down Expand Up @@ -129,7 +121,7 @@ tools:
lazygit:
repo: jesseduffield/lazygit
version_prefix: v
artifact: lazygit_{version}_Linux_{larch}.tar.gz
artifact: lazygit_{version}_linux_{larch}.tar.gz
method: tar.gz
binaries: lazygit
tar_extract: lazygit
Expand Down
1 change: 0 additions & 1 deletion scripts/squarebox-update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ yazi_current() { yazi --version 2>/dev/null | _extract_first_version; }
starship_current() { starship --version 2>/dev/null | _extract_first_version; }
ghdash_current() { gh-dash --version 2>/dev/null | _extract_first_version; }
glow_current() { glow --version 2>/dev/null | _extract_first_version; }
gum_current() { gum --version 2>/dev/null | _extract_first_version; }
micro_current() { micro --version 2>/dev/null | _extract_first_version; }
fresh_current() { fresh --version 2>/dev/null | _extract_first_version; }
edit_current() { edit --version 2>/dev/null | _extract_first_version; }
Expand Down
8 changes: 7 additions & 1 deletion tests/test-e2e-evidence.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ report() {
"$ROOT/scripts/e2e-report.sh" "$TMP/evidence"
}

record pass alpha "Alpha requirement"
(umask 077; record pass alpha "Alpha requirement")
test "$(stat -c %a "$TMP/evidence/alpha.evidence")" = 644 || {
echo "FAIL: container-created Evidence is not readable by the artifact runner" >&2
exit 1
}

if report > "$TMP/missing.md"; then
echo "FAIL: report accepted missing required evidence" >&2
Expand Down Expand Up @@ -107,6 +111,8 @@ grep -Fq 'interactive prompt detected' "$RUNNER"
# Compose Evidence must observe actual replacement and both persistent stores.
grep -Fq 'docker compose up -d --force-recreate --no-deps squarebox' "$WORKFLOW"
grep -Fq 'test "$replacement_box" != "$original_box"' "$WORKFLOW"
test "$(grep -Fc 'wait_for_compose_workspace' "$WORKFLOW")" -eq 3
grep -Fq 'test "$(stat -c %u /proc/1)" = "$1" && test -w /workspace' "$WORKFLOW"
grep -Fq '~/.squarebox-compose-e2e' "$WORKFLOW"
grep -Fq '$SQUAREBOX_WORKSPACE/from-compose' "$WORKFLOW"
grep -Fq -- '--cap-drop=ALL --cap-add=CHOWN --cap-add=DAC_OVERRIDE' "$WORKFLOW"
Expand Down
39 changes: 39 additions & 0 deletions tests/test-gum-image-policy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
DOCKERFILE="$ROOT/Dockerfile"
EXPECTED_IMAGE='ghcr.io/charmbracelet/gum@sha256:426c1e40739f11083e06d58ffaac910289eeace709a3d9bddcb8d4566140c93c'

fail() {
echo "FAIL: $*" >&2
exit 1
}

mapfile -t gum_images < <(sed -n 's/^ARG GUM_IMAGE=//p' "$DOCKERFILE")
[ "${#gum_images[@]}" -eq 1 ] || fail "Dockerfile must declare exactly one Gum OCI image"
[ "${gum_images[0]}" = "$EXPECTED_IMAGE" ] \
|| fail "Gum OCI image must remain pinned to the reviewed multi-architecture digest"
[[ "${gum_images[0]}" =~ ^ghcr\.io/charmbracelet/gum@sha256:[0-9a-f]{64}$ ]] \
|| fail "Gum OCI image must not use a mutable tag"

grep -Fqx 'FROM ${GUM_IMAGE} AS gum-source' "$DOCKERFILE" \
|| fail "Dockerfile does not consume the pinned Gum manifest"
grep -Fqx 'COPY --from=gum-source /usr/local/bin/gum /usr/local/bin/gum' "$DOCKERFILE" \
|| fail "Gum binary is not copied from the pinned upstream image"
grep -Fqx 'ARG GUM_EXPECTED_VERSION=v0.17.1-devel' "$DOCKERFILE" \
|| fail "expected Gum development version is not explicit"
grep -Fqx 'ARG GUM_EXPECTED_COMMIT=591ded2' "$DOCKERFILE" \
|| fail "expected Gum source commit is not explicit"
grep -Fq 'gum version ${GUM_EXPECTED_VERSION} (${GUM_EXPECTED_COMMIT})' "$DOCKERFILE" \
|| fail "image build does not assert Gum version and commit identity"

if grep -Eq '^ARG GUM_VERSION=|sb_install gum' "$DOCKERFILE"; then
fail "obsolete Gum release-tar install path remains in Dockerfile"
fi
if grep -Eq '^ gum:|gum_[^ ]*Linux_(x86_64|arm64)[.]tar[.]gz' \
"$ROOT/scripts/lib/tools.yaml" "$ROOT/checksums.txt"; then
fail "obsolete Gum release-tar registry or checksum metadata remains"
fi

echo "PASS: Gum uses the reviewed immutable OCI manifest and asserted build identity"
38 changes: 38 additions & 0 deletions tests/test-tool-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,44 @@ else
not_ok "exact release tag and exact asset name select the authoritative digest"
fi

LAZYGIT_CASE="$TMP/lazygit-linux-asset"
mkdir -p "$LAZYGIT_CASE/home" "$LAZYGIT_CASE/cache" "$LAZYGIT_CASE/archive"
printf '#!/usr/bin/env bash\necho "lazygit version 0.63.0"\n' > "$LAZYGIT_CASE/archive/lazygit"
chmod +x "$LAZYGIT_CASE/archive/lazygit"
tar -czf "$LAZYGIT_CASE/lazygit_0.63.0_linux_x86_64.tar.gz" -C "$LAZYGIT_CASE/archive" lazygit
LAZYGIT_HASH=$(sha256sum "$LAZYGIT_CASE/lazygit_0.63.0_linux_x86_64.tar.gz" | awk '{print $1}')
if (
export HOME="$LAZYGIT_CASE/home" SB_TOOLS_YAML="$REPO_ROOT/scripts/lib/tools.yaml" SB_DPKG_ARCH=amd64
export SB_GITHUB_API_BASE=https://api.test SB_GH_METADATA_CACHE_DIR="$LAZYGIT_CASE/cache"
export LAZYGIT_CASE LAZYGIT_HASH
source "$REPO_ROOT/scripts/lib/tool-lib.sh"
curl() {
local output="" url=""
while [ "$#" -gt 0 ]; do
case "$1" in
-w) shift 2 ;;
-o) output=$2; shift 2 ;;
-*o) output=$2; shift 2 ;;
-*) shift ;;
*) url=$1; shift ;;
esac
done
printf '%s\n' "$url" >> "$LAZYGIT_CASE/curl.log"
if [[ "$url" == https://api.test/* ]]; then
printf '{"tag_name":"v0.63.0","assets":[{"name":"lazygit_0.63.0_linux_x86_64.tar.gz","digest":"sha256:%s"}]}\n200\n' "$LAZYGIT_HASH"
else
cp "$LAZYGIT_CASE/lazygit_0.63.0_linux_x86_64.tar.gz" "$output"
fi
}
sb_install lazygit 0.63.0 >/dev/null \
&& [ "$("$HOME/.local/bin/lazygit")" = 'lazygit version 0.63.0' ] \
&& grep -q '/releases/download/v0.63.0/lazygit_0.63.0_linux_x86_64.tar.gz$' "$LAZYGIT_CASE/curl.log"
); then
ok "production LazyGit registry installs the lowercase Linux release asset"
else
not_ok "production LazyGit registry installs the lowercase Linux release asset"
fi

run_github_digest_case duplicate-asset 1.0.0 \
'{"tag_name":"v1.0.0","assets":[{"name":"sample-1.0.0-amd64","digest":"sha256:'"$PAYLOAD_HASH"'"},{"name":"sample-1.0.0-amd64","digest":"sha256:'"$PAYLOAD_HASH"'"}]}'
if [ "$(<"$TMP/duplicate-asset/rc")" -ne 0 ] \
Expand Down