fix(action): classify gh's real denial stderr as contradicted, not unavailable - #111
Merged
Merged
Conversation
SollanSystems
added a commit
that referenced
this pull request
Jul 30, 2026
Collects slice 4a (#106) and slice 4b (#110, #111, #112): loop verdict, opt-in keyless attestation, --compare, replay-based chain ancestry, anchor@1, signer trust, and the head-bearing subject. Neither slice shipped on its own; both sat in Unreleased and go out under one number. Beyond the version surfaces, this cut carries corrections a mechanical bump would not have made. The changelog said the predicate is handed to actions/attest as a subject-path. It is handed as predicate-path, alongside a SEPARATE subject file, and §23 says outright that the predicate bytes are deliberately not the subject — doctor validation_mode and tool.version live inside the predicate, so the same run projects different bytes in different environments. A reader following that sentence would hash the wrong file and fail every verification. "the three attestations minted before this release": the count was right, the scope word was wrong. attest.yml shipped inside this same window, so nothing predates the release; the three carrying the retired subject form are the pushes through c493804. Re-scoped to match what attest.yml and reference §24 already said. ADR 0002 decision 6 is withdrawn, in a new dated amendment. It described code-owner review as in force once the ruleset required it. The ruleset cannot require it: one collaborator, no self-approval, bypass_actors empty, current_user_can_bypass never, and no classic protection underneath. Flipping it would invert the intent rather than merely fail — maintainer-authored pull requests become unmergeable while bot-authored ones become gated, and agent work here lands under the maintainer's account. CODEOWNERS is demoted in place to what it honestly is, a record of the gate-defining surface, across the ADR, the changelog, reference §23 and §24, and the file's own header. test_docs_version.py's changelog check now ADDS 0.12.0 and keeps 0.11.0, and restores 0.10.0 — the previous cut replaced its predecessor's assertion instead of adding to it, leaving that heading with no cover. Baselines, live checkout: 1577 passed / 16 skipped with pyyaml+jsonschema+pytest, 1478 / 115 with pyyaml+pytest. Both legs +7 against pre-cut, which is the CLI guard's new tests plus the retired version-bump pin offset by the new code-owner pin. CI additionally installs hypothesis, which moves the number.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The same-day correction slice 4b's plan mandated, closing the M2 fixture pair with a real
vendor string instead of a paraphrase. Follows #110.
What the post-merge experiment falsified
#110's first
attest.ymlrun (30509952627)succeeded on the first attempt, and the D1 prediction held both ways —
subject[0].digest.sha256(
6530115e…) is notpredicate.chain.head(3f0aa6d5…) and issha256(<the 64 head bytes>). The pinned certificate claim names were right too: all three ofREQUIRED_CERTIFICATE_CLAIMSare present, and both_TRIGGER_CLAIM_ALIASES(
githubWorkflowTriggerandbuildTrigger) are present, so the fail-closed"at least one" rule was satisfied twice over. No correction needed there.
One thing was wrong. Now that a verifiable attestation exists, the denial-shape stderr
could finally be captured — it could not be before, because no attestation this repo had
minted was verifiable at all. Verbatim, from live
ghwith a deliberately wrong--signer-workflow:No marker in
_CONTRADICTED_MARKERSmatched it. The classifier's fail-closed defaulttherefore reported the most common denial as
anchor_attestation_unavailable.Why this matters, and why it is not a security hole
Both outcomes are non-promoting and both exit 1, so nothing was ever promoted that should
not have been — the fail-closed default did its job. What broke is D5's observability
distinction, in the more misleading direction: "I looked and it said no" was being
reported as "I could not look." An operator reading that log would go looking for an
index outage instead of an unexpected signer.
The pre-merge marker set was a remembered approximation of a vendor string. That is exactly
the failure mode the plan's M2 fixture requirement exists to catch — "a test author's
remembered approximation of a vendor message is exactly the thing that passes review and
fails in production" — and it is why the denial fixture was scheduled for capture the
moment it became possible.
The fix
"verifying with issuer"added to_CONTRADICTED_MARKERS, ahead of the pre-mergeguesses. Ordering is unchanged and still safe: the "nothing found / transport" markers are
checked first, so a 404 cannot be misread as a denial.
scripts/fixtures/gh_attestation_verify/signer_denied.txt— the 45-byte verbatim capture.test_resolve_classifies_the_real_captured_denial_stderrdrives the classifier from thatfile and asserts
contradicted. Verified to fail with the marker removed, so it is nota vacuous pin.
The M2 fixture pair is now complete: both the 404 branch and the denial branch are pinned
against real captured
ghoutput.Suite (fresh tmpfs worktree): 1568 passed / 18 skipped canonical
(
pyyaml+jsonschema+pytest), 1469 passed / 117 skipped structural-fallback.scripts/test_action_anchor_resolve.py24 passed. No version bump.