Skip to content

Declare signing posture at route registration — one place, not three that can drift #2049

Description

@KonstantinMirin

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions