Repository navigation
verify_from_agent_url reports request_signature_jwks_unavailable for failures the discovery-chain table assigns their own code #1358
Description
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
bokelley commentedon Oct 3, 2026
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_urland_brand_resolution_errorthat collapses discovery-chain codes intorequest_signature_jwks_unavailableis being fixed in that PR.Triaged by Claude Code. Session: https://claude.ai/code/session_01WZRxYfmMWTU7aqXGzYZDcW
Generated by Claude Code