Skip to content

Take the ruleset bypass list out of the fleet config entirely - #545

Merged
ptr727 merged 1 commit into
developfrom
ruleset-bypass-clean
Aug 3, 2026
Merged

Take the ruleset bypass list out of the fleet config entirely#545
ptr727 merged 1 commit into
developfrom
ruleset-bypass-clean

Conversation

@ptr727

@ptr727 ptr727 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Who may bypass a ruleset decides who the rules do not apply to. That is a per-repository human decision, not a value the fleet config hands out.

All three payloads declared bypass_actors: [{ actor_id: 5, actor_type: RepositoryRole, bypass_mode: always }], so applying them granted repository admins a standing exemption on main and develop in every repo. Verified live: every fleet repo sampled carries exactly that entry on both branches.

It was a real grant, not a restatement

GitHub's documentation is explicit that a ruleset applies to administrators by default and that the bypass list starts empty. Admins are eligible for bypass and must be added. The maintainer's UI screenshot confirms the same from the other side: the entry offers a Delete bypass action, so it is a deliberate list member rather than an inherent property of the role.

Whether these repos first got it from this config or from hand-made rulesets that predate the tooling is unknown, and it does not matter. The config should not be the thing maintaining it either way.

Removing the declaration alone would have made it worse

apply sends the payload with gh api --method PUT, which replaces the whole document. A payload with no bypass_actors would therefore have cleared the live list on every run. That is code deleting a bypass, which is the specific thing that must never happen.

So apply now reads the live list and writes it back unchanged, and aborts rather than proceeding if that read fails, since applying without it would silently clear what it was meant to preserve. On create there is no live list and none is sent, so a new ruleset starts at GitHub's own empty default and nothing is deleted, because nothing existed.

check reports and never asserts

No payload declares a bypass value, so there is nothing to compare against, and inventing an expectation would put code back in charge of the decision. check now prints the live list on every run, the way it already prints the secrets manual-verify note:

ruleset 'develop' bypass list: RepositoryRole 5 always (not managed by this script)

Visible on every run, judged by nothing.

Verification

The preserve path was proven by composing the PUT body against this repo's live ruleset without sending it:

live bypass:                  [{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"always"}]
payload declares:             ABSENT
composed PUT body would send: [{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"always"}]

bash -n clean, prose_lint clean, editorconfig clean, live read-only check against this repo passes.

What this does not do

It does not remove the bypass entries that already exist on the fleet's rulesets. Removing those would itself be code deleting a bypass. Clearing them is a deliberate per-repo act in the UI, and check now makes each one visible so they can be reviewed rather than forgotten.

Who may bypass a ruleset decides who the rules do not apply to, so it is a
human decision taken per repository rather than a value the fleet config hands
out. The three payloads declared RepositoryRole 5 with bypass_mode always, and
applying them granted repository admins a standing exemption on main and
develop across every repo.

The docs are explicit that a ruleset applies to administrators by default and
that the bypass list starts empty, so the declaration was a real grant rather
than a restatement of an inherent privilege. Whether these repos first got it
from the config or from earlier hand-made rulesets is unknown, and it does not
matter: the config should not be the thing maintaining it either way.

Removing the declaration alone would have made this worse. apply sends the
payload as a PUT, which replaces the whole document, so a payload with no
bypass_actors would clear the live list on every run. That is code deleting a
bypass, which is the one thing that must never happen here. apply now reads the
live list and writes it back unchanged, and aborts rather than proceeding if
that read fails, since applying without it would silently clear what it was
meant to preserve. On create there is no live list to read and none is sent, so
a new ruleset starts at GitHub's own empty default.

check reports the list on every run and asserts nothing about it, because no
payload declares a value to compare against and inventing one would put code
back in charge of the decision.

Verified by composing the PUT body against this repo's live ruleset without
sending it: the payload declares nothing, the live list is RepositoryRole 5
always, and the composed body carries that same list back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 22:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes bypass_actors from the fleet ruleset payloads so bypass permissions remain a per-repository human decision, while updating the config script to preserve any existing live bypass list during ruleset updates and to report (but not assert) the live bypass list during check.

Changes:

  • Removed bypass_actors from the main and develop ruleset payload JSON files.
  • Updated apply_ruleset to read the live bypass_actors list on update and write it back unchanged to avoid accidental clearing via PUT replacement semantics.
  • Updated check_ruleset to print the live bypass list each run without enforcing it against the payload.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
repo-config/operational/develop.json Removes bypass list from operational develop ruleset payload.
repo-config/main.json Removes bypass list from main ruleset payload.
repo-config/develop.json Removes bypass list from release develop ruleset payload.
repo-config/configure.sh Preserves live bypass list on ruleset update; reports bypass list during check without asserting it.

@ptr727
ptr727 merged commit 90e3255 into develop Aug 3, 2026
7 checks passed
@ptr727
ptr727 deleted the ruleset-bypass-clean branch August 3, 2026 22:28
ptr727 added a commit that referenced this pull request Aug 3, 2026
Three commits. Merged as a **merge commit**, never squashed, per the
branching model.

| Commit | PR | What |
| --- | --- | --- |
| `e48d64d` | #544 | Regenerates the divergence ledger against the
previously promoted `main` |
| `90e3255` | #545 | Takes the ruleset bypass list out of the fleet
config entirely |
| `362aec8` | #546 | Queues the agent-isolation rule and the
vendored-tooling surface |

## The one that matters

**#545 is a behaviour change to `configure.sh`, not a doc edit.** All
three ruleset payloads declared `bypass_actors: [{ actor_id: 5,
actor_type: RepositoryRole, bypass_mode: always }]`, so applying them
granted repository admins a standing exemption on `main` and `develop`
in every repo.

GitHub's documentation is explicit that a ruleset applies to
administrators by default and that the bypass list starts empty. Admins
are *eligible* for bypass and must be added. So the declaration was a
real grant rather than a restatement of something inherent, and the
fleet config was the thing handing it out.

Removing the declaration alone would have been worse than leaving it.
`apply` sends the payload as a `PUT`, which replaces the whole document,
so a payload with no `bypass_actors` would have **cleared the live list
on every run**. That is code deleting a bypass. `apply` now reads the
live list and writes it back unchanged, and aborts rather than
proceeding if that read fails. On create it sends none, so a new ruleset
starts at GitHub's own empty default and nothing is deleted, because
nothing existed.

`check` reports the list on every run and asserts nothing, since no
payload declares a value to compare against and inventing one would put
code back in charge of a human decision.

This does **not** remove the bypass entries already live on the fleet's
rulesets. Removing those would itself be code deleting a bypass. They
are now visible on every `check` run so they can be cleared
deliberately, per repo, in the UI.

## Expected consequence, stated rather than discovered

`repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"`,
so this promotion makes every repo holding a copy stale against the hub.
That is the third re-vendor this file has generated in one session,
which is exactly the evidence the vendored-tooling entry added in #546
now records.

The follow-up plan is to fold that re-vendor into the payload migration
already owed by five repos, so each gets one visit that renames its
`ruleset-*.json` payloads and takes the current script, rather than two.

## Verification

`spec/validate.py` OK (21 cataloged), diff-scoped `prose_lint` clean,
`markdownlint-cli2` clean, editorconfig clean, live read-only `check`
against this repo passes with the bypass list reported and unasserted.
The preserve path was proven by composing the `PUT` body against this
repo's live ruleset without sending it: the payload declares nothing,
the live list is `RepositoryRole 5 always`, and the composed body
carries that same list back.
ptr727 added a commit that referenced this pull request Aug 3, 2026
Blog was stood up on 2026-08-01 and cut release `1.0.11` the same day,
but it was never added to `registry/repos.json`. Every hub tool is
registry-driven, so `spec/audit.py` and `spec/fidelity_honesty.py` had
never measured it, and `reports/divergences.md` was under-reporting the
fleet by exactly one repo while reading as complete. This catalogs it
and lands its first audit.

## The entry records what ran, not what was predicted

`reports/conformance-matrix.md` already named Blog as the reference repo
for the `source-only` + `release` shape, and predicted `releaseTrigger:
none` with an empty `publish[]` until a deploy ran. What actually
shipped is a dispatch-only publisher that cut a tag and a source
archive, so the entry is `dispatch-only` with the GitHub release
declared, and the matrix row is corrected to match. The VPS deploy still
has not run, so the static-site type stays deferred to #456 and
`publish[]` gets revisited when it does.

## What the audit found

Nine findings, no defects, verdict **operational**. Every one is the hub
advancing after the carry rather than the repo regressing:

- **Eight verbatim units behind the canonical.** Six are stale copies.
Two never arrived at all, `AGENTS.md > Fleet Bootstrap` and
`GOVERNANCE.md > Representative Data in Agent-Authored Text`, and that
distinction matters because a stale copy still states the rule in an
older form while an absent one states nothing.
- **One undeclared section**, `AGENTS.md > Project Conventions`, whose
four rules are genuinely repo-specific and duplicate no verbatim
section.

The convergence PRs against Blog follow separately, one per drift class
per AUDIT.md section 10.

## Measured against `main`, with `develop` recorded separately

The run reads hub `main` (`3a7cc64`) per AUDIT.md section 1. A re-run
from a tree at `develop` (`362aec8`) reports two more re-vendors and two
DEFECTs, all four from #545 taking `bypass_actors` out of the ruleset
payloads an hour earlier. Those are recorded in the report rather than
counted, because measuring against un-promoted hub content reports work
in flight as a conformance failure. The same four will appear against
every fleet repo when that promotes, and the live-ruleset half is a
settings change on a protected branch, so it is yours to apply rather
than an agent's.

## Two spec questions, raised rather than resolved

1. **The hub contradicts itself on whether a repo may carry its own
`AGENTS.md` section.** `AGENTS.md` says a project's own conventions live
in that project's `AGENTS.md`. `spec/section-model.md` says an
undeclared section is drift to reconcile and lists four destinations,
none of them `AGENTS.md`. A repo following the one is flagged for
violating the other, and Blog is exactly that case.
2. **The character-set tiers do not classify the COPYRIGHT SIGN**, which
any repo generating a feed or a rendered document will hit.

A third, smaller one: the report directory here is `reports/blog/`,
lowercase, matching every existing report directory rather than the
repo's CamelCase name. AUDIT.md section 8 writes it as
`reports/<repo>/audit.md`, which reads as exact case, and the two have
quietly disagreed since the first report. Worth settling in the wording,
and worth settling before someone clones on a case-insensitive
filesystem.

## Verification

- `python3 spec/validate.py`: 22 cataloged, 0 backlog, classifying
cleanly.
- `python3 spec/audit.py Blog` from a tree at hub `main`: 9 drift, 0
defect/letter/error.
- `python3 spec/fidelity_honesty.py --report`: regenerated, and the
whole diff is Blog appearing in the rows it belongs in.
- `python3 scripts/prose_lint.py reports/blog/audit.md`: clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 3, 2026
…551)

Regression fix. #545 removed `bypass_actors` from the ruleset payloads,
and **every repo in the fleet has been reporting two ruleset DEFECTs
since it promoted**.

## What broke

`spec/audit.py` compares the live ruleset against the payload over a
fixed subset, and that subset included `bypass_actors`. A payload that
deliberately declares no bypass list, against a live ruleset that still
has one, is a normalized diff — so the audit called it divergence:

```text
== Utilities (csharp, nuget; release) @ main@e179288 ==
  DEFECT ruleset: develop diverges from repo-config/develop.json (normalized diff)
  DEFECT ruleset: main diverges from repo-config/main.json (normalized diff)
```

Reproduced on Utilities, PlexCleaner and Blog. It is every repo, because
every live ruleset carries the entry the payloads stopped declaring.

## The fix, and why it is this way round

The field is dropped from the compared subset rather than restored to
the payloads.

Who may bypass a ruleset is a per-repository human decision taken in the
UI, and `configure.sh` already treats it that way after #545: `apply`
writes the live list back unchanged, `check` reports it without
asserting. The audit asserting the same field contradicted that
directly.

**The defect was two tools comparing one field under opposite policies,
not the field's value.** The comment says so and records what happened
when it was not, because the next person to look at that list will
otherwise put it back.

## How it was found

While reviewing the Blog registration in #547. That report claims nine
drift and no defects, which was true when it was written against hub
`main` at `3a7cc64`, and stopped being true an hour later when #545
promoted. The PR body had even predicted it: "The same four will appear
against every fleet repo when that promotes."

It did. This is the same staleness the audit's own run-stamp rule exists
to expose, arriving from the hub's side rather than the repo's.

## Verification

- Utilities: 2 ruleset DEFECTs before, **0** after. Its remaining two
findings are LETTER results for absent `GOVERNANCE.md` and
`OPERATIONS.md`, which are the real propagation gap rather than this
regression.
- PlexCleaner: same, 2 before and 0 after.
- `spec/validate.py` OK, diff-scoped `prose_lint` clean, editorconfig
clean, syntax checked.

## Follow-on

#547's `reports/blog/audit.md` should be regenerated against this
corrected tool before it merges, so the committed report matches what
the audit now reports rather than what it reported an hour before the
payload change landed.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants