What
Register routes in one place, and declare at registration whether a route is signature-verified. Signing posture becomes a property of the route rather than a prefix convention re-asserted in several places that can drift apart.
Not critical — filed as a note-to-self while working #1291 / #1757. The immediate duplication is already fixed (see "Already done" below); this issue is the deeper change.
Why
Today the question "is this an AdCP surface that must be signature-verified?" is answered by matching a request path against ADCP_SURFACE_PREFIXES (/mcp, /a2a, /api/v1). That makes membership implicit — a relationship between two independently-editable things:
app.routes — grows whenever anyone adds a feature; owned by nobody in particular
ADCP_SURFACE_PREFIXES — a tuple in the signing layer; changes only when someone remembers
Neither is wrong on its own. A new route is a fine route; the tuple is a fine tuple. The defect exists only in the gap between them — which is why it can't be forbidden by a type, and has to be re-asserted by a structural guard (TestAllowlistTiedToRouteTable) instead.
And the failure is silent. As that guard's own docstring puts it: "an unverified surface has no runtime symptom." A surface that falls out of the allowlist serves traffic normally, returns correct responses, and passes the suite — it simply is not signature-checked. make quality is the only place that signal can exist.
What the refactor buys
With posture declared at registration, a surface outside the verifier's scope becomes unconstructible. Several places collapse to one, and the guard class becomes unnecessary rather than merely correct.
Caveat (owner's own, and it's the right bar)
One can still create a route and forget to place that signing parameter in it, but we cannot guard against plain stupidity.
Agreed. The goal isn't perfection — it's reducing an invariant spread across several drifting artifacts to a single required decision at one call site. A forgotten parameter at one seam is a far smaller and far more visible surface than a silent gap between a route table and a prefix tuple.
Already done — do not redo
The duplication is gone. matches_surface_prefix / is_adcp_surface in src/core/signing/operations.py now carry the single boundary rule (path == prefix or path.startswith(f"{prefix}/"), so /mcpx and /api/v1x cannot sneak in); request_verifier_middleware._is_adcp_surface only adapts the ASGI scope and holds no boundary rule of its own; and the structural guard imports the production predicate instead of grading a private copy.
Verified by mutation: rewriting the predicate to a bare startswith turns the guard red six ways (segment / hyphen / dot boundary variants), where it previously stayed green because the guard graded its own intact copy.
What remains for this issue is only the deeper change: make membership declared rather than matched.
What
Register routes in one place, and declare at registration whether a route is signature-verified. Signing posture becomes a property of the route rather than a prefix convention re-asserted in several places that can drift apart.
Not critical — filed as a note-to-self while working #1291 / #1757. The immediate duplication is already fixed (see "Already done" below); this issue is the deeper change.
Why
Today the question "is this an AdCP surface that must be signature-verified?" is answered by matching a request path against
ADCP_SURFACE_PREFIXES(/mcp,/a2a,/api/v1). That makes membership implicit — a relationship between two independently-editable things:app.routes— grows whenever anyone adds a feature; owned by nobody in particularADCP_SURFACE_PREFIXES— a tuple in the signing layer; changes only when someone remembersNeither is wrong on its own. A new route is a fine route; the tuple is a fine tuple. The defect exists only in the gap between them — which is why it can't be forbidden by a type, and has to be re-asserted by a structural guard (
TestAllowlistTiedToRouteTable) instead.And the failure is silent. As that guard's own docstring puts it: "an unverified surface has no runtime symptom." A surface that falls out of the allowlist serves traffic normally, returns correct responses, and passes the suite — it simply is not signature-checked.
make qualityis the only place that signal can exist.What the refactor buys
With posture declared at registration, a surface outside the verifier's scope becomes unconstructible. Several places collapse to one, and the guard class becomes unnecessary rather than merely correct.
Caveat (owner's own, and it's the right bar)
Agreed. The goal isn't perfection — it's reducing an invariant spread across several drifting artifacts to a single required decision at one call site. A forgotten parameter at one seam is a far smaller and far more visible surface than a silent gap between a route table and a prefix tuple.
Already done — do not redo
The duplication is gone.
matches_surface_prefix/is_adcp_surfaceinsrc/core/signing/operations.pynow carry the single boundary rule (path == prefix or path.startswith(f"{prefix}/"), so/mcpxand/api/v1xcannot sneak in);request_verifier_middleware._is_adcp_surfaceonly adapts the ASGI scope and holds no boundary rule of its own; and the structural guard imports the production predicate instead of grading a private copy.Verified by mutation: rewriting the predicate to a bare
startswithturns the guard red six ways (segment / hyphen / dot boundary variants), where it previously stayed green because the guard graded its own intact copy.What remains for this issue is only the deeper change: make membership declared rather than matched.