Skip to content

guard(auth): the ungated control must name a router scheduled to be fixed, so every fix breaks it #16242

Description

@mrveiss

Problem

repo_tests/router_auth_coverage_test.py proves its detector works by asserting one control router per gating mechanism, including one that must classify as UNGATED — genuinely nothing. That control has to name a real registered router with no gate.

Every such router is, by definition, on KNOWN_UNGATED and scheduled to be fixed. So the control is structurally guaranteed to break each time the router it names gets its gate. That has now happened once:

It was re-pointed to api.knowledge_search, which is itself scheduled for tenant scoping under #15745 AC4. The same failure will recur there, and again for every successor, until the last ungated router is fixed — at which point no real router can serve as the control at all, and the test either fails permanently or someone deletes the case.

The breakage is correct behaviour — a control that stops matching is exactly what a control is for. The defect is that the control's subject is drawn from the population the work is trying to empty.

Scope

Give the UNGATED control a subject that stays ungated regardless of how the real tree changes.

Acceptance criteria

  • The "genuinely nothing" UNGATED control classifies a synthetic router — a module built under tmp_path and fed to the enumerator — not a production module on KNOWN_UNGATED.
  • The synthetic subject carries no gate by any of the four mechanisms and no intentional marker, so the verdict it witnesses is exactly the one the case is named for.
  • The other four controls (router-level, per-route, inline, documented-open) are checked for the same exposure. They currently name routers that are gated, so they are stable — but the test should say so, so a later reader does not "fix" them into the same trap.
  • Gating every router in KNOWN_UNGATED no longer requires touching the control list.

Notes

The pattern generalises beyond this file: a positive control for "the bad state" cannot be drawn from the set of things you are fixing. Drawing it from there makes every fix look like a guard failure, which teaches people to edit the control rather than read it.

Found while landing the first #15745 fix. Related: #15745.

Activity

  1. added this to the v0.9.0 milestone on Sep 12, 2026
  2. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    What landed today (#16370, afccf73)

    Checked against origin/main at b688b3e. #16370 referenced this issue (Refs); it did not close it. Its body marks this issue as separate scope.

    AC Evidence on origin/main Status
    1. The "genuinely nothing" control classifies a synthetic tmp_path router repo_tests/router_auth_coverage_test.py:93 is still ("api.knowledge_search", "UNGATED", "genuinely nothing"), a production module listed in KNOWN_UNGATED (:35-41) ❌
    2. The synthetic subject has no gate and no marker No synthetic subject exists in that test ❌
    3. The other four controls are checked, and the test says why they are stable The other rows carry no such note. test_one_control_per_gating_mechanism's docstring says only "If a control's verdict changes, the DETECTOR broke" ❌
    4. Gating every KNOWN_UNGATED router leaves the control list alone :93 fails as soon as api.knowledge_search is gated ❌

    What landed: #16370 added the shape AC1 asks for to the sibling config-registry guard: repo_tests/config_router_auth_coverage_test.py:132 test_a_planted_ungated_feature_router_is_caught(tmp_path) and :144 for the gated contrast. The same pattern can be ported to router_auth_coverage_test.py.

    What remains: all four ACs.

  3. modified the milestones: v0.9.0, v0.10.0 on Sep 17, 2026
  4. mrveiss commented on Oct 2, 2026

    @mrveiss
    OwnerAuthor

    The outcome this issue predicted has already happened. Verified against origin/main during a closed-state duplicate pass for the v0.10.0 triage (#17863).

    repo_tests/router_auth_coverage_test.py:81-91 now parametrises five controls:

    ("api.agent_org",        "GATED",              "router-level APIRouter(dependencies=[...])")
    ("api.system",           "GATED",              "per-route check_admin_permission")
    ("api.websockets",       "GATED",              "inline in the handler body")
    ("api.knowledge_search", "GATED",              "router-level APIRouter(dependencies=[...]) (#15745)")
    ("api.chat_embed",       "UNGATED-BY-DESIGN",  "documented in the module docstring")
    

    Four GATED, one UNGATED-BY-DESIGN, and no plain-ungated control. api.knowledge_search was the ungated one and is now gated — the row itself cites #15745 for that change.

    Why that matters, in the guard's own words. Its docstring at :92-96 states the design: "If a control's verdict changes, the DETECTOR broke -- not the tree. Each row is a different mechanism. Two controls of the same shape cannot tell you the detector covers a shape it has never seen."

    By that logic the detector's ability to report a plain ungated router is now untested. UNGATED-BY-DESIGN is a different verdict value exercising a different path — the documented-exception path, not the genuinely-unprotected one. So if a router silently loses its gate tomorrow, nothing in this control table demonstrates the detector would say so.

    That is precisely the failure mode this issue was opened to prevent, and it is live rather than hypothetical.

    Not proposing a fix here, since the remedy is this issue's own subject: a synthetic control that is ungated on purpose and cannot be "fixed" out from under the guard. Worth noting that a git grep for tmp_path and synthetic in this file and in repo_tests/router_auth_enumerator.py returns nothing, so no synthetic control exists yet.

    One attribution note: a parallel reading of this attributed the gating of api.knowledge_search to #16507. The control row itself cites #15745. I have not established which is correct and am reporting what the file says.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions