Skip to content

feat(signing): implement async_resolve_agent + verify_from_agent_url + CLI --resolve (#344)#398

Draft
bokelley wants to merge 2 commits intomainfrom
claude/issue-344-agent-resolver
Draft

feat(signing): implement async_resolve_agent + verify_from_agent_url + CLI --resolve (#344)#398
bokelley wants to merge 2 commits intomainfrom
claude/issue-344-agent-resolver

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 3, 2026

Refs #344

Summary

  • async_resolve_agent(agent_url) — walks identity.brand_json_url → brand.json → JWKS via three SSRF-pinned hops; returns AgentResolution (Pydantic model with agent_url, brand_json_url, agent_entry, jwks_uri, jwks, freshness, trace).
  • resolve_agent(agent_url) — sync wrapper via asyncio.run.
  • verify_from_agent_url(..., agent_url, options) — composes resolver + verify_request_signature; injects StaticJwksResolver backed by the resolved JWKS snapshot into caller's VerifyOptions.
  • adcp --resolve AGENT_URL [--json] [--quiet] — CLI flag (not subcommand, avoids agent="resolve" dispatch collision at __main__.py:562).
  • [identity] extra — adds tldextract>=5.0.0 for future eTLD+1 domain-binding (Tier-3, adcp#3690).
  • All names exported from adcp.signing.

Security properties

All three hops are SSRF-hardened:

  • Hop 1 (capabilities): build_async_ip_pinned_transport resolves + validates the agent hostname once, pins all connections to that IP, follow_redirects=False, trust_env=False.
  • Hop 2 (brand.json): existing _fetch_brand_json (already IP-pinned).
  • Hop 3 (JWKS): existing async_default_jwks_fetcher (already IP-pinned).

Domain-binding guard (_validate_brand_json_origin): brand_json_url must be same-origin or a parent domain of agent_url before 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 omits jwks_uri, delegates to brand_jwks._default_jwks_uri which enforces agent-origin ≡ brand.json-origin parity, closing the cross-origin trust-pivot vector.

Out of scope (Tier-3 / follow-on)

  • get_agent_jwks top-level helper (superseded by AgentResolution.jwks)
  • 8 typed AgentResolverError subclasses keyed by code
  • verify_starlette_request/verify_flask_request integration
  • eTLD+1 cross-subdomain binding via tldextract (adcp#3690 Tier-3)
  • brand_json_url etag caching (requires revisiting data is None guard at capability call site)

Pre-PR review

Two parallel expert passes on the diff post-push, per triage protocol.

code-reviewer — findings addressed:

  • [blocker] Fallback JWKS URI in _find_agent_by_url skipped origin check → replaced with _default_jwks_uri call ✓
  • [blocker] --fresh flag documented but silently ignored → removed ✓
  • [issue] verify_from_agent_url untestable (no _client_factory seam, zero tests) → added seam + test ✓
  • [issue] _find_agent_by_url docstring claimed top-level agents[] fallback when house key present → fixed ✓
  • [nit] try: async with ...: ... except AgentResolverError: raise pattern noted (consistent with brand_jwks, left as-is)

security-reviewer — findings addressed:

  • [blocker] brand_json_url accepted verbatim from attacker; no domain binding → _validate_brand_json_origin added, new brand_json_origin_mismatch error code ✓
  • [blocker] _find_agent_by_url fallback JWKS URI skipped _default_jwks_uri origin check → fixed (same fix as code-reviewer blocker) ✓
  • [nit] DNS rebinding on hop 1 confirmed closed by IP-pinned transport (no action needed)
  • [nit] Resolved IPs in AgentResolverError.detail on stderr — acceptable for a developer CLI; flagged for API-surface callers

What was tested

  • pytest tests/test_agent_resolver.py — 26 tests, all passing (added: origin mismatch rejected, parent domain accepted, verify_from_agent_url round-trip, --fresh removed)
  • ruff check — clean
  • mypy src/adcp/signing/agent_resolver.py — only pre-existing repo-wide pydantic stub errors (not introduced here)

This PR is triage-managed. Created by the autonomous triage agent in response to @bokelley's design sign-off on 2026-05-03.


Generated by Claude Code

claude added 2 commits May 3, 2026 00:37
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants