feat(signing): implement async_resolve_agent + verify_from_agent_url + CLI --resolve (#344)#398
Draft
feat(signing): implement async_resolve_agent + verify_from_agent_url + CLI --resolve (#344)#398
Conversation
…solve Implements §"Discovering an agent's signing keys via brand_json_url" from security.mdx (8-step algorithm). Per design decisions in issue #344: - agent_resolver.py: async_resolve_agent (canonical), resolve_agent (sync wrapper), verify_from_agent_url (composes resolver + verifier). SSRF-pinned capability fetch uses own IP-pinned transport — does NOT route through ADCPClient (threat model differs for attacker-supplied URLs). - AgentResolution Pydantic model: agent_url, brand_json_url, agent_entry, jwks_uri, jwks, freshness, trace. No SDK-invented terms (identity_posture, consistency dropped per spec-provenance check). - brand_json_url read via identity dict from capabilities response (3.0.5+ has additionalProperties: true on identity, so field flows through). - tldextract added as [identity] optional extra (Tier-3 eTLD+1 binding gate). - CLI: --resolve <agent-url> flag (avoids positional alias collision found at __main__.py:528). - 23 unit tests, all passing. ruff + mypy (signing module) clean. Refs #344 https://claude.ai/code/session_01KkMQ5QS2Mhpyv2nfvx1btN
Security blockers: - Add _validate_brand_json_origin: brand_json_url must be same-origin or parent-domain of agent_url; prevents a compromised agent from redirecting hop-2 key discovery to an attacker-controlled public host - Replace inline JWKS fallback in _find_agent_by_url with _default_jwks_uri from brand_jwks, which enforces agent/brand.json origin parity (prevents cross-origin trust pivot when jwks_uri is absent) - Add brand_json_origin_mismatch error code to AgentResolverErrorCode Code review findings: - Remove --fresh CLI flag (documented but silently ignored, no caching layer) - Add _client_factory seam to verify_from_agent_url so it is testable - Fix _find_agent_by_url docstring (no top-level agents[] fallback when house key present) - Add tests: origin mismatch rejected, parent domain accepted, verify_from_agent_url https://claude.ai/code/session_01KkMQ5QS2Mhpyv2nfvx1btN
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #344
Summary
async_resolve_agent(agent_url)— walksidentity.brand_json_url→ brand.json → JWKS via three SSRF-pinned hops; returnsAgentResolution(Pydantic model withagent_url,brand_json_url,agent_entry,jwks_uri,jwks,freshness,trace).resolve_agent(agent_url)— sync wrapper viaasyncio.run.verify_from_agent_url(..., agent_url, options)— composes resolver +verify_request_signature; injectsStaticJwksResolverbacked by the resolved JWKS snapshot into caller'sVerifyOptions.adcp --resolve AGENT_URL [--json] [--quiet]— CLI flag (not subcommand, avoidsagent="resolve"dispatch collision at__main__.py:562).[identity]extra — addstldextract>=5.0.0for future eTLD+1 domain-binding (Tier-3, adcp#3690).adcp.signing.Security properties
All three hops are SSRF-hardened:
build_async_ip_pinned_transportresolves + validates the agent hostname once, pins all connections to that IP,follow_redirects=False,trust_env=False._fetch_brand_json(already IP-pinned).async_default_jwks_fetcher(already IP-pinned).Domain-binding guard (
_validate_brand_json_origin):brand_json_urlmust be same-origin or a parent domain ofagent_urlbefore hop 2 is dispatched. Prevents a compromised agent from redirecting key discovery to an attacker-controlled public host.Fallback JWKS URI (
_find_agent_by_url): when an agent entry omitsjwks_uri, delegates tobrand_jwks._default_jwks_uriwhich enforces agent-origin ≡ brand.json-origin parity, closing the cross-origin trust-pivot vector.Out of scope (Tier-3 / follow-on)
get_agent_jwkstop-level helper (superseded byAgentResolution.jwks)AgentResolverErrorsubclasses keyed by codeverify_starlette_request/verify_flask_requestintegrationtldextract(adcp#3690 Tier-3)brand_json_urletag caching (requires revisitingdata is Noneguard at capability call site)Pre-PR review
Two parallel expert passes on the diff post-push, per triage protocol.
code-reviewer — findings addressed:
_find_agent_by_urlskipped origin check → replaced with_default_jwks_uricall ✓--freshflag documented but silently ignored → removed ✓verify_from_agent_urluntestable (no_client_factoryseam, zero tests) → added seam + test ✓_find_agent_by_urldocstring claimed top-levelagents[]fallback whenhousekey present → fixed ✓try: async with ...: ... except AgentResolverError: raisepattern noted (consistent withbrand_jwks, left as-is)security-reviewer — findings addressed:
brand_json_urlaccepted verbatim from attacker; no domain binding →_validate_brand_json_originadded, newbrand_json_origin_mismatcherror code ✓_find_agent_by_urlfallback JWKS URI skipped_default_jwks_uriorigin check → fixed (same fix as code-reviewer blocker) ✓AgentResolverError.detailon stderr — acceptable for a developer CLI; flagged for API-surface callersWhat was tested
pytest tests/test_agent_resolver.py— 26 tests, all passing (added: origin mismatch rejected, parent domain accepted,verify_from_agent_urlround-trip,--freshremoved)ruff check— cleanmypy src/adcp/signing/agent_resolver.py— only pre-existing repo-wide pydantic stub errors (not introduced here)Generated by Claude Code