From 7df2bf5675d765859c68d635f8c9dddf99240b15 Mon Sep 17 00:00:00 2001 From: theBlanketBaron Date: Thu, 5 Nov 2026 16:34:24 -0500 Subject: [PATCH 1/4] Check the installer's download against SHA256SUMS; strip release binaries (MUS-95) install-codex.sh now fetches SHA256SUMS from the same release and checks the shim with sha256sum, or shasum -a 256 on macOS, before it runs it. A mismatch, a missing entry, a missing SHA256SUMS or no tool to check with all stop the install. scripts/test-install-codex.sh runs it against a local fixture through a fake curl, so nothing is downloaded and Codex isn't touched: a matching shim installs, a tampered one is refused and never runs, and the shasum-only path is covered. 9 checks pass; against the installer from before, 7 fail. CI runs it. The release workflow builds with CARGO_PROFILE_RELEASE_STRIP=symbols, so shipped binaries carry no symbols or debuginfo; local release builds are unchanged. Its pins, no-cache rule and smoke test stay. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++ .github/workflows/release.yml | 4 ++ docs/roadmap.md | 9 +++ scripts/install-codex.sh | 40 ++++++++++++- scripts/test-install-codex.sh | 105 ++++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100755 scripts/test-install-codex.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2474e0b1..41b42923 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,10 @@ jobs: run: cargo build --workspace && ./scripts/test-gate-e2e.sh - name: Links between daemons end-to-end (MUS-66) run: ./scripts/test-links-e2e.sh + # MUS-95: install-codex.sh runs the shim only when it matches the release's SHA256SUMS. + # A fake curl serves a local fixture, so nothing is downloaded. + - name: Installer checks its download (MUS-95) + run: sh scripts/test-install-codex.sh # MUS-58: the sweeper starts only once myko has replayed Postgres, so an # ad-hoc room membership survives a restart. Uses the job's Postgres # service, in a table of its own. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 490efc7d..b1980a49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,10 @@ jobs: toolchain: stable targets: ${{ matrix.target }} - name: Build + env: + # MUS-95: shipped binaries carry no symbol tables or debuginfo. Here rather than in + # Cargo.toml, so local release builds keep theirs. + CARGO_PROFILE_RELEASE_STRIP: symbols run: cargo build --release --locked --target ${{ matrix.target }} -p muster-shim -p muster-daemon - name: Name the binaries for the release run: | diff --git a/docs/roadmap.md b/docs/roadmap.md index 4317ff07..d0e06bbd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -403,6 +403,15 @@ Merged and needing a live run: MUS-36 and MUS-37. Linux x86_64 under the names install-codex.sh asks for, with one SHA256SUMS. workflow_dispatch is a dry run that only uploads artifacts; a `v*` tag also makes a draft release, which stays unpublished until Josh publishes it. Left: a tag and a published release, on Josh's word. +- [x] **MUS-95: The installer checks its download, and release binaries are stripped.** From + MUS-14's review. Done when: `scripts/install-codex.sh` fetches SHA256SUMS from the same release, + checks the shim against it with sha256sum (or `shasum -a 256` on macOS), and refuses to run it on + a mismatch, a missing entry, a missing SHA256SUMS or no tool to check with; a fixture test + shows a tampered shim refused; and the release workflow strips the binaries, keeping its smoke + test, SHA-pinned actions and no cache. `scripts/test-install-codex.sh` runs in CI with a fake + curl: 9 checks pass, and 7 of them fail against the installer from before. This guards against a + corrupt or swapped download, not a tampered release, since both files come from the same + release; signing waits for Josh's Apple developer setup. - [ ] **MUS-91: Keep `windows-cross-check` from timing out.** It hit its 20-minute limit on #110, #113 and #124, and a re-run passed each time. Every time it was the apt step: `mingw-w64` pulls 278 MB (the i686 and x86_64 toolchains, C and C++), which normally takes 7 s, but the Azure diff --git a/scripts/install-codex.sh b/scripts/install-codex.sh index bd533340..86e809ca 100755 --- a/scripts/install-codex.sh +++ b/scripts/install-codex.sh @@ -49,13 +49,49 @@ case "$os" in ;; esac -url="$REPO/releases/latest/download/muster-shim-$target" +asset="muster-shim-$target" +base="$REPO/releases/latest/download" tmp=$(mktemp -d) bin="$tmp/muster-shim" trap 'rm -rf "$tmp"' EXIT INT TERM +# The SHA-256 of a file, with whichever tool this machine has (macOS ships shasum). +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + return 1 + fi +} + echo "Downloading muster-shim ($target)..." -curl -fSL --proto '=https' --tlsv1.2 -o "$bin" "$url" +curl -fSL --proto '=https' --tlsv1.2 -o "$bin" "$base/$asset" + +# Check it against the release's SHA256SUMS before running it (MUS-95). Both come from the same +# release over HTTPS, so this catches a corrupt or swapped download, not a tampered release. +# Anything short of a match stops here, before the binary runs. +if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$tmp/SHA256SUMS" "$base/SHA256SUMS"; then + echo "error: couldn't download SHA256SUMS from the release; not installing." >&2 + exit 1 +fi +expected=$(awk -v f="$asset" '{ name = $2; sub(/^\*/, "", name) } name == f { print $1; exit }' "$tmp/SHA256SUMS") +if [ -z "$expected" ]; then + echo "error: the release's SHA256SUMS has no entry for $asset; not installing." >&2 + exit 1 +fi +if ! actual=$(sha256_of "$bin"); then + echo "error: neither sha256sum nor shasum is installed, so the download can't be checked; not installing." >&2 + exit 1 +fi +if [ "$actual" != "$expected" ]; then + echo "error: $asset doesn't match the release's SHA256SUMS; not installing." >&2 + echo " expected $expected" >&2 + echo " got $actual" >&2 + exit 1 +fi +echo "Checked $asset against SHA256SUMS." chmod +x "$bin" # Clear the Gatekeeper quarantine in case this binary was fetched via a browser # (curl downloads carry none). Harmless on Linux. diff --git a/scripts/test-install-codex.sh b/scripts/test-install-codex.sh new file mode 100755 index 00000000..3e726247 --- /dev/null +++ b/scripts/test-install-codex.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# Check that scripts/install-codex.sh runs the shim only when it matches the release's SHA256SUMS +# (MUS-95). A fake `curl` serves a local fixture instead of GitHub, and the "shim" is a script that +# records how it was run, so nothing is downloaded and Codex is never touched. Runs in CI. +set -u +REPO=$(cd "$(dirname "$0")/.." && pwd) +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT INT TERM +pass=0 +fail=0 +check() { + if eval "$2"; then echo "PASS $1"; pass=$((pass + 1)); else echo "FAIL $1"; fail=$((fail + 1)); fi +} + +# The fake curl: `-o FILE ... URL` copies the fixture named like the URL's last part, or fails as +# `curl -f` does when the release has no such file. +mkdir -p "$WORK/fake" +cat >"$WORK/fake/curl" <<'EOF' +#!/bin/sh +out="" +url="" +while [ $# -gt 0 ]; do + case "$1" in + -o) out=$2; shift 2 ;; + --proto | --tlsv1.2) [ "$1" = --proto ] && shift; shift ;; + -*) shift ;; + *) url=$1; shift ;; + esac +done +src="$FIXTURE/${url##*/}" +[ -f "$src" ] || { echo "curl: (22) 404 for $url" >&2; exit 22; } +cp "$src" "$out" +EOF +chmod +x "$WORK/fake/curl" + +# A fixture release: the "shim" under both targets' names, and a SHA256SUMS listing them. +release() { + FIXTURE="$WORK/release-$1" + rm -rf "$FIXTURE" + mkdir -p "$FIXTURE" + for target in aarch64-apple-darwin x86_64-unknown-linux-gnu; do + printf '#!/bin/sh\necho "$*" > "$MARK"\n' >"$FIXTURE/muster-shim-$target" + done + (cd "$FIXTURE" && sha256sum muster-shim-* >SHA256SUMS) + export FIXTURE +} + +# Runs the installer with PATH=$1, and records its exit status, its output and what the shim saw. +install() { + MARK="$WORK/shim-ran" + rm -f "$MARK" + export MARK + PATH=$1 sh "$REPO/scripts/install-codex.sh" --daemon ws://test:6255 >"$WORK/out" 2>&1 + status=$? +} +ran() { [ -f "$MARK" ]; } +said() { grep -q "$1" "$WORK/out"; } + +FULL="$WORK/fake:$PATH" + +release good +install "$FULL" +check "a matching shim installs" '[ $status -eq 0 ] && said "Checked muster-shim-"' +check "and is run with codex-setup and the daemon" '[ "$(cat "$MARK")" = "codex-setup --daemon ws://test:6255" ]' + +release tampered +for f in "$FIXTURE"/muster-shim-*; do echo "# one byte more" >>"$f"; done +install "$FULL" +check "a tampered shim is refused" '[ $status -ne 0 ] && said "doesn.t match the release.s SHA256SUMS"' +check "and never runs" '! ran' + +release no-entry +: >"$FIXTURE/SHA256SUMS.new" +grep -v muster-shim "$FIXTURE/SHA256SUMS" >"$FIXTURE/SHA256SUMS.new" +mv "$FIXTURE/SHA256SUMS.new" "$FIXTURE/SHA256SUMS" +install "$FULL" +check "a SHA256SUMS without the shim is refused" '[ $status -ne 0 ] && said "has no entry for muster-shim-" && ! ran' + +release no-sums +rm "$FIXTURE/SHA256SUMS" +install "$FULL" +check "a release without SHA256SUMS is refused" '[ $status -ne 0 ] && said "couldn.t download SHA256SUMS" && ! ran' + +# macOS has shasum, not sha256sum. A PATH with only the tools the installer needs, a shasum that +# answers like the real one, and no sha256sum. +mkdir -p "$WORK/min" +for tool in sh awk uname mktemp chmod rm cp cat; do ln -s "$(command -v "$tool")" "$WORK/min/$tool"; done +ln -s "$WORK/fake/curl" "$WORK/min/curl" +SHA256SUM=$(command -v sha256sum) +printf '#!/bin/sh\n[ "$1" = -a ] && [ "$2" = 256 ] || exit 2\nexec %s "$3"\n' "$SHA256SUM" >"$WORK/min/shasum" +chmod +x "$WORK/min/shasum" +release good +install "$WORK/min" +check "with only shasum, a matching shim installs" '[ $status -eq 0 ] && ran' +release tampered +for f in "$FIXTURE"/muster-shim-*; do echo "# one byte more" >>"$f"; done +install "$WORK/min" +check "with only shasum, a tampered shim is refused" '[ $status -ne 0 ] && ! ran' +rm "$WORK/min/shasum" +release good +install "$WORK/min" +check "with neither tool, it refuses rather than skip the check" '[ $status -ne 0 ] && said "neither sha256sum nor shasum" && ! ran' + +echo "$pass passed, $fail failed" +[ "$fail" -eq 0 ] From 33bcbcc140468570c95d4ba42b4beef14d39e49d Mon Sep 17 00:00:00 2001 From: theBlanketBaron Date: Thu, 5 Nov 2026 16:34:33 -0500 Subject: [PATCH 2/4] TEMPORARY: run the release workflow on this PR to test stripping (to be reverted) Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1980a49..099d5a17 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ on: push: tags: ["v*"] workflow_dispatch: + # TEMPORARY, to test the stripped build on this PR; reverted before review. + pull_request: permissions: contents: read From cdce7208b423f23ea08650d438d288f56eeec659 Mon Sep 17 00:00:00 2001 From: theBlanketBaron Date: Thu, 5 Nov 2026 16:42:33 -0500 Subject: [PATCH 3/4] Revert the temporary pull_request trigger; the workflow runs on tags and workflow_dispatch only Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 099d5a17..b1980a49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,8 +12,6 @@ on: push: tags: ["v*"] workflow_dispatch: - # TEMPORARY, to test the stripped build on this PR; reverted before review. - pull_request: permissions: contents: read From 6b3873363fe8731510acc2212030cdfa153f3824 Mon Sep 17 00:00:00 2001 From: theBlanketBaron Date: Thu, 5 Nov 2026 16:44:29 -0500 Subject: [PATCH 4/4] Renumber to MUS-96; say in the README what the checksum guards against gilt-skate's open #167 already files MUS-95, so this story is MUS-96, which is unused on main and in every open PR. The README no longer says there's no release pipeline, and says SHA256SUMS comes from the same release, so it catches a corrupt or swapped download, not a compromised release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- README.md | 2 +- docs/roadmap.md | 2 +- scripts/install-codex.sh | 2 +- scripts/test-install-codex.sh | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41b42923..724d4f5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,9 +72,9 @@ jobs: run: cargo build --workspace && ./scripts/test-gate-e2e.sh - name: Links between daemons end-to-end (MUS-66) run: ./scripts/test-links-e2e.sh - # MUS-95: install-codex.sh runs the shim only when it matches the release's SHA256SUMS. + # MUS-96: install-codex.sh runs the shim only when it matches the release's SHA256SUMS. # A fake curl serves a local fixture, so nothing is downloaded. - - name: Installer checks its download (MUS-95) + - name: Installer checks its download (MUS-96) run: sh scripts/test-install-codex.sh # MUS-58: the sweeper starts only once myko has replayed Postgres, so an # ad-hoc room membership survives a restart. Uses the job's Postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1980a49..d9839403 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,7 @@ jobs: targets: ${{ matrix.target }} - name: Build env: - # MUS-95: shipped binaries carry no symbol tables or debuginfo. Here rather than in + # MUS-96: shipped binaries carry no symbol tables or debuginfo. Here rather than in # Cargo.toml, so local release builds keep theirs. CARGO_PROFILE_RELEASE_STRIP: symbols run: cargo build --release --locked --target ${{ matrix.target }} -p muster-shim -p muster-daemon diff --git a/README.md b/README.md index 4ae30242..b39f7a46 100644 --- a/README.md +++ b/README.md @@ -556,7 +556,7 @@ it to a LAN or mesh address, or `0.0.0.0:6255`, to accept other machines. | [`muster-prime-agent`](plugins/muster-prime-agent/) | Prime Agent live-conversation extension. | | [`muster-opencode`](plugins/muster-opencode/) | opencode native plugin. | -Muster has no release pipeline or published packages yet; install from the repository (see Install). CI (`.github/workflows/ci.yml`) runs fmt, clippy, the tests and `scripts/test-gate-e2e.sh` on every push and pull request to `main`. +Muster has no published release or packages yet; install from the repository (see Install). `.github/workflows/release.yml` builds muster-shim and muster-daemon for macOS arm64 and Linux x86_64 with a `SHA256SUMS` (MUS-14), and `scripts/install-codex.sh` checks the shim against that file before running it (MUS-96). Both come from the same release, so the check guards against a corrupt or swapped download, not against a compromised release. CI (`.github/workflows/ci.yml`) runs fmt, clippy, the tests and `scripts/test-gate-e2e.sh` on every push and pull request to `main`. ## Status diff --git a/docs/roadmap.md b/docs/roadmap.md index d0e06bbd..047d2bc9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -403,7 +403,7 @@ Merged and needing a live run: MUS-36 and MUS-37. Linux x86_64 under the names install-codex.sh asks for, with one SHA256SUMS. workflow_dispatch is a dry run that only uploads artifacts; a `v*` tag also makes a draft release, which stays unpublished until Josh publishes it. Left: a tag and a published release, on Josh's word. -- [x] **MUS-95: The installer checks its download, and release binaries are stripped.** From +- [x] **MUS-96: The installer checks its download, and release binaries are stripped.** From MUS-14's review. Done when: `scripts/install-codex.sh` fetches SHA256SUMS from the same release, checks the shim against it with sha256sum (or `shasum -a 256` on macOS), and refuses to run it on a mismatch, a missing entry, a missing SHA256SUMS or no tool to check with; a fixture test diff --git a/scripts/install-codex.sh b/scripts/install-codex.sh index 86e809ca..bdb7363e 100755 --- a/scripts/install-codex.sh +++ b/scripts/install-codex.sh @@ -69,7 +69,7 @@ sha256_of() { echo "Downloading muster-shim ($target)..." curl -fSL --proto '=https' --tlsv1.2 -o "$bin" "$base/$asset" -# Check it against the release's SHA256SUMS before running it (MUS-95). Both come from the same +# Check it against the release's SHA256SUMS before running it (MUS-96). Both come from the same # release over HTTPS, so this catches a corrupt or swapped download, not a tampered release. # Anything short of a match stops here, before the binary runs. if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$tmp/SHA256SUMS" "$base/SHA256SUMS"; then diff --git a/scripts/test-install-codex.sh b/scripts/test-install-codex.sh index 3e726247..a904d421 100755 --- a/scripts/test-install-codex.sh +++ b/scripts/test-install-codex.sh @@ -1,6 +1,6 @@ #!/bin/sh # Check that scripts/install-codex.sh runs the shim only when it matches the release's SHA256SUMS -# (MUS-95). A fake `curl` serves a local fixture instead of GitHub, and the "shim" is a script that +# (MUS-96). A fake `curl` serves a local fixture instead of GitHub, and the "shim" is a script that # records how it was run, so nothing is downloaded and Codex is never touched. Runs in CI. set -u REPO=$(cd "$(dirname "$0")/.." && pwd)