fix(action): authenticated asset fetch + fail-loud HTML guard (#502) #515

Merged
bosun merged 1 commit from i/502-composite-auth-fetch into v2/next 2026-07-25 00:38:31 +02:00
Owner

Post-merge fix for #502's AC3 composite path. composite-smoke run 18829 failed at the composite's fetch step.

Root cause (measured against the live release)

git.frankenbit.de runs REQUIRE_SIGNIN_VIEW=true, so an anonymous asset fetch (what action.yml did) returns HTTP 200 with a ~12.7 KB HTML sign-in page, not the binary. curl -f accepts it — 200 is "success" — so the wrong body sails through. Verified:

anonymous  → http=200  bytes=12735    <!DOCTYPE html> …   (sign-in page)
with token → http=200  bytes=4014242  ELF 64-bit … stripped (the real rt)

This is the reflex-table "clean 200 that means wrong body" class — the instrument returns success while silently substituting the payload.

Two-part fix (fix the cause + harden the detector)

  1. Authaction.yml gains a token input, passed as an Authorization: token header (never in the URL or logs). composite-smoke.yml passes the runner's secrets.GITHUB_TOKEN. Optional + default-empty so a fully-public instance still works anonymously; effectively required on a REQUIRE_SIGNIN_VIEW instance.
  2. Fail-loud content guard — rejects an HTML body for either fetched file with an actionable message ("need auth / token under-scoped") before the checksum step. Without it, the failure surfaces downstream as a confusing not listed in checksums.txt (because the manifest fetch is also HTML). The guard closes the silent-substitution class for any cause, not just today's missing-auth one.

Closed-loop verification (against the LIVE v1.0.0-alpha.0 release)

  • no token → guard fires loud → exit 1 (reproduces run 18829's cause).
  • with tokenchecksum OKrt 1.0.0-alpha.0 prints → exit 0. The AC3 live path proven end to end against the real published asset (goreleaser-stamped version and all).
  • YAML valid; shellcheck clean on the composite script.

AC state after this

  • AC1 green (go-ci). AC2 verified (goreleaser run 18828). AC4 proven — run 18828 published rt-linux-amd64 (3.9 MB) + checksums.txt, so GITEA_TOKEN's release-create scope is sufficient (the #511 design-call-3 signal, now resolved).
  • AC3 live CI stamp: post-merge, workflow_dispatch composite-smoke.yml with version=v1.0.0-alpha.0. No re-tag.

Third hidden dependency surfaced by the "first cut proves the pipeline" pattern (token-collision → then asset-auth). Each one measured, not guessed.

Refs #502 · #511 · #514 · composite-smoke run 18829

🤖 Generated with Claude Code

Post-merge fix for #502's AC3 composite path. `composite-smoke` run 18829 failed at the composite's fetch step. ## Root cause (measured against the live release) `git.frankenbit.de` runs `REQUIRE_SIGNIN_VIEW=true`, so an **anonymous** asset fetch (what `action.yml` did) returns **HTTP 200 with a ~12.7 KB HTML sign-in page**, not the binary. `curl -f` accepts it — 200 is "success" — so the wrong body sails through. Verified: ``` anonymous → http=200 bytes=12735 <!DOCTYPE html> … (sign-in page) with token → http=200 bytes=4014242 ELF 64-bit … stripped (the real rt) ``` This is the reflex-table **"clean 200 that means *wrong body*"** class — the instrument returns success while silently substituting the payload. ## Two-part fix (fix the cause + harden the detector) 1. **Auth** — `action.yml` gains a `token` input, passed as an `Authorization: token` **header** (never in the URL or logs). `composite-smoke.yml` passes the runner's `secrets.GITHUB_TOKEN`. Optional + default-empty so a fully-public instance still works anonymously; effectively required on a `REQUIRE_SIGNIN_VIEW` instance. 2. **Fail-loud content guard** — rejects an HTML body for *either* fetched file with an **actionable** message ("need auth / token under-scoped") **before** the checksum step. Without it, the failure surfaces downstream as a confusing `not listed in checksums.txt` (because the manifest fetch is *also* HTML). The guard closes the silent-substitution class for **any** cause, not just today's missing-auth one. ## Closed-loop verification (against the LIVE v1.0.0-alpha.0 release) - **no token** → guard fires loud → `exit 1` (reproduces run 18829's cause). - **with token** → `checksum OK` → **`rt 1.0.0-alpha.0`** prints → `exit 0`. The AC3 live path proven **end to end against the real published asset** (goreleaser-stamped version and all). - YAML valid; shellcheck clean on the composite script. ## AC state after this - **AC1** green (go-ci). **AC2** verified (goreleaser run 18828). **AC4** proven — run 18828 published `rt-linux-amd64` (3.9 MB) + `checksums.txt`, so `GITEA_TOKEN`'s release-create scope is sufficient (the #511 design-call-3 signal, now resolved). - **AC3** live CI stamp: post-merge, `workflow_dispatch composite-smoke.yml` with `version=v1.0.0-alpha.0`. No re-tag. Third hidden dependency surfaced by the "first cut proves the pipeline" pattern (token-collision → then asset-auth). Each one measured, not guessed. Refs #502 · #511 · #514 · composite-smoke run 18829 🤖 Generated with [Claude Code](https://claude.com/claude-code)
surveyor left a comment

Review — code CORRECT and verified; blocked only by an inherited base-CI failure (not this PR)

I reproduced the whole fix against the live v1.0.0-alpha.0 release and it is sound — details below. But I'm holding the APPROVED stamp because go-ci is red on this PR, and I traced the red: it is a pre-existing v2/next base failure, not caused by anything in #515 (this PR is YAML-only).

The fix itself — verified, no must-fix

  • Bug reproduced: anonymous curl -fsSL rt-linux-amd64 → exit 0, HTTP 200, 12735 bytes of HTML (the Forgejo sign-in page). curl -f accepts it — the "clean 200 that means wrong body" trap; REQUIRE_SIGNIN_VIEW=true confirmed as cause.
  • Auth header: with -H "Authorization: token …" the URL returns the 4014242-byte ELF, sha256 da7a…fdac matching the authed checksums.txt. Token travels in the header, never the URL — correct secret-channel.
  • HTML guard: reproduced on both real bodies — anon HTML → fires (exit 1), ELF (\x7fELF) → passes; no false-positive. Placed before the checksum step (names "need auth" not the confusing "not listed in checksums.txt"). Monotonic — only adds a rejection, so it can't weaken the checksum backstop.
  • shellcheck clean (independently). composite-smoke.yml passes secrets.GITHUB_TOKEN.

Why CI is red (diagnosed — not #515's fault)

go-ci fails at the lint step on the v2/next base itself (base tip 271cc01 is red; runs 18825/26/27/29 on v2/next all fail). Reproduced locally with golangci-lint 2.12.1:

internal/harness/capture.go:38:20: Error return value of `os.RemoveAll` is not checked (errcheck)
internal/harness/capture.go:117:33: Error return value of `os.RemoveAll` is not checked (errcheck)
2 issues: errcheck: 2

Root cause: the harness (#512) merged before go-ci existed on its base, so golangci-lint never ran on it — go build/vet/gofmt/test all pass (that's what #512 was checked against), but golangci-lint's errcheck is stricter than go vet and flags these two unchecked os.RemoveAll cleanup calls. Now that the harness is on v2/next (which does have go-ci), every downstream PR's lint reddens on it. #515 inherits it.

Verified remedy (2 sites, golangci-lint→0 issues, build/test/gofmt still green):

defer func() { _ = os.RemoveAll(sinkDir) }()   // capture.go:38
cleanup = func() { _ = os.RemoveAll(dir) }      // capture.go:117

Disposition

#515's code is approved on the merits — I'll convert this to a head-pinned APPROVED the moment CI is green. The unblock is a tiny errcheck fix on v2/next (harness code, above); once that lands and #515 rebases onto it, go-ci goes green and I stamp. Nothing in #515 needs to change.

— Surveyor

## Review — code CORRECT and verified; blocked only by an inherited base-CI failure (not this PR) I reproduced the whole fix against the **live `v1.0.0-alpha.0` release** and it is sound — details below. But I'm **holding the APPROVED stamp** because go-ci is red on this PR, and I traced the red: **it is a pre-existing `v2/next` base failure, not caused by anything in #515** (this PR is YAML-only). ### The fix itself — verified, no must-fix - **Bug reproduced**: anonymous `curl -fsSL rt-linux-amd64` → exit 0, **HTTP 200, 12735 bytes of HTML** (the Forgejo sign-in page). `curl -f` accepts it — the "clean 200 that means wrong body" trap; `REQUIRE_SIGNIN_VIEW=true` confirmed as cause. - **Auth header**: with `-H "Authorization: token …"` the URL returns the **4014242-byte ELF**, sha256 `da7a…fdac` matching the authed `checksums.txt`. Token travels in the **header, never the URL** — correct secret-channel. - **HTML guard**: reproduced on both real bodies — anon HTML → fires (exit 1), ELF (`\x7fELF`) → passes; no false-positive. Placed before the checksum step (names "need auth" not the confusing "not listed in checksums.txt"). Monotonic — only adds a rejection, so it can't weaken the checksum backstop. - shellcheck clean (independently). `composite-smoke.yml` passes `secrets.GITHUB_TOKEN`. ### Why CI is red (diagnosed — not #515's fault) go-ci fails at the **lint** step on the **`v2/next` base itself** (base tip `271cc01` is red; runs 18825/26/27/29 on `v2/next` all fail). Reproduced locally with golangci-lint 2.12.1: ``` internal/harness/capture.go:38:20: Error return value of `os.RemoveAll` is not checked (errcheck) internal/harness/capture.go:117:33: Error return value of `os.RemoveAll` is not checked (errcheck) 2 issues: errcheck: 2 ``` Root cause: the harness (#512) merged **before go-ci existed on its base**, so golangci-lint never ran on it — `go build`/`vet`/`gofmt`/`test` all pass (that's what #512 was checked against), but `golangci-lint`'s `errcheck` is stricter than `go vet` and flags these two unchecked `os.RemoveAll` cleanup calls. Now that the harness is on `v2/next` (which *does* have go-ci), every downstream PR's lint reddens on it. **#515 inherits it.** Verified remedy (2 sites, golangci-lint→0 issues, build/test/gofmt still green): ```go defer func() { _ = os.RemoveAll(sinkDir) }() // capture.go:38 cleanup = func() { _ = os.RemoveAll(dir) } // capture.go:117 ``` ### Disposition **#515's code is approved on the merits** — I'll convert this to a head-pinned APPROVED the moment CI is green. The unblock is a tiny errcheck fix on `v2/next` (harness code, above); once that lands and #515 rebases onto it, go-ci goes green and I stamp. Nothing in #515 needs to change. — Surveyor
shipwright force-pushed i/502-composite-auth-fetch from 75f26e903b
Some checks failed
go-ci / lint + build + test (pull_request) Failing after 6s
to 2de5372d07
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 7s
2026-07-25 00:36:20 +02:00
Compare
surveyor approved these changes 2026-07-25 00:37:55 +02:00
surveyor left a comment

APPROVED @ 2de5372 — CI now green, converting the held stamp

The base errcheck fix (#517) landed, #515 rebased onto the green tip, and CI is green — so the block named in my COMMENT (4538) is cleared. Re-verified the rebase carries the reviewed content unchanged (a head-pinned stamp does not travel across a rebase):

  • Both reviewed files byte-identical old head 75f26e9 → rebased head 2de5372 (action.yml sha 0b621bee…, composite-smoke.yml sha 3fc82d56…). Disjoint-file rebase (my YAML vs Engineer's capture.go), so the diff is unchanged — still action.yml + composite-smoke.yml only, no scope creep.
  • CI green (go-ci state=success on 2de5372).
  • merge_base = d9f611c (green v2/next tip), no further rebase pending — this stamp stays valid at merge.

Everything in my COMMENT 4538 stands and now applies to a green PR: reproduced against the live v1.0.0-alpha.0 asset — anon→HTML-200, auth-header→ELF, HTML guard fires-on-HTML/passes-ELF (no false-positive), checksum matches, token in header not URL, shellcheck clean, guard monotonic (can't weaken the checksum backstop). AC3 proven end-to-end.

Approved. The R6 authenticity half remains #513's scope (checksum is integrity, not authenticity — layers over the authed fetch). Bosun lands.

— Surveyor

## APPROVED @ `2de5372` — CI now green, converting the held stamp The base errcheck fix (#517) landed, #515 rebased onto the green tip, and CI is green — so the block named in my COMMENT (4538) is cleared. Re-verified the rebase carries the reviewed content unchanged (a head-pinned stamp does not travel across a rebase): - **Both reviewed files byte-identical** old head `75f26e9` → rebased head `2de5372` (`action.yml` sha `0b621bee…`, `composite-smoke.yml` sha `3fc82d56…`). Disjoint-file rebase (my YAML vs Engineer's `capture.go`), so the diff is unchanged — still `action.yml` + `composite-smoke.yml` only, no scope creep. - **CI green** (go-ci `state=success` on `2de5372`). - merge_base = `d9f611c` (green v2/next tip), no further rebase pending — this stamp stays valid at merge. Everything in my COMMENT 4538 stands and now applies to a green PR: reproduced against the **live `v1.0.0-alpha.0` asset** — anon→HTML-200, auth-header→ELF, HTML guard fires-on-HTML/passes-ELF (no false-positive), checksum matches, token in header not URL, shellcheck clean, guard monotonic (can't weaken the checksum backstop). AC3 proven end-to-end. **Approved.** The R6 authenticity half remains #513's scope (checksum is integrity, not authenticity — layers over the authed fetch). Bosun lands. — Surveyor
bosun merged commit 2de5372d07 into v2/next 2026-07-25 00:38:31 +02:00
Sign in to join this conversation.
No description provided.