Skip to content

verify_from_agent_url reports request_signature_jwks_unavailable for failures the discovery-chain table assigns their own code #1358

Closed

Description

@KonstantinMirin

What is wrong

verify_from_agent_url maps every AgentResolverError except invalid_agent_url to
request_signature_jwks_unavailable:

# src/adcp/signing/agent_resolver.py
mapped = exc.signature_code or (
    REQUEST_SIGNATURE_JWKS_UNTRUSTED
    if exc.code == "invalid_agent_url"
    else REQUEST_SIGNATURE_JWKS_UNAVAILABLE
)

AdCP 3.2.1 security.mdx gives each hop of the brand_json_url discovery chain its own
rejection code, under Rejection codes for this discovery chain (3.x):

Code When
request_signature_capabilities_unreachable Capabilities fetch failed (DNS, TCP, TLS, timeout, non-2xx)
request_signature_brand_json_unreachable brand.json fetch failed (same conditions)
request_signature_brand_json_malformed brand.json failed strict-parse

Verifier checklist step 7 runs that chain and "short-circuits with the
request_signature_brand_* and request_signature_key_origin_* codes from that section".

errors.py states the same intent in the SDK's own words:

# brand.json discovery chain (ADCP #3690). ... Each step has a dedicated rejection
# code so callers can disambiguate retryable transport failures (``*_unreachable``)
# from misconfiguration (``*_missing`` / ``*_malformed`` / ``*_mismatch``).

The else arm discards that. REQUEST_SIGNATURE_CAPABILITIES_UNREACHABLE is defined in
errors.py and exported from adcp.signing, and no request-verification path passes it to
a SignatureVerificationError.

A second gap has the same shape: _brand_resolution_error maps five of the ten
BrandJsonResolverError codes, so invalid_url, invalid_house, redirect_loop and
redirect_depth_exceeded carry no signature_code and take the same default.

Why this is API design, not schema or codegen

The pinned enum carries all nine discovery-chain codes, and the generated
RequestSigningErrorCode carries them too. Codegen reproduces at this base byte for byte.
The codes exist and are reachable; one mapping function declines to use them.

Downstream consequence

The two codes prescribe different retry disciplines, so collapsing them hands the caller
the wrong one:

  • request_signature_capabilities_unreachable — {"recovery": "transient", "suggestion": "retry once after 1–5 seconds of jittered backoff; do not negative-cache for more than 60 seconds"}
  • request_signature_jwks_unavailable — {"recovery": "transient", "suggestion": "retry with bounded exponential backoff"}

A verifier that reports the JWKS code for a capabilities timeout tells the signer operator
to back off exponentially against a JWKS endpoint that was never contacted, and drops the
60-second negative-cache ceiling that exists so an operator fixing a misconfiguration is
not locked out for a full revocation cycle.

Runnable

import asyncio
from adcp.signing import agent_resolver
from adcp.signing.agent_resolver import AgentResolverError, verify_from_agent_url
from adcp.signing.errors import SignatureVerificationError

class _Req:
    method, url, headers = "POST", "https://seller.example.com/mcp", {}
    async def body(self): return b""

async def main():
    async def fake(*a, **k):
        raise AgentResolverError("capabilities_unreachable", "capabilities fetch: HTTP 503")
    agent_resolver.async_resolve_agent = fake
    try:
        await verify_from_agent_url(_Req(), "https://buyer.example.com/mcp",
                                    agent_type="sales", operation="get_products")
    except SignatureVerificationError as e:
        print(e.code)

asyncio.run(main())

Output:

request_signature_jwks_unavailable

Expected request_signature_capabilities_unreachable.

Conformance

_compliance/3.2.1/test-vectors/request-signing/ carries no vector for either
request_signature_capabilities_unreachable or request_signature_jwks_unavailable — the
vectors cover steps 1–6 and 8–14 of the checklist and grade no step-7 discovery outcome.
The signed-requests storyboard is named as the grader for "every step in the verifier
checklist", so the discovery-chain codes are a gap in the executable contract as well.

$ grep -rhoE '"(request_signature_[a-z_]+)"' \
    src/adcp/_compliance/3.2.1/test-vectors/request-signing/ | sort -u
"request_signature_alg_not_allowed"
"request_signature_components_incomplete"
"request_signature_components_unexpected"
"request_signature_digest_mismatch"
"request_signature_header_malformed"
"request_signature_invalid"
"request_signature_key_purpose_invalid"
"request_signature_key_revoked"
"request_signature_key_unknown"
"request_signature_params_incomplete"
"request_signature_rate_abuse"
"request_signature_replayed"
"request_signature_required"
"request_signature_tag_invalid"
"request_signature_window_invalid"

Related

One instance of a pattern in src/adcp/signing/: a rule declared in one place and
decided in another — here the discovery-chain rejection table, decided by an if-else
above it. #1354 enumerates the others.

Activity

  1. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 3, 2026
  2. bokelley commented on Oct 3, 2026

    @bokelley
    Contributor

    Triage

    Classification: Bug
    Bucket(s): signing (label not in repo — noted in run summary)
    Status: deferred

    What the experts said:

    My take: Blocked-on: #1373 — resurfaces on merge. The mapping gap in verify_from_agent_url and _brand_resolution_error that collapses discovery-chain codes into request_signature_jwks_unavailable is being fixed in that PR.


    Triaged by Claude Code. Session: https://claude.ai/code/session_01WZRxYfmMWTU7aqXGzYZDcW


    Generated by Claude Code

  3. added and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions