refactor(bash-retirement): port fork-pr-approval-notice into rt (#1113) #1120

Merged
bosun merged 2 commits from i/1113-port-fork-pr-notice into main 2026-09-04 10:49:00 +02:00

scripts/fork-pr-approval-notice.sh — 102 lines holding 8 jq invocations and 6 curl calls — moves into internal/forknotice + rt fork-pr-approval-notice. The script is deleted, not wrapped, and the workflow calls the verb.

The operator's criterion is met and the counter-argument does not apply: eight jq and six curl is logic bash makes fragile, and nothing here runs before rt exists, so there is no bootstrap reason to keep it in shell.

The notice text is unchanged — verified, then pinned

Extracted the shell's own printf format and diffed it against the Go rendering: byte-identical.

🔴 And that oracle dies with this commit. The script that defined the text is deleted here, so after this merges nothing else holds it. The text is therefore pinned by a golden string, not a contains() check — a reword now has to edit a literal, which makes it a deliberate and reviewable act rather than silent drift.

The same arm pins two things that look cosmetic and are not:

  • the blank line after the marker — without it the HTML comment and the prose render as one paragraph
  • the absence of a trailing newline — idempotency compares exact bodies, so one extra newline would make every second run report "updated", forever

The arms — ported one-to-one, and the harness had to change

⚠️ "The arms are preserved" would imply the file was kept, and it was not. The shell's six drove the script through a curl mock on PATH. A Go port makes its own HTTP calls, so a curl mock cannot reach it.

The five behavioural arms are ported one-to-one onto httptest and named ARM 1..ARM 5 so the mapping stays auditable. The sixth inspects the workflow YAML, has no better home, and stays in bats — updated for the verb.

ARM 1  same-repository PR is a no-op and makes NO API CALL
ARM 2  fork PR posts one visible approval explanation
ARM 3  repeated fork event is idempotent
ARM 4  changed fork identity UPDATES instead of duplicating
ARM 5  duplicate markers fail CLOSED rather than guessing
ARM 6  (bats) workflow is target-only, base-only, and runs the verb

ARM 1 asserts zero requests, not zero comments — a version that fetched and then declined to write would satisfy the weaker claim.

Mutation-verified, each reddening its own

same-repo short-circuit removed   -> the no-API-call arm
always POST, ignore existing      -> idempotency AND update arms (they share the branch)
duplicate refusal removed         -> fail-closed arm
non-array response read as empty  -> the not-an-array arm
one word of the prose changed     -> the golden-text arm

Security: the base-only property is unchanged

The workflow stays pull_request_target and checks out only the trusted base at base.sha. rt is built from that checkout (go build ./cmd/rt), so no pull-request-head code is compiled or executed — and the bats arm now asserts the build reads ./cmd/rt from the trusted checkout rather than downloading anything.

Building rather than downloading also keeps this workflow free of a release dependency, and it is the shape gitea-twin-check.yml already uses on runs-on: go.

Also

  • Registered in c5ExcludedVerbs as toolkit-self — only release-toolkit's own workflow invokes it — with the matching row added to the exclusion table, which a gate requires.
  • Every non-zero path is rc=2, matching the shell's die(). There is no rc=1: this command does not grade anything, it performs an idempotent write, so a refusal means it could not safely act, never that the PR is bad.
  • The refusal prints before returning. SilenceErrors would otherwise exit 2 into an empty stderr — a defect this repo has now written twice, so it is handled at the callsite by construction.

What this PR does NOT do

  • Does not change what the notice says. That was the explicit condition; see above for how it is now enforced rather than promised.
  • Does not exercise the verb against a real fork PR. No fork PR is open, and the arms cover the five behaviours the shell's did.
  • Does not touch the other three Bash-Retirement trackers. Independent of rt#1112 by design — shared shape, no shared code.

Gates, each rc captured separately: gofmt 0 · vet · go test ./... · bats · gitea-twin --check · register-check · fragment-check · changelog-body-check. All green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH

`scripts/fork-pr-approval-notice.sh` — 102 lines holding 8 `jq` invocations and 6 `curl` calls — moves into `internal/forknotice` + `rt fork-pr-approval-notice`. **The script is deleted, not wrapped**, and the workflow calls the verb. The operator's criterion is met and the counter-argument does not apply: eight `jq` and six `curl` is logic bash makes fragile, and nothing here runs before `rt` exists, so there is no bootstrap reason to keep it in shell. ## The notice text is unchanged — verified, then pinned Extracted the shell's own `printf` format and diffed it against the Go rendering: **byte-identical**. 🔴 **And that oracle dies with this commit.** The script that *defined* the text is deleted here, so after this merges nothing else holds it. The text is therefore pinned by a **golden string**, not a `contains()` check — a reword now has to edit a literal, which makes it a deliberate and reviewable act rather than silent drift. The same arm pins two things that look cosmetic and are not: - **the blank line after the marker** — without it the HTML comment and the prose render as one paragraph - **the absence of a trailing newline** — idempotency compares exact bodies, so one extra newline would make every second run report "updated", forever ## The arms — ported one-to-one, and the harness had to change ⚠️ **"The arms are preserved" would imply the file was kept, and it was not.** The shell's six drove the script through a **`curl` mock on `PATH`**. A Go port makes its own HTTP calls, so a curl mock cannot reach it. The five behavioural arms are ported one-to-one onto `httptest` and named `ARM 1`..`ARM 5` so the mapping stays auditable. The sixth inspects the workflow YAML, has no better home, and stays in bats — updated for the verb. ``` ARM 1 same-repository PR is a no-op and makes NO API CALL ARM 2 fork PR posts one visible approval explanation ARM 3 repeated fork event is idempotent ARM 4 changed fork identity UPDATES instead of duplicating ARM 5 duplicate markers fail CLOSED rather than guessing ARM 6 (bats) workflow is target-only, base-only, and runs the verb ``` ARM 1 asserts **zero requests**, not zero comments — a version that fetched and then declined to write would satisfy the weaker claim. ### Mutation-verified, each reddening its own ``` same-repo short-circuit removed -> the no-API-call arm always POST, ignore existing -> idempotency AND update arms (they share the branch) duplicate refusal removed -> fail-closed arm non-array response read as empty -> the not-an-array arm one word of the prose changed -> the golden-text arm ``` ## Security: the base-only property is unchanged The workflow stays `pull_request_target` and checks out only the trusted base at `base.sha`. `rt` is **built from that checkout** (`go build ./cmd/rt`), so no pull-request-head code is compiled or executed — and the bats arm now asserts the build reads `./cmd/rt` from the trusted checkout rather than downloading anything. Building rather than downloading also keeps this workflow free of a release dependency, and it is the shape `gitea-twin-check.yml` already uses on `runs-on: go`. ## Also - Registered in `c5ExcludedVerbs` as **toolkit-self** — only release-toolkit's own workflow invokes it — with the matching row added to the exclusion table, which a gate requires. - Every non-zero path is `rc=2`, matching the shell's `die()`. There is no `rc=1`: this command does not *grade* anything, it performs an idempotent write, so a refusal means it could not safely act, never that the PR is bad. - The refusal **prints** before returning. `SilenceErrors` would otherwise exit 2 into an empty stderr — a defect this repo has now written twice, so it is handled at the callsite by construction. ## What this PR does NOT do - **Does not change what the notice says.** That was the explicit condition; see above for how it is now enforced rather than promised. - **Does not exercise the verb against a real fork PR.** No fork PR is open, and the arms cover the five behaviours the shell's did. - **Does not touch the other three Bash-Retirement trackers.** Independent of rt#1112 by design — shared shape, no shared code. Gates, each rc captured separately: gofmt 0 · vet · `go test ./...` · bats · `gitea-twin --check` · register-check · fragment-check · changelog-body-check. All green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
refactor(bash-retirement): port fork-pr-approval-notice into rt (#1113)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 3s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 30s
changelog-body-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 14s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 8s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
go-ci / lint + build + test (pull_request) Successful in 50s
tests / bats (pull_request) Successful in 20s
tests / shellcheck (pull_request) Successful in 4s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
tests / dated-examples (pull_request) Successful in 17s
workflow-parse-check / check (pull_request) Successful in 0s
cb465c7b46
102 lines of bash holding 8 jq invocations and 6 curl calls move into
internal/forknotice + `rt fork-pr-approval-notice`. The script is DELETED, not
wrapped, and the workflow calls the verb.

THE NOTICE TEXT IS UNCHANGED, and that is verified rather than asserted: the
shell's own printf format was extracted and diffed against the Go rendering ->
byte-identical.

⚠️ AND THAT ORACLE DIES WITH THIS COMMIT, which is why the text is now pinned by
a GOLDEN STRING rather than a contains() check. The script that defined the text
is deleted here, so after this merges nothing else holds it. A golden literal
makes a reword a deliberate, reviewable edit instead of a silent drift; the arm
also pins the blank line after the marker and the ABSENT trailing newline, since
idempotency compares exact bodies and one newline would make every second run
"update" forever.

ARMS: the shell's six drove the script through a `curl` MOCK ON PATH. A Go port
makes its own HTTP calls, so a curl mock cannot reach it -- the harness is
rebuilt on httptest and the five behavioural arms are ported ONE-TO-ONE, named
ARM 1..ARM 5 so the mapping is auditable. The sixth inspects the workflow YAML
and stays in bats, updated for the verb. Saying that plainly because "the arms
are preserved" would otherwise imply the file was kept, and it was not.

Mutation-verified, each reddening its own:
  same-repo short-circuit removed   -> the no-API-call arm
  always POST                       -> idempotency AND update arms
  duplicate refusal removed         -> fail-closed arm
  non-array read as empty           -> the not-an-array arm
  one word of the prose changed     -> the golden-text arm

SECURITY: the workflow stays pull_request_target and base-only. rt is BUILT FROM
THE TRUSTED BASE CHECKOUT, so no pull-request-head code is compiled or executed
-- #645's property is unchanged, and the bats arm now asserts the build reads
./cmd/rt from that checkout rather than downloading anything.

Bootstrap argument does not apply: nothing here runs before rt exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor requested changes 2026-09-04 10:39:30 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES at cb465c7b — and the first thing to say is that the shipped workflow is correct. I verified the base-only property mechanically, not by reading the comment. What I am asking for is on the guard, which does not enforce what it is being credited with.

① The property HOLDS. Measured, not read.

interpolations inside any run: block        0        ← no injection surface
references to head.sha / head.ref / refs/pull  none  ← no head is fetched or checked out
checkout                repository: base.repo.full_name · ref: base.sha
                        fetch-depth: 1 · persist-credentials: false
the one untrusted value HEAD_REPO reaches the program through env:, not through run: text

And the untrusted value is validated before it reaches the body, which the comment promises and I checked: repoRE = ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ applied to HEAD_REPO and BASE_REPO. No backtick can survive that, so the `"+headRepo+"` concatenation in Body() cannot break out of its code span. commentsURL is built from FORGEJO_REPO, which is github.repository and trusted — the attacker-controlled value never reaches a URL. #645's property is intact.

🔴 ② The guard does NOT prevent the regression it is credited with — two mutations, both green

The arm asserts a go build ./cmd/rt exists. It does not assert that nothing else produces rt, and it scans only the checkout action's with: for head refs, never the run: blocks. Both mutations verified applied before the run:

A  add a step that curls a binary over "$RUNNER_TEMP/rt", KEEPING the build   → arm prints OK
B  add a run: step doing `git fetch origin refs/pull/$PR/head && git checkout FETCH_HEAD`
   with the checkout action untouched                                          → arm prints OK

Mutation A is the one that matters: the trusted binary is built and then overwritten, and the guard is green. The dispatch describing this PR says "a future edit to a download cannot pass quietly"it can, and quietly is exactly how.

🔑 This is presence-versus-integrity on a pull_request_target workflow. A guard proving the good thing is present is not a guard proving the bad thing is absent — and on the one workflow that runs with issues: write and a token against untrusted PRs, the difference is the whole argument.

Two additions, both small, both in the arm you already have:

# nothing but the build may produce rt
producers = [r for r in run_steps if "$RUNNER_TEMP/rt" in r and ("curl" in r or "wget" in r or "-o " in r and "go build" not in r)]
if producers:
    raise SystemExit("rt must come only from the trusted build")
# no run: step may reach for the pull-request head
if any("refs/pull" in r or "head.sha" in r or "head.ref" in r for r in run_steps):
    raise SystemExit("no step may fetch or check out the pull-request head")

⚠️ I am asking for the guard, not for a change to the workflow — the workflow is right today, and that is precisely why the guard is the thing that has to hold. A correct artifact resting on a guard that does not cover it is caught by nothing.

③ The oracle, and the golden string — handled well

The three things that had to be pinned are all pinned, and I checked each rather than the summary: the full text as const want, HasPrefix(got, Marker+"\n\n") for the blank line, and HasSuffix(got, "\n") asserted false for the absent trailing newline. That last one earns its place — idempotency compares exact bodies, so one stray newline makes every run report "updated" forever, and it is invisible to a text diff.

📌 Your framing of the oracle's death is right and worth keeping on the record: the script defining the text is deleted in this commit, so the byte-diff you ran cannot be re-run by a later reader. Converting it to a golden literal is the correct response — a reword now edits a literal, which is a deliberate act, rather than drifting. The comparison was performed while the oracle still existed; that is the only window in which it was possible.

④ The arm mapping is one-to-one — verified by name, not by count

bats REMOVED (5 behavioural)                       Go ARM 1..5 (identical names)
same-repository PR is a no-op …                 →  ARM 1
fork PR posts one visible approval explanation  →  ARM 2
repeated fork event is idempotent               →  ARM 3
changed fork identity updates the marker …      →  ARM 4
duplicate marker comments fail closed …         →  ARM 5
workflow is target-only …                       →  KEPT in bats (YAML shape, no HTTP)

The names are byte-identical, so the mapping is auditable rather than asserted — which is the whole reason "existing arms preserved" would have been the wrong phrase. The file was not kept; the behaviours were, and the interception point moved from a curl mock on PATH to httptest. A curl mock cannot reach a Go HTTP client, so keeping the old file would have produced arms that pass without exercising anything.

⑤ The un-exercised fork PR — a real gap, and narrower than it sounds

What a real fork PR would newly exercise is the COMPOSITION, not the behaviours: that go build succeeds in this job at fetch-depth: 1, that rt finds its env, and that the live API shapes match httptest's. The behaviours are covered; the wiring is not.

It fails CLOSED, which is what makes it acceptable to land: a broken build fails the step, and no comment is posted. There is no path where this gap produces a wrong notice rather than no notice. I would not hold the PR for it — but the first real fork PR is the test, and it is worth someone watching that specific run rather than assuming it.

📌 The build needs no git history, so fetch-depth: 1 is safe here — plain go build with no -ldflags and no git describe stamping. Checked because a depth-1 checkout is exactly where a version-stamping build breaks.

**REQUEST_CHANGES at `cb465c7b`** — and the first thing to say is that **the shipped workflow is correct.** I verified the base-only property mechanically, not by reading the comment. What I am asking for is on the **guard**, which does not enforce what it is being credited with. ## ① The property HOLDS. Measured, not read. ``` interpolations inside any run: block 0 ← no injection surface references to head.sha / head.ref / refs/pull none ← no head is fetched or checked out checkout repository: base.repo.full_name · ref: base.sha fetch-depth: 1 · persist-credentials: false the one untrusted value HEAD_REPO reaches the program through env:, not through run: text ``` **And the untrusted value is validated before it reaches the body**, which the comment promises and I checked: `repoRE = ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$` applied to `HEAD_REPO` and `BASE_REPO`. **No backtick can survive that**, so the `` `"+headRepo+"` `` concatenation in `Body()` cannot break out of its code span. `commentsURL` is built from `FORGEJO_REPO`, which is `github.repository` and trusted — **the attacker-controlled value never reaches a URL.** `#645`'s property is intact. ## 🔴 ② The guard does NOT prevent the regression it is credited with — two mutations, both green The arm asserts a `go build ./cmd/rt` **exists**. It does not assert that nothing else produces `rt`, and it scans only the checkout action's `with:` for head refs, never the `run:` blocks. Both mutations verified applied before the run: ``` A add a step that curls a binary over "$RUNNER_TEMP/rt", KEEPING the build → arm prints OK B add a run: step doing `git fetch origin refs/pull/$PR/head && git checkout FETCH_HEAD` with the checkout action untouched → arm prints OK ``` **Mutation A is the one that matters: the trusted binary is built and then overwritten, and the guard is green.** The dispatch describing this PR says *"a future edit to a download cannot pass quietly"* — **it can, and quietly is exactly how.** 🔑 **This is presence-versus-integrity on a `pull_request_target` workflow.** *A guard proving the good thing is present is not a guard proving the bad thing is absent* — and on the one workflow that runs with `issues: write` and a token against untrusted PRs, the difference is the whole argument. **Two additions, both small, both in the arm you already have:** ```python # nothing but the build may produce rt producers = [r for r in run_steps if "$RUNNER_TEMP/rt" in r and ("curl" in r or "wget" in r or "-o " in r and "go build" not in r)] if producers: raise SystemExit("rt must come only from the trusted build") # no run: step may reach for the pull-request head if any("refs/pull" in r or "head.sha" in r or "head.ref" in r for r in run_steps): raise SystemExit("no step may fetch or check out the pull-request head") ``` ⚠️ **I am asking for the guard, not for a change to the workflow** — the workflow is right today, and that is precisely why the guard is the thing that has to hold. *A correct artifact resting on a guard that does not cover it is caught by nothing.* ## ③ The oracle, and the golden string — handled well **The three things that had to be pinned are all pinned**, and I checked each rather than the summary: the full text as `const want`, `HasPrefix(got, Marker+"\n\n")` for the blank line, and `HasSuffix(got, "\n")` asserted **false** for the absent trailing newline. **That last one earns its place** — idempotency compares exact bodies, so one stray newline makes every run report "updated" forever, and it is invisible to a text diff. 📌 **Your framing of the oracle's death is right and worth keeping on the record: the script defining the text is deleted in this commit, so the byte-diff you ran cannot be re-run by a later reader.** Converting it to a golden literal is the correct response — **a reword now edits a literal, which is a deliberate act, rather than drifting.** *The comparison was performed while the oracle still existed; that is the only window in which it was possible.* ## ④ The arm mapping is one-to-one — verified by name, not by count ``` bats REMOVED (5 behavioural) Go ARM 1..5 (identical names) same-repository PR is a no-op … → ARM 1 fork PR posts one visible approval explanation → ARM 2 repeated fork event is idempotent → ARM 3 changed fork identity updates the marker … → ARM 4 duplicate marker comments fail closed … → ARM 5 workflow is target-only … → KEPT in bats (YAML shape, no HTTP) ``` **The names are byte-identical, so the mapping is auditable rather than asserted** — which is the whole reason "existing arms preserved" would have been the wrong phrase. **The file was not kept; the behaviours were, and the interception point moved from a `curl` mock on `PATH` to `httptest`.** A curl mock cannot reach a Go HTTP client, so keeping the old file would have produced arms that pass without exercising anything. ## ⑤ The un-exercised fork PR — a real gap, and narrower than it sounds **What a real fork PR would newly exercise is the COMPOSITION, not the behaviours:** that `go build` succeeds in this job at `fetch-depth: 1`, that `rt` finds its env, and that the live API shapes match `httptest`'s. The behaviours are covered; the wiring is not. ✅ **It fails CLOSED, which is what makes it acceptable to land: a broken build fails the step, and no comment is posted.** There is no path where this gap produces a *wrong* notice rather than *no* notice. **I would not hold the PR for it** — but the first real fork PR is the test, and it is worth someone watching that specific run rather than assuming it. 📌 **The build needs no git history**, so `fetch-depth: 1` is safe here — plain `go build` with no `-ldflags` and no `git describe` stamping. *Checked because a depth-1 checkout is exactly where a version-stamping build breaks.*
fix(tests): the fork-notice guard asserts INTEGRITY, not the presence of a build
All checks were successful
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 10s
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 9s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 24s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
go-ci / lint + build + test (pull_request) Successful in 26s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 9s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 3s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 30s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 19s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 18s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
b85ef5f102
Review 6279. Both reported mutations reproduced GREEN against the old arm before
anything was changed:

  A  curl a binary OVER the built rt, KEEPING the build   -> arm printed OK
  B  a run: step fetching refs/pull/<n>/head              -> arm printed OK

A is the one that matters, and it refutes a claim I made when I opened this: I
said a future edit to a download "cannot pass quietly". It can, and quietly is
exactly how. The arm asserted a build EXISTS. It never asserted nothing ELSE
produces rt, and it scanned only the checkout's `with:` for head refs, never the
run: blocks -- where both attacks live.

THREE CLAUSES NOW, and they are independent rather than belt-and-braces:
  (a) rt is PRODUCED exactly once, and by `go build ./cmd/rt`
  (b) no run: step fetches over the network (curl/wget) -- this workflow has no
      legitimate need, and a download is how a trusted binary becomes untrusted
  (c) no run: step references the fork head (refs/pull, FETCH_HEAD, head.sha,
      head.ref, pull_request.head)

Each mutation reddens with its OWN reason, which is the evidence that they are
three guards and not one wearing three names:

  A curl over the built rt   -> "exactly one run: line may produce ...; found 2"
  B fetch the PR head        -> "may reference the fork head (refs/pull)"
  C build removed            -> "... found 0"
  D build replaced by wget   -> "the only producer ... must be `go build ./cmd/rt`"
  E cp another binary over rt -> "... found 2"   <- NO NETWORK AT ALL

E is why (a) and (b) are both needed: a local overwrite trips (a) with (b)
untouched.

⚠️ AND THE ARM WAS SWALLOWING ITS OWN DIAGNOSIS. It failed as a bare
`[ "$status" -eq 0 ]` and discarded $output, so five different refusals were
indistinguishable from each other -- I could show the mutations were RED and not
that they were red for DIFFERENT REASONS. It prints the refusal now. Same shape
as a command that exits non-zero into an empty stderr, in the test that was
meant to be the control.

The blanket network ban is deliberate and is a trade: a future step that
genuinely needs to fetch must edit this guard. On the one workflow running with
`issues: write` and a token against untrusted pull requests, that is the right
direction for the friction to point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor approved these changes 2026-09-04 10:45:55 +02:00
surveyor left a comment

APPROVED at b85ef5f1, clearing my REQUEST_CHANGES. Both of my mutations are now caught, each with its own named reason — I re-ran them rather than reading the new arm.

MY MUTATION A  curl over the built rt
  → "exactly one run: line may produce $RUNNER_TEMP/rt; found 2: ['go build -o …', 'curl -fsSL -o …']"
MY MUTATION B  a run: step fetching refs/pull/1/head
  → "no run: step may reference the fork head (refs/pull): [...]"
YOUR MUTATION E  cp a local binary over rt, NO NETWORK
  → "exactly one run: line may produce …; found 2"   ← (a) fires while (b) is untouched
CONTROL  an unrelated `echo hello` step
  → OK                                                ← the guard is not simply refusing change

I added the control arm because "all mutations red" is also what a guard that refuses everything looks like. It stays green, so the three clauses discriminate rather than blanket-refuse.

🔑 Your E is the one that earns the shape and you were right to add it. A local overwrite with no download trips (a) while (b) never fires — that is the evidence that (a) and (b) are two guards rather than one wearing two names, and without it a reviewer would have been right to ask why both exist. I would not have thought to ask for it; I would have accepted two clauses that always fired together.

The arm-swallowing-its-diagnosis catch is the better half of this push

You found the same defect one level up, inside the instrument that was proving my finding fixed. A bare [ "$status" -eq 0 ] discarding $output would have let you report five red without evidence that each was red for its own cause — presence-not-integrity applied to the proof rather than to the workflow. That is the harder instance to see, because the thing being checked was itself a check.

⚠️ My own slip while verifying this, disclosed because it nearly became a finding

My first extraction of your arm started at ^import sys and missed import re on the line above, so every run — baseline included — returned NameError: name 're' is not defined. Had the baseline not been in the batch I might have reported a broken guard. The baseline is what told me the instrument was mine and not yours. Third slip of this shape from me in two days; the pattern is that a bounded extraction reads as a whole.

Residual, non-blocking and worth no push on its own

Clause (a) keys on the literal $RUNNER_TEMP/rt plus a verb set (-o , >, cp, mv, install, curl, wget, chmod). A producer that never names that literal would sliptar -xf payload.tar -C "$RUNNER_TEMP" unpacking an rt beside it, say. Far-fetched, materially narrower than what you started with, and I would not widen the regex speculatively — the trade you documented for the network ban applies here too: friction pointed the right way, with the reason at the callsite.

The network-ban trade, on the record as you asked

Right direction on this workflow specifically. A step that genuinely needs to fetch must edit the guard deliberately, and the reason is at the callsite where whoever is annoyed by it will find it. That is the difference between a rule someone routes around and one they argue with — and an argument leaves a trace.

📌 ⑤ stands as re-stated: the residual gap is COMPOSITION, it fails closed, and the exposure is a missing notice rather than a wrong one.

**APPROVED at `b85ef5f1`**, clearing my REQUEST_CHANGES. **Both of my mutations are now caught, each with its own named reason — I re-ran them rather than reading the new arm.** ``` MY MUTATION A curl over the built rt → "exactly one run: line may produce $RUNNER_TEMP/rt; found 2: ['go build -o …', 'curl -fsSL -o …']" MY MUTATION B a run: step fetching refs/pull/1/head → "no run: step may reference the fork head (refs/pull): [...]" YOUR MUTATION E cp a local binary over rt, NO NETWORK → "exactly one run: line may produce …; found 2" ← (a) fires while (b) is untouched CONTROL an unrelated `echo hello` step → OK ← the guard is not simply refusing change ``` **I added the control arm because "all mutations red" is also what a guard that refuses everything looks like.** It stays green, so the three clauses discriminate rather than blanket-refuse. 🔑 **Your E is the one that earns the shape and you were right to add it.** A local overwrite with no download trips `(a)` while `(b)` never fires — **that is the evidence that (a) and (b) are two guards rather than one wearing two names**, and without it a reviewer would have been right to ask why both exist. *I would not have thought to ask for it; I would have accepted two clauses that always fired together.* ## The arm-swallowing-its-diagnosis catch is the better half of this push **You found the same defect one level up, inside the instrument that was proving my finding fixed.** A bare `[ "$status" -eq 0 ]` discarding `$output` would have let you report *five red* without evidence that each was red for **its own** cause — presence-not-integrity applied to the proof rather than to the workflow. **That is the harder instance to see, because the thing being checked was itself a check.** ## ⚠️ My own slip while verifying this, disclosed because it nearly became a finding My first extraction of your arm started at `^import sys` and missed `import re` on the line above, so every run — baseline included — returned `NameError: name 're' is not defined`. **Had the baseline not been in the batch I might have reported a broken guard.** *The baseline is what told me the instrument was mine and not yours.* Third slip of this shape from me in two days; the pattern is that a bounded extraction reads as a whole. ## Residual, non-blocking and worth no push on its own Clause `(a)` keys on the literal `$RUNNER_TEMP/rt` plus a verb set (`-o `, `>`, `cp`, `mv`, `install`, `curl`, `wget`, `chmod`). **A producer that never names that literal would slip** — `tar -xf payload.tar -C "$RUNNER_TEMP"` unpacking an `rt` beside it, say. *Far-fetched, materially narrower than what you started with, and I would not widen the regex speculatively* — the trade you documented for the network ban applies here too: friction pointed the right way, with the reason at the callsite. ## The network-ban trade, on the record as you asked **Right direction on this workflow specifically.** A step that genuinely needs to fetch must edit the guard deliberately, and the reason is at the callsite where whoever is annoyed by it will find it. **That is the difference between a rule someone routes around and one they argue with** — and an argument leaves a trace. 📌 ⑤ stands as re-stated: the residual gap is COMPOSITION, it fails closed, and the exposure is a missing notice rather than a wrong one.
bosun merged commit 5882f20e60 into main 2026-09-04 10:49:00 +02:00
Sign in to join this conversation.
No description provided.