Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ pg = [
"psycopg[binary]>=3.1.0",
"psycopg-pool>=3.2.0",
]
identity = [
# eTLD+1 binding for brand_json_url domain-consistency checks
# (Tier-3, gated on adcp#3690 Tier-3 work). Bundled PSL snapshot
# updates lazily; never fetches at runtime.
"tldextract>=5.0.0",
]

[project.urls]
Homepage = "https://github.com/adcontextprotocol/adcp-client-python"
Expand Down
41 changes: 41 additions & 0 deletions src/adcp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,33 @@ def handle_show_config() -> None:
print(f"Config file: {CONFIG_FILE}")


def _handle_resolve(agent_url: str, *, json_output: bool, quiet: bool) -> None:
"""Handle --resolve: walk brand_json_url and print the resolution result."""
from adcp.signing.agent_resolver import AgentResolverError, resolve_agent

try:
result = resolve_agent(agent_url)
except AgentResolverError as exc:
print(f"Error [{exc.code}]: {exc.detail}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)

if json_output:
print(result.model_dump_json(indent=2))
else:
print(f"Agent URL: {result.agent_url}")
print(f"Brand JSON URL: {result.brand_json_url}")
print(f"JWKS URI: {result.jwks_uri}")
print(f"Keys: {len(result.jwks.get('keys', []))}")
if not quiet:
for hop in result.trace:
status_str = f" → HTTP {hop.status}" if hop.status is not None else ""
time_str = f" ({hop.elapsed_ms}ms)" if hop.elapsed_ms is not None else ""
print(f" [{hop.label}] {hop.url}{status_str}{time_str}")


def resolve_agent_config(agent_identifier: str) -> dict[str, Any]:
"""Resolve agent identifier to configuration."""
# Check if it's a saved alias
Expand Down Expand Up @@ -517,6 +544,12 @@ def main() -> None:
parser.add_argument("--show-config", action="store_true", help="Show config file location")
parser.add_argument("--version", action="store_true", help="Show SDK and AdCP version")

# Identity resolution
parser.add_argument(
"--resolve", metavar="AGENT_URL", help="Resolve agent URL to signing keys"
)
parser.add_argument("--quiet", action="store_true", help="Suppress trace output from --resolve")

# Execution options
parser.add_argument("--protocol", choices=["mcp", "a2a"], help="Force protocol type")
parser.add_argument("--auth", help="Authentication token")
Expand All @@ -542,6 +575,7 @@ def main() -> None:
args.remove_agent,
args.show_config,
args.version,
args.resolve,
]
)
):
Expand All @@ -559,6 +593,9 @@ def main() -> None:
print(' adcp cs-agent calibrate_content \'{"content_standards_id":"cs-123"}\'')
print(" adcp si-agent si_get_offering")
print(" adcp gov-agent list_property_lists")
print("\nIdentity Resolution:")
print(" adcp --resolve https://buyer.example.com/mcp")
print(" adcp --resolve https://buyer.example.com/mcp --json")
sys.exit(0)

# Handle configuration commands
Expand Down Expand Up @@ -587,6 +624,10 @@ def main() -> None:
handle_show_config()
sys.exit(0)

if args.resolve:
_handle_resolve(args.resolve, json_output=args.json, quiet=args.quiet)
sys.exit(0)

# Execute tool
if not args.agent:
print("Error: Agent identifier required", file=sys.stderr)
Expand Down
18 changes: 18 additions & 0 deletions src/adcp/signing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@

from __future__ import annotations

from adcp.signing.agent_resolver import (
AgentResolution,
AgentResolutionFreshness,
AgentResolutionHop,
AgentResolverError,
AgentResolverErrorCode,
async_resolve_agent,
resolve_agent,
verify_from_agent_url,
)
from adcp.signing.autosign import (
SigningConfig,
SigningDecision,
Expand Down Expand Up @@ -263,6 +273,11 @@ def __init__(self, *args: object, **kwargs: object) -> None:


__all__ = [
"AgentResolution",
"AgentResolutionFreshness",
"AgentResolutionHop",
"AgentResolverError",
"AgentResolverErrorCode",
"ALG_ED25519",
"ALG_ES256",
"ALLOWED_ALGS",
Expand Down Expand Up @@ -344,6 +359,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
"as_async_resolver",
"async_default_jwks_fetcher",
"async_default_revocation_list_fetcher",
"async_resolve_agent",
"async_sign_request",
"averify_detached_jws",
"averify_jws_document",
Expand Down Expand Up @@ -371,6 +387,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
"pem_to_adcp_jwk",
"private_key_from_jwk",
"public_key_from_jwk",
"resolve_agent",
"resolve_and_validate_host",
"sign_request",
"sign_signature_base",
Expand All @@ -379,6 +396,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
"validate_jwks_uri",
"verify_detached_jws",
"verify_flask_request",
"verify_from_agent_url",
"verify_jws_document",
"verify_request_signature",
"verify_signature",
Expand Down
Loading
Loading