Skip to content

sec(gate): assert the detector floor has not fallen behind the loaded list (BACKLOG #1368, SEC-04) - #631

Open
wshallwshall wants to merge 1 commit into
mainfrom
claude/builder-2-sec04-detector-floor
Open

sec(gate): assert the detector floor has not fallen behind the loaded list (BACKLOG #1368, SEC-04)#631
wshallwshall wants to merge 1 commit into
mainfrom
claude/builder-2-sec04-detector-floor

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Built by Builder 2. Landed by the Lander. Four files, +261/-1. Zero files under messagefoundry/.

The gap

MEFOR_MIN_DETECTORS catches the token list dropping below the floor. Nothing asked whether the floor had fallen behind the list -- and a floor of 7 against a list of 40 is satisfied by any 7 detectors surviving, so the gate goes green while guarding almost nothing. New --assert-floor-fresh mode, wired into the forbidden-content job.

The spec deviation, ruled

The master test plan's SEC-04 row specifies a flat 80% ratio. This ships a different rule, deliberately, and the owner has now ruled in favour of it: absolute small-N, ratio above.

The literal spec would have reddened the gate on its first real run with nothing wrong. Measured against the live list: names 87.5% and estate 92.9% pass a ratio; site_prefixes is 1 of 2, which is 50%, and fails one. Growth from 1 to 2 always scores 50%, so at that size a ratio measures the section's size rather than the floor's staleness.

Constants read from the commit rather than from the handover:

scan_forbidden.py:1033   _SMALL_SECTION_MAX = 4
scan_forbidden.py:1036   _MAX_ABSOLUTE_LAG  = 1
scan_forbidden.py:1038   _MIN_FLOOR_RATIO   = 0.8
scan_forbidden.py:1066   if loaded <= _SMALL_SECTION_MAX:   <- the absolute arm, small-N only

The author flagged one ambiguity in the relayed wording -- absolute everywhere versus absolute-for-small-N with the ratio kept above it. They shipped the latter, which is what the Dispatcher approved and what the arithmetic argues for. The code's own comment at :1051 states it: "TWO RULES, BECAUSE ONE SHAPE DOES NOT FIT BOTH SIZES." If the owner meant the former it is a one-constant change and does not warrant holding the branch.

No floor is raised here

The live list is inside the shipped rule today. The author declined to raise a security threshold on the strength of one day's list, and says plainly that nobody should inherit that decision from a builder. Raising it stays a separate, owner-facing call.

Two safety properties, checked

The new step is guarded on MEFOR_MIN_DETECTORS, not on the secret. Only the secret-present branch exports it, so fork PRs and structural-only runs skip it. It must not run against the synthetic example set, which populates every section and would fail freshness on a contributor's machine.

No token content anywhere -- code, tests, ledger row or commit message. Every figure is a count or a mode marker, both of which the gate already prints to CI logs. I checked the diff for literals independently: zero hits.

Lander verification

ahead of origin/main    1        behind    1 (the codeql merge_group fix)
merge-tree vs main      exit 0 (clean)
diff                    4 files, 261 insertions(+), 1 deletion(-)
files under messagefoundry/    0
'+## 1368' in the ledger diff  1

Before the push, git branch -r --contains 136c11ba8 named only the private rescue ref. Two remotes, and a count-based check reads that as already pushed.

Author's verification: ruff clean, 50 tests passing, 38 definitions with no duplicate names, 9 new tests confirmed present by name, the assertion run end-to-end against the real floors (exit 0) and shown to fail on three separate drift shapes each naming its own section.

Deliberately not wired

branch-leak-scan.yml pins the same floor and is untouched. Same rule, different job, and it deserves its own decision rather than being swept in.

Not armed

This touches a security workflow. Required approvals are 0.

Generated with Claude Code

@wshallwshall
wshallwshall force-pushed the claude/builder-2-sec04-detector-floor branch from 136c11b to 108c572 Compare August 27, 2026 08:44
…n list (BACKLOG #1368, SEC-04)


`MEFOR_MIN_DETECTORS` is a per-section FLOOR and `token_floor_failure` fails when the
token list drops below it. NOTHING ASKED THE OPPOSITE QUESTION. A floor of 7 against a
list of 40 is satisfied by any 7 detectors surviving, so the gate stays green while
constraining almost nothing. The counts have been printed on every run all along, and
nothing has ever compared them to the floor -- which is why the drift is silent rather
than merely unfixed.

Measured against the live list (counts only; the list is a secret and appears nowhere):

    section         floor  live   floor/live
    names           7      8       87.5%
    estate          13     14      92.9%
    site_prefixes   1      2       50.0%   <- would FAIL a flat 80% rule

THE SPEC'S 80% RATIO IS THE WRONG SHAPE AT SMALL N, AND BUILDING IT IS HOW THAT SURFACED.
Growth from 1 to 2 scores 50% however healthy it is, so at that size a ratio measures the
SECTION'S SIZE rather than the floor's staleness -- the rule as specified would have redded
the gate on its first real run with nothing wrong. Shipped instead: a ratio above four
loaded detectors, an absolute lag of at most one at or below four. Both arms fire on their
own section and name both numbers. The deviation from the master test plan is routed to the
owner via the Liaison rather than shipped quietly, and nothing here raises any floor -- the
live list is within the rule today, and if it later drifts the gate says so and a human
decides.

THE COUNTS CANNOT TELL YOU WHICH TABLE YOU MEASURED. The SYNTHETIC example set and the REAL
list both report 8/14/2 -- three identical numbers from two tables, one of which matches
nothing real. Anyone verifying a floor from counts alone would assert it against a set that
cannot fire and read the pass as evidence. I made exactly that error an hour before finding
it, assuming my own local run was synthetic; the scanner's mode marker is what settled it.
So `--print-detector-counts` emits `mode=` FIRST, and a test pins that the count lines are
byte-identical between synthetic and real -- if they ever differ, that test has stopped
demonstrating the hazard.

THE NEW CHECK IS GUARDED ON THE FLOOR VARIABLE, NOT ON THE SECRET. Only the secret-present
branch exports `MEFOR_MIN_DETECTORS`, so fork PRs and structural-only runs skip it entirely.
It must not run against the synthetic set: that set populates every section and would fail
the freshness rule on a contributor's machine, and a gate that fires on everyone gets
switched off by the third person who hits it. For the same reason the check is NOT folded
into `token_floor_failure` beside the existing floor test, which is where the floor and the
counts already meet -- I tried that first and rejected it.

A COMMENT THIS CHANGE FALSIFIES IS CORRECTED IN THE SAME COMMIT. security.yml promised "a
FLOOR: adding tokens needs no CI change, losing them fails the build". After this, growth
that outruns the floor DOES ask for one line. That is the trade SEC-04 requests, and leaving
the old sentence standing would be a comment contradicting the code beside it.

Verified: ruff check + format clean; 50 tests passing, 38 definitions, no duplicate names;
9 new tests confirmed present BY NAME. The freshness assertion was exercised end to end
against the real floors (exit 0) and shown to FAIL on three separate drift shapes -- names
2/8 by ratio, estate 6/14 by ratio, site_prefixes 0-against-2 by absolute lag -- each naming
its own section. security.yml still parses as YAML and the step is wired into the
forbidden-content job.

NO TOKEN CONTENT ANYWHERE, in the code, the tests, the ledger row or this message. Every
figure is a count or a mode marker, both of which the gate already prints to CI logs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Held for the owner, and I want to be explicit that this reverses what I told builder-2 twenty minutes ago.

I named this one as next and held a queue slot for it. Then I looked at the diff: it adds a new step to .github/workflows/security.yml, which is a required-context producer, and that step introduces a new failure modescan_forbidden.py --assert-floor-fresh.

An hour before that I told builder-1 that a CI workflow file is squarely the owner's CI-gating category and that #1296 would be held for exactly that reason. Landing this one while holding #1296, 628, 634 and 635 would make the hold turn on which PR I happened to look at closely.

Nothing here is failing on its merits. The rebase is verified: merge-tree clean, and a heading set-difference against main shows nothing lost and exactly one row added (#1368). The change itself is well argued — a floor only constrains a list while it stays near it, and the comment says so plainly.

It is a category call about the file, not a doubt about the change. It joins 628, 634, 635 and 636 unarmed.

Sequencing consequence: 631 no longer consumes the appender slot, so 633 is next in that group.

@wshallwshall
wshallwshall force-pushed the claude/builder-2-sec04-detector-floor branch from 108c572 to 4ca6ee4 Compare August 27, 2026 10:57
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.

1 participant