From 7030ce0cf107909daca0f50b3cbb6294a194c314 Mon Sep 17 00:00:00 2001 From: Nikita Ivanov Date: Sun, 3 May 2026 12:50:00 +0000 Subject: [PATCH] fix(installer): four dogfood-discovered bugs blocking curl-pipe-bash on bare Ubuntu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end dogfood retest of v0.3.2-rc1 against bare ubuntu:24.04 (no preinstalled deps beyond curl + ca-certificates) surfaced four issues that block the documented `curl ... | bash` happy path: 1. **`file(1)` dependency in curl-installer** — packaging/curl-installer/ install.sh:168 used `file --` to validate gzip magic before sha256. The `file` package is NOT preinstalled on minimal Ubuntu/Debian cloud images or most Docker base images. Replaced with `head -c 2 + od -tx1` magic-byte read against `1f8b` (RFC 1952). Pure coreutils — `head` and `od` are always present. Diagnostic value preserved (still distinguishes "wrong magic" from "wrong sha256"; on failure now reports the actual magic bytes observed, e.g. `(magic bytes: 3c21)` for an HTML 404 body). 2. **`sudo` package missing on bare Ubuntu** — plugin/provisioner/ 20-sudoers.sh:75 invoked `visudo -cf` which comes from the `sudo` package (not preinstalled on minimal images). Mirrors the existing pattern in 10-agent-user.sh that auto-installs `locales` if `locale-gen` is missing. New gate at the top of 20-sudoers.sh: `command -v visudo || apt-get install -y --no-install-recommends sudo`. 3. **No VERSION sentinel in release artifacts** — packaging/curl-installer/ install.sh:111 follows `releases/latest/download/VERSION` and parses the redirect URL to discover the latest tag. The asset isn't read — only the redirect URL matters — but `curl -fsSIL` requires the redirect target to return 2xx, not 404. Without VERSION shipped per release, any unpinned `curl ... | bash` against an asset-less release dies with `could not resolve latest version`. Fixed by adding a one-line write to `dist/VERSION` (containing the tag) in scripts/build-release.sh §10b and adding `dist/VERSION` to the publish files glob in release.yml. 4. **(operational, not in this commit)** v0.3.2-rc1 was published as a full GitHub release rather than a pre-release, so `releases/latest` redirected to it. Fixed via `gh release edit v0.3.2-rc1 --prerelease`. Future RCs should be built/published with the GitHub Releases "pre-release" flag set; tracked separately. Test rig: bare ubuntu:{22.04, 24.04, 26.04} Docker, curl + ca-certificates only, AGENTLINUX_RELEASE_BASE pointed at a local serve of the patched tarball, AGENTLINUX_VERSION=v0.3.2-rc1. All three Ubuntu versions reach a green `claude update` (2.1.98 → 2.1.126) with zero EACCES / permission-denied lines in the install + agent-install + claude-update transcripts. AGT-02 release-gate behavior confirmed against the published RC artifacts after applying these fixes locally. Refs: AL-18 (first-dogfood follow-up), AL-21 (dogfood retest sub-task) Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 1 + packaging/curl-installer/install.sh | 11 +++++++++-- plugin/provisioner/20-sudoers.sh | 10 ++++++++++ scripts/build-release.sh | 15 ++++++++++++++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63c0318..3eaf6d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -324,3 +324,4 @@ jobs: dist/agentlinux-*.tar.gz.sha256 dist/catalog-*.json dist/agentlinux_*.deb + dist/VERSION diff --git a/packaging/curl-installer/install.sh b/packaging/curl-installer/install.sh index 6156781..4ba8793 100755 --- a/packaging/curl-installer/install.sh +++ b/packaging/curl-installer/install.sh @@ -165,8 +165,15 @@ main() { # HTTP 404 HTML body would land in the tarball path and sha256sum -c would # emit a confusing "FAILED" verdict. Asserting the gzip magic bytes BEFORE # sha256 gives a precise error. - if ! file -- "${tmpdir}/${tarball}" | grep -q 'gzip compressed'; then - die "downloaded ${tarball} is not a gzip archive — possible 404-as-HTML or proxy-rewrite; refusing to proceed" + # + # Read the first two bytes via `head` + `od` rather than `file(1)`: the + # `file` package is NOT preinstalled on minimal Ubuntu/Debian cloud images + # (and many Docker base images). `head` and `od` are coreutils, always + # present. Magic for gzip is 1f 8b (RFC 1952). + local _magic + _magic=$(head -c 2 "${tmpdir}/${tarball}" 2>/dev/null | od -An -tx1 | tr -d ' \n') + if [[ "$_magic" != "1f8b" ]]; then + die "downloaded ${tarball} is not a gzip archive (magic bytes: ${_magic:-empty}) — possible 404-as-HTML or proxy-rewrite; refusing to proceed" fi # SHA256 verification BEFORE extraction (T-06-02 — hard security gate). diff --git a/plugin/provisioner/20-sudoers.sh b/plugin/provisioner/20-sudoers.sh index bff6129..fe58171 100644 --- a/plugin/provisioner/20-sudoers.sh +++ b/plugin/provisioner/20-sudoers.sh @@ -37,6 +37,16 @@ log_info "20-sudoers: starting" +# Minimal Ubuntu/Debian cloud images (and many Docker base images) ship without +# the `sudo` package, which provides both the `sudo` binary AND `visudo`. We +# need `visudo` to validate the drop-in before installing it (T-05.1-01), and +# the agent user obviously needs `sudo` afterwards. Mirror the pattern used by +# 10-agent-user.sh's `locales` install. +if ! command -v visudo >/dev/null 2>&1; then + log_warn "visudo not found; installing 'sudo' package" + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends sudo +fi + readonly SUDOERS_FILE="/etc/sudoers.d/agentlinux" # Single-quoted heredoc — no shell expansion, byte-stable across re-runs. The # meaningful policy is the single line `agent ALL=(ALL) NOPASSWD: ALL`; the diff --git a/scripts/build-release.sh b/scripts/build-release.sh index e9a0a0c..69137ef 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -315,6 +315,19 @@ if [[ "$SRC_SHA" != "$SNAPSHOT_SHA" ]]; then exit 1 fi +# --------------------------------------------------------------------------- +# 10b. VERSION sentinel asset. +# packaging/curl-installer/install.sh resolves an unpinned tag by +# following https://github.com/.../releases/latest/download/VERSION and +# capturing the redirect URL with curl -fsSIL. The asset itself doesn't +# need to be machine-parsed — but it MUST exist so curl -f doesn't fail +# on the redirect target. Without this file shipped on every release, +# `curl -fsSL https://agentlinux.org/install.sh | bash` fails with +# "could not resolve latest version" against any release that lacks the +# sentinel (dogfood-discovered against v0.3.2-rc1). +# --------------------------------------------------------------------------- +printf '%s\n' "$TAG" >dist/VERSION + # --------------------------------------------------------------------------- # 11. Optional .deb via fpm (ADR-006 — optional v0.3.0 path). # Skip gracefully if fpm is absent or SKIP_DEB=1 or --no-deb — the @@ -345,4 +358,4 @@ fi # --------------------------------------------------------------------------- # 12. Final summary (stdout-only; no emojis per CLAUDE.md). # --------------------------------------------------------------------------- -printf 'Built: %s + .sha256 + catalog-%s.json%s\n' "$TARBALL" "$TAG" "$DEB_SUFFIX" +printf 'Built: %s + .sha256 + catalog-%s.json + VERSION%s\n' "$TARBALL" "$TAG" "$DEB_SUFFIX"