Skip to content

Commit 19d1380

Browse files
rmednitzerclaude
andcommitted
docs: close F12 with the verified branch-protection contents
F12 asked four questions about `main` protection across the fleet and has been open since the 2026-05-27 engagement because the contents were not reachable: only the `protected` boolean was. They are now read directly, via an authenticated gh api from the Vertex control plane, which is not subject to the egress policy that blocks api.github.com from the session that raised this. Both reads were read-only GETs recorded in the MCP broker log (op-9a17fbde789036a9d16f658b, op-cfb3cb50b92ecd2341d0fc31). The detail that made this hard to close, recorded first because it inverts the obvious reading: protection is implemented as RULESETS, not legacy branch protection. GET /repos/{owner}/{repo}/branches/main/protection returns 404 on all eleven repositories. Taken alone that 404 says "main is unprotected", which is the opposite of the truth. The four answers: no admin bypass verified. bypass_actors is [] on all eleven rulesets. Nobody bypasses, including the owner. required review not required anywhere. required_approving_review_count 0 and require_code_owner_review false on all eleven. A pull request is required; an approval is not. The fleet-wide CODEOWNERS files are therefore advisory, assigning a reviewer without gating the merge. Deliberate single-operator posture, now stated rather than assumed from the files' presence. required checks present on ten of eleven, listed per repository. ai-stack has no required_status_checks rule at all. signed commits not required anywhere. No required_signatures rule. Also now on the record: every ruleset carries deletion + non_fast_forward, so force-push and deletion of main are blocked server-side on all eleven. That is the real control behind the .claude/settings.json force-push denies landed earlier today, which are a client-side convenience by comparison. Two contexts in infra's own required list are not job names and are called out so a future reader does not prune them as stale: `Trivy` is the code-scanning check from the upload-sarif step in ci.yml (category trivy-config), and `CodeQL` / `Analyze (actions)` come from CodeQL default setup, which has no workflow file in the repository. All sixteen reported on PR #62, which merged under strict policy with an empty bypass list, so none is a phantom. Matrix contexts are correctly expanded on automation, relay-shell and 6dof-ascent-sim. A required check named for an unexpanded matrix expression would block merges forever; noted because the failure mode is silent and the correct state is invisible. The one gap, ai-stack, is carried forward as F13 rather than held inside F12. Its Renovate path is already mitigated by platformAutomerge:false in ai-stack/renovate.json5; the residual is that repository-level allow_auto_merge:true lets a manually enabled auto-merge land an ordinary PR with no CI gate. F13 records that the obvious fix, a trivial always-green job, does not work: it would convert the gap into the appearance of a gate. The effective shape is the ci-success aggregate that agents and aiops-mcp already require, which means restructuring lint.yaml and is the maintainer's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdProB4ZJuSaD8WZ84Sdy8
1 parent c46923c commit 19d1380

1 file changed

Lines changed: 116 additions & 1 deletion

File tree

BACKLOG.md

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ moving it to **Resolved**.
1010

1111
| Id | Item | Origin | Why deferred | Next step |
1212
|----|------|--------|--------------|-----------|
13-
| F12 | Verify the *contents* of `main` branch protection (which checks are required, required review, no admin bypass, signed commits if intended) | [audit/2026-05-27-engagement.md](audit/2026-05-27-engagement.md) §8.1 (F12) | The branch-protection contents are still not exposed by any available tool; only the `protected` boolean is. Needs the repo-admin API/UI | An admin confirms the required-checks list and bypass posture, and records them under "F12 evidence" below |
13+
| F13 | Give `ai-stack` a requireable aggregate status check, so the repository is not the one fleet member where a merge can land with no CI | F12 evidence 2026-08-13 (this file) | `ai-stack` is the only fleet repository with no `required_status_checks` rule, because every PR-triggered workflow is `paths`-filtered. The Renovate path is already mitigated by `platformAutomerge: false` in `ai-stack/renovate.json5`; the residual is that repository-level `allow_auto_merge: true` lets a manually enabled auto-merge land an ordinary PR with no CI gate. Fixing it properly means restructuring `lint.yaml`, which is a design change to the repository's primary CI and needs the maintainer's call | Adopt the `ci-success` aggregate pattern already used by `agents` and `aiops-mcp`: drop the workflow-level `paths` filter, add a `changes` job that computes changed paths, gate each existing job on it with `if:`, and finish with an `if: always()` aggregate that fails when any non-skipped job failed. Then require that one context on ruleset 15857143 |
1414

1515
### F12 evidence (2026-08-12)
1616

@@ -59,9 +59,124 @@ Also unverified from here, and worth recording when an admin next has the UI
5959
open: whether the ruleset bypass list is empty, and whether **Allow auto-merge**
6060
is enabled per repository, since `platformAutomerge` is a no-op without it.
6161

62+
### F12 evidence (2026-08-13) — contents verified, F12 closed
63+
64+
Read with an authenticated `gh api` from the Vertex control plane, which is not
65+
subject to the egress policy that blocked `api.github.com` from earlier
66+
sessions. Two read-only operations, both recorded in the MCP broker log:
67+
`op-9a17fbde789036a9d16f658b` (protection endpoint, rulesets index, repo flags)
68+
and `op-cfb3cb50b92ecd2341d0fc31` (ruleset contents).
69+
70+
**Protection is implemented as rulesets, not legacy branch protection.** This is
71+
the detail that made F12 hard to close and is the first thing to know when
72+
re-verifying: `GET /repos/{owner}/{repo}/branches/main/protection` returns
73+
**404 on all eleven repositories**. Read alone, that 404 reads as "`main` is
74+
unprotected", which is the opposite of the truth. The protection lives at
75+
`GET /repos/{owner}/{repo}/rulesets/{id}`.
76+
77+
#### The four questions F12 asked
78+
79+
| Question | Answer | Confidence |
80+
|---|---|---|
81+
| No admin bypass? | **Yes, verified.** `bypass_actors` is `[]` on all eleven rulesets. Nobody bypasses, including the owner | Direct read |
82+
| Required review? | **Not required anywhere.** `required_approving_review_count: 0` and `require_code_owner_review: false` on all eleven. A pull request *is* required (direct push to `main` is blocked), but no approval is | Direct read |
83+
| Required checks? | **Configured on ten of eleven.** `ai-stack` has no `required_status_checks` rule at all. Full list below | Direct read |
84+
| Signed commits? | **Not required anywhere.** No `required_signatures` rule on any of the eleven | Direct read |
85+
86+
`CODEOWNERS` files exist fleet-wide but are advisory: with
87+
`require_code_owner_review: false` they assign a reviewer and do not gate the
88+
merge. That is a deliberate single-operator posture, not an oversight, but it
89+
should be stated rather than assumed from the file's presence.
90+
91+
#### Per-repository
92+
93+
Every ruleset is `enforcement: active`, targets `branch`, and carries
94+
`deletion` + `non_fast_forward` (so branch deletion and force-push to `main`
95+
are blocked server-side) plus a `pull_request` rule. All except
96+
`renovate-config` also carry `required_linear_history`.
97+
98+
| Repository | Ruleset | Required status checks | `allow_auto_merge` |
99+
|---|---|---|---|
100+
| `infra` | `main-protection` (15857150), strict | 16: `Format Check`, `Lint`, `Pre-commit (hygiene + EditorConfig)`, `Secret Scan (gitleaks)`, `Security Scan (Trivy)`, `Trivy`, `CodeQL`, `Analyze (actions)`, `Module Tests (modules/{libvirt-vm,talos-cluster})`, `Validate (environments/{lab,production,talos-lab})`, `Validate (modules/{libvirt-vm,talos-cluster})`, `Validate renovate preset` | true |
101+
| `agents` | `main-protection` (17307994) | `lint`, `type-check`, `ci-success` | true |
102+
| `relay-shell` | `main-protection` (17307996) | `check (py3.12)`, `check (py3.13)`, `check (py3.14)`, `gitleaks (secret scan)` | true |
103+
| `runbooks` | `main-protection` (15857152) | `pre-commit (shellcheck + shfmt + hygiene)`, `bats (script behaviour tests)`, `secret scan (gitleaks)` | true |
104+
| `automation` | `main-protection` (15857151) | `Lint`, `Syntax Check`, `Pre-commit (hygiene + EditorConfig)`, `Secret Scan (gitleaks)`, `Molecule (common)`, `Molecule (users)`, `Molecule (ssh_hardening)`, `Molecule (auditd)` | true |
105+
| `ai-stack` | `main-protection` (15857143) | **none** | true |
106+
| `6dof-ascent-sim` | `main-protection` (15857138) | `lint`, `typecheck`, `test (3.11)`, `test (3.12)`, `test (3.13)` | true |
107+
| `rmednitzer.github.io` | `main-protection` (15857146) | `validate` | true |
108+
| `core-graph` | `main-protection` (15857142) | `python-lint`, `typecheck`, `python-test`, `schema-and-rls-test`, `policy-test`, `secret-scan`, `lockfile-check`, `actionlint` | true |
109+
| `aiops-mcp` | `main-protection` (17385194) | `ci-success` | true |
110+
| `renovate-config` | `protect-default-branch` (20737544), strict | `Validate re-export` | **false** |
111+
112+
`infra` and `renovate-config` set
113+
`strict_required_status_checks_policy: true` (branch must be current with
114+
`main` before merge); the other nine set it false.
115+
116+
Two of `infra`'s sixteen contexts are not job names and are worth recording so
117+
a future reader does not delete them as stale: `Trivy` is the code-scanning
118+
check produced by the `upload-sarif` step in `ci.yml` (category
119+
`trivy-config`), and `CodeQL` / `Analyze (actions)` come from CodeQL **default
120+
setup**, which has no workflow file in the repository. All sixteen reported on
121+
PR #62, which merged under the strict policy with an empty bypass list, so none
122+
of them is a permanently-blocking phantom context.
123+
124+
**Matrix contexts are correctly expanded.** A required check named for an
125+
unexpanded matrix expression (`Molecule (${{ matrix.role }})`) would never
126+
match a reported check and would block merges forever. `automation`,
127+
`relay-shell` and `6dof-ascent-sim` all name the expanded values. No action
128+
needed; noted because the failure mode is silent and the fix is invisible once
129+
correct.
130+
131+
#### The one gap: `ai-stack`
132+
133+
`ai-stack` is the single repository where the preset's "automerge low-risk
134+
updates once required checks pass" property is **not** backed by a required
135+
check, which is exactly the case the 2026-08-12 evidence above predicted would
136+
matter. It is also the one repository where that is a deliberate design:
137+
138+
- Every PR-triggered workflow (`docs.yaml`, `lint.yaml`,
139+
`sync-image-artifacts.yml`) carries a `paths` filter, so no check is
140+
guaranteed to run and any required check would permanently block a PR that
141+
touches none of those paths.
142+
- The live mitigation is in `ai-stack/renovate.json5`, which overrides
143+
`platformAutomerge: false` for minor, patch, digest, pin and pinDigest with a
144+
comment naming this exact hazard. Renovate therefore falls back to evaluating
145+
the checks that did report, rather than handing the merge to GitHub.
146+
147+
So the Renovate path is covered. **The residual is the non-Renovate path**: with
148+
`allow_auto_merge: true` at the repository level and no required check, anyone
149+
enabling GitHub auto-merge on an ordinary `ai-stack` PR lands it with no CI
150+
gate at all, subject only to `required_review_thread_resolution`. That is
151+
tracked as **F13** below rather than held inside F12.
152+
153+
One trap worth recording, because it is the obvious fix and it does not work:
154+
adding a trivial unconditional job that always exits 0 and requiring it gives
155+
native auto-merge something to wait on, but that check goes green in seconds
156+
regardless of the diff, so auto-merge still lands the PR without any chart
157+
validation having run. It converts the gap into the appearance of a gate. The
158+
effective shape is an **aggregate** check that is itself conditional on the
159+
real jobs, which is what `agents` and `aiops-mcp` already require as
160+
`ci-success`, and it means restructuring `lint.yaml` rather than adding a
161+
file beside it.
162+
163+
#### Other observations, recorded not actioned
164+
165+
- **Signed commits are not required on any repository.** F12 asked about this
166+
conditionally ("if intended"). Recorded as a decision, not a finding: adopting
167+
it fleet-wide is a separate change with key-management consequences.
168+
- **`renovate-config` lacks `required_linear_history`** while the other ten
169+
carry it, and it is the only repository with `allow_auto_merge: false` and
170+
`delete_branch_on_merge: false`. The auto-merge setting is consistent with its
171+
deliberate no-automerge posture; the linear-history difference looks like
172+
drift from it having been created later (2026-08-12) than the others.
173+
- **`allow_auto_merge: true` on ten repositories** confirms `platformAutomerge`
174+
is not a no-op there, which the 2026-08-12 evidence listed as unverified.
175+
62176
## Resolved
63177

64178
| Id | Item | Origin | Resolved by |
65179
|----|------|--------|-------------|
180+
| F12 | Verify the *contents* of `main` branch protection (which checks are required, required review, no admin bypass, signed commits if intended) | [audit/2026-05-27-engagement.md](audit/2026-05-27-engagement.md) §8.1 (F12) | "F12 evidence (2026-08-13)" above. All four questions answered by direct read of the eleven rulesets: bypass lists empty, no required approvals, required checks present on ten of eleven, signed commits not required. The `ai-stack` exception is carried forward as F13 |
66181
| BL-1 | Execute the libvirt 0.9.x migration evaluation gates against a real lab host, then author the successor pin-bump ADR | [ADR-0009](docs/adr/0009-begin-libvirt-0.9-migration-evaluation.md) gates 2–5; [ADR-0012](docs/adr/0012-libvirt-0.9-schema-diff-inventory.md) | [ADR-0016](docs/adr/0016-migrate-libvirt-provider-to-0.9.md) (PR #29, merged 2026-06-04): gates 2–5 host-verified by the maintainer, pin bumped to `~> 0.9.0`, ADR-0002/0009/0012 superseded |
67182
| BL-2 | Evaluate `siderolabs/talos` write-only secret arguments (`client_configuration_wo`, `machine_configuration_input_wo`) to keep rendered machine config out of state | audit 2026-05-31 | [ADR-0017](docs/adr/0017-adopt-talos-write-only-secret-arguments.md) (PR #38, 2026-06-09): adopted on `talos_machine_configuration_apply` + `talos_machine_bootstrap`; outcome noted in [ADR-0014](docs/adr/0014-pin-siderolabs-talos-provider.md) |

0 commit comments

Comments
 (0)