Skip to content

Commit 4ea16a1

Browse files
bokelleyclaude
andauthored
feat: add adagents.json validation and discovery (#42)
* feat: add adagents.json validation support Add utilities for fetching, parsing, and validating adagents.json files per the AdCP specification. This allows sales agents to verify they are authorized for specific publisher properties. Features: - fetch_adagents(): Async function to fetch and validate adagents.json - verify_agent_authorization(): Check if agent is authorized for a property - verify_agent_for_property(): Convenience wrapper combining fetch + verify - domain_matches(): Domain matching logic per AdCP rules (wildcards, subdomains) - identifiers_match(): Property identifier matching logic Implements AdCP spec for publisher authorization: - Wildcard domain patterns (*.example.com) - Common subdomain matching (www, m) - Protocol-agnostic agent URL matching - Property type and identifier validation - Multiple identifier types (domain, bundle_id, etc.) Exception hierarchy: - AdagentsValidationError: Base error for validation issues - AdagentsNotFoundError: adagents.json not found (404) - AdagentsTimeoutError: Request timeout Tests: - 27 unit tests covering all core logic - Domain matching edge cases - Identifier matching rules - Authorization verification scenarios - Error handling and validation All existing tests pass (207 tests total) Type checking passes with mypy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add adagents validation example Add comprehensive example demonstrating adagents.json validation: - Fetching and parsing adagents.json - Verifying agent authorization - Domain matching rules - Identifier matching - Error handling Includes working examples with mock data and explanations of all key use cases for sales agents and publishers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add Publisher Authorization Validation section to README Document the new adagents.json validation functionality: - Usage examples for fetch_adagents and verify_agent_authorization - Domain matching rules (wildcards, subdomains, protocol-agnostic) - Use cases for sales agents and publishers - Reference to complete examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add property and tag discovery functions Add utilities to extract and query properties from adagents.json: - get_all_properties(): Extract all properties across all agents - get_all_tags(): Get unique tags from all properties - get_properties_by_agent(): Filter properties by agent URL Features: - Extracts properties with agent_url for reference - Protocol-agnostic agent URL matching - Handles empty/missing properties gracefully - Returns structured data for indexing and discovery Use cases: - Build property indexes and registries - Discover available inventory by tags - Query what properties an agent can sell - Aggregate publisher inventory across agents Tests: - 8 new unit tests covering all edge cases - All 215 tests pass Example added demonstrating property discovery and tag extraction. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add security and performance improvements to adagents validation - Add domain input validation to prevent injection attacks - Add comprehensive domain normalization (handles trailing dots/slashes) - Add HTTP session management with optional client parameter for connection pooling - Add 14 new tests for domain validation and normalization - Fix line length violation in docstring Security improvements: - Validate publisher domains before HTTP requests - Check for suspicious characters (backslash, @, newlines, tabs) - Prevent path traversal attempts - Enforce DNS domain length limits (253 chars) Performance improvements: - Optional httpx.AsyncClient parameter for connection reuse - Enables connection pooling for multiple adagents.json fetches - Reduces overhead for production use cases All 229 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: correct import sorting in __init__.py for CI linter Ruff's import sorter requires imports to be in alphabetical order. This fixes the CI linter failure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c3cd590 commit 4ea16a1

File tree

6 files changed

+1519
-0
lines changed

6 files changed

+1519
-0
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,50 @@ auth = index.get_agent_authorizations("https://agent-x.com")
424424
premium = index.find_agents_by_property_tags(["premium", "ctv"])
425425
```
426426

427+
## Publisher Authorization Validation
428+
429+
Verify sales agents are authorized to sell publisher properties via adagents.json:
430+
431+
```python
432+
from adcp import (
433+
fetch_adagents,
434+
verify_agent_authorization,
435+
verify_agent_for_property,
436+
)
437+
438+
# Fetch and parse adagents.json from publisher
439+
adagents_data = await fetch_adagents("publisher.com")
440+
441+
# Verify agent authorization for a property
442+
is_authorized = verify_agent_authorization(
443+
adagents_data=adagents_data,
444+
agent_url="https://sales-agent.example.com",
445+
property_type="website",
446+
property_identifiers=[{"type": "domain", "value": "publisher.com"}]
447+
)
448+
449+
# Or use convenience wrapper (fetch + verify in one call)
450+
is_authorized = await verify_agent_for_property(
451+
publisher_domain="publisher.com",
452+
agent_url="https://sales-agent.example.com",
453+
property_identifiers=[{"type": "domain", "value": "publisher.com"}],
454+
property_type="website"
455+
)
456+
```
457+
458+
**Domain Matching Rules:**
459+
- Exact match: `example.com` matches `example.com`
460+
- Common subdomains: `www.example.com` matches `example.com`
461+
- Wildcards: `api.example.com` matches `*.example.com`
462+
- Protocol-agnostic: `http://agent.com` matches `https://agent.com`
463+
464+
**Use Cases:**
465+
- Sales agents verify authorization before accepting media buys
466+
- Publishers test their adagents.json files
467+
- Developer tools build authorization validators
468+
469+
See `examples/adagents_validation.py` for complete examples.
470+
427471
## CLI Tool
428472

429473
The `adcp` command-line tool provides easy interaction with AdCP agents without writing code.

examples/adagents_validation.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example: Validating Publisher Authorization with adagents.json
4+
5+
This example demonstrates how to use the adagents validation utilities
6+
to verify that a sales agent is authorized to sell ads for a publisher's
7+
properties.
8+
"""
9+
10+
import asyncio
11+
12+
from adcp import (
13+
AdagentsNotFoundError,
14+
AdagentsValidationError,
15+
fetch_adagents,
16+
verify_agent_authorization,
17+
verify_agent_for_property,
18+
)
19+
20+
21+
async def example_fetch_and_verify():
22+
"""Example: Fetch adagents.json and verify authorization."""
23+
print("=" * 60)
24+
print("Example 1: Fetch and Verify Authorization")
25+
print("=" * 60)
26+
27+
publisher_domain = "example-publisher.com"
28+
agent_url = "https://sales-agent.example.com"
29+
30+
try:
31+
# Fetch the adagents.json file from the publisher
32+
print(f"\n1. Fetching adagents.json from {publisher_domain}...")
33+
adagents_data = await fetch_adagents(publisher_domain)
34+
print(f" ✓ Found {len(adagents_data['authorized_agents'])} authorized agents")
35+
36+
# Verify if our agent is authorized for a specific property
37+
print(f"\n2. Checking if {agent_url} is authorized...")
38+
is_authorized = verify_agent_authorization(
39+
adagents_data=adagents_data,
40+
agent_url=agent_url,
41+
property_type="website",
42+
property_identifiers=[{"type": "domain", "value": "example-publisher.com"}],
43+
)
44+
45+
if is_authorized:
46+
print(" ✓ Agent is authorized for this property")
47+
else:
48+
print(" ✗ Agent is NOT authorized for this property")
49+
50+
except AdagentsNotFoundError as e:
51+
print(f" ✗ Error: {e}")
52+
print(" The publisher has not deployed an adagents.json file")
53+
except AdagentsValidationError as e:
54+
print(f" ✗ Validation Error: {e}")
55+
56+
57+
async def example_convenience_wrapper():
58+
"""Example: Use the convenience wrapper for one-step verification."""
59+
print("\n\n" + "=" * 60)
60+
print("Example 2: Convenience Wrapper (Fetch + Verify)")
61+
print("=" * 60)
62+
63+
try:
64+
# Single function call to fetch and verify
65+
print("\nChecking authorization in one step...")
66+
is_authorized = await verify_agent_for_property(
67+
publisher_domain="example-publisher.com",
68+
agent_url="https://sales-agent.example.com",
69+
property_identifiers=[{"type": "domain", "value": "example-publisher.com"}],
70+
property_type="website",
71+
)
72+
73+
if is_authorized:
74+
print("✓ Agent is authorized!")
75+
else:
76+
print("✗ Agent is NOT authorized")
77+
78+
except Exception as e:
79+
print(f"✗ Error: {e}")
80+
81+
82+
def example_manual_verification():
83+
"""Example: Manual verification with pre-fetched data."""
84+
print("\n\n" + "=" * 60)
85+
print("Example 3: Manual Verification with Pre-fetched Data")
86+
print("=" * 60)
87+
88+
# Example adagents.json data structure
89+
adagents_data = {
90+
"authorized_agents": [
91+
{
92+
"url": "https://sales-agent.example.com",
93+
"properties": [
94+
{
95+
"property_type": "website",
96+
"name": "Main Website",
97+
"identifiers": [{"type": "domain", "value": "example.com"}],
98+
},
99+
{
100+
"property_type": "mobile_app",
101+
"name": "iOS App",
102+
"identifiers": [{"type": "bundle_id", "value": "com.example.app"}],
103+
},
104+
],
105+
},
106+
{
107+
"url": "https://another-agent.com",
108+
"properties": [], # Empty properties = authorized for all
109+
},
110+
]
111+
}
112+
113+
# Test various scenarios
114+
print("\nScenario 1: Agent authorized for website")
115+
result = verify_agent_authorization(
116+
adagents_data,
117+
"https://sales-agent.example.com",
118+
"website",
119+
[{"type": "domain", "value": "www.example.com"}], # www subdomain
120+
)
121+
print(f" Result: {result} (www subdomain matches example.com)")
122+
123+
print("\nScenario 2: Agent authorized for mobile app")
124+
result = verify_agent_authorization(
125+
adagents_data,
126+
"https://sales-agent.example.com",
127+
"mobile_app",
128+
[{"type": "bundle_id", "value": "com.example.app"}],
129+
)
130+
print(f" Result: {result}")
131+
132+
print("\nScenario 3: Agent NOT authorized for different property")
133+
result = verify_agent_authorization(
134+
adagents_data,
135+
"https://sales-agent.example.com",
136+
"website",
137+
[{"type": "domain", "value": "different.com"}],
138+
)
139+
print(f" Result: {result}")
140+
141+
print("\nScenario 4: Agent with empty properties = authorized for all")
142+
result = verify_agent_authorization(
143+
adagents_data, "https://another-agent.com", "website", [{"type": "domain", "value": "any.com"}]
144+
)
145+
print(f" Result: {result}")
146+
147+
print("\nScenario 5: Protocol-agnostic matching (http vs https)")
148+
result = verify_agent_authorization(
149+
adagents_data,
150+
"http://sales-agent.example.com", # http instead of https
151+
"website",
152+
[{"type": "domain", "value": "example.com"}],
153+
)
154+
print(f" Result: {result} (protocol ignored)")
155+
156+
157+
def example_property_discovery():
158+
"""Example: Discover all properties and tags from adagents.json."""
159+
print("\n\n" + "=" * 60)
160+
print("Example 4: Property and Tag Discovery")
161+
print("=" * 60)
162+
163+
from adcp import get_all_properties, get_all_tags, get_properties_by_agent
164+
165+
# Example adagents.json with tags
166+
adagents_data = {
167+
"authorized_agents": [
168+
{
169+
"url": "https://sales-agent-1.example.com",
170+
"properties": [
171+
{
172+
"property_type": "website",
173+
"name": "News Site",
174+
"identifiers": [{"type": "domain", "value": "news.example.com"}],
175+
"tags": ["premium", "news", "desktop"],
176+
},
177+
{
178+
"property_type": "mobile_app",
179+
"name": "News App",
180+
"identifiers": [{"type": "bundle_id", "value": "com.example.news"}],
181+
"tags": ["premium", "news", "mobile"],
182+
},
183+
],
184+
},
185+
{
186+
"url": "https://sales-agent-2.example.com",
187+
"properties": [
188+
{
189+
"property_type": "website",
190+
"name": "Sports Site",
191+
"identifiers": [{"type": "domain", "value": "sports.example.com"}],
192+
"tags": ["sports", "live-streaming"],
193+
}
194+
],
195+
},
196+
]
197+
}
198+
199+
print("\n1. Get all properties across all agents:")
200+
all_props = get_all_properties(adagents_data)
201+
print(f" Found {len(all_props)} total properties")
202+
for prop in all_props:
203+
print(f" - {prop['name']} ({prop['property_type']}) - Agent: {prop['agent_url']}")
204+
205+
print("\n2. Get all unique tags:")
206+
all_tags = get_all_tags(adagents_data)
207+
print(f" Tags: {', '.join(sorted(all_tags))}")
208+
209+
print("\n3. Get properties for a specific agent:")
210+
agent_props = get_properties_by_agent(adagents_data, "https://sales-agent-1.example.com")
211+
print(f" Agent 1 has {len(agent_props)} properties:")
212+
for prop in agent_props:
213+
print(f" - {prop['name']} (tags: {', '.join(prop.get('tags', []))})")
214+
215+
216+
def example_domain_matching():
217+
"""Example: Domain matching rules."""
218+
print("\n\n" + "=" * 60)
219+
print("Example 5: Domain Matching Rules")
220+
print("=" * 60)
221+
222+
from adcp import domain_matches
223+
224+
print("\n1. Exact match:")
225+
print(f" example.com == example.com: {domain_matches('example.com', 'example.com')}")
226+
227+
print("\n2. Common subdomains (www, m) match bare domain:")
228+
print(f" www.example.com matches example.com: {domain_matches('www.example.com', 'example.com')}")
229+
print(f" m.example.com matches example.com: {domain_matches('m.example.com', 'example.com')}")
230+
231+
print("\n3. Other subdomains DON'T match bare domain:")
232+
print(
233+
f" api.example.com matches example.com: {domain_matches('api.example.com', 'example.com')}"
234+
)
235+
236+
print("\n4. Wildcard pattern matches all subdomains:")
237+
print(
238+
f" api.example.com matches *.example.com: {domain_matches('api.example.com', '*.example.com')}"
239+
)
240+
print(
241+
f" www.example.com matches *.example.com: {domain_matches('www.example.com', '*.example.com')}"
242+
)
243+
244+
print("\n5. Case-insensitive matching:")
245+
print(f" Example.COM matches example.com: {domain_matches('Example.COM', 'example.com')}")
246+
247+
248+
async def main():
249+
"""Run all examples."""
250+
print("\n🔍 AdCP adagents.json Validation Examples\n")
251+
252+
# Note: Examples 1 and 2 would require actual HTTP requests
253+
# Uncomment to test with real domains:
254+
# await example_fetch_and_verify()
255+
# await example_convenience_wrapper()
256+
257+
# These examples work with mock data:
258+
example_manual_verification()
259+
example_property_discovery()
260+
example_domain_matching()
261+
262+
print("\n\n" + "=" * 60)
263+
print("Summary")
264+
print("=" * 60)
265+
print("""
266+
Key Functions:
267+
1. fetch_adagents(domain) - Fetch and validate adagents.json
268+
2. verify_agent_authorization(data, agent_url, ...) - Check authorization
269+
3. verify_agent_for_property(domain, agent_url, ...) - Convenience wrapper
270+
4. get_all_properties(data) - Extract all properties from all agents
271+
5. get_all_tags(data) - Get all unique tags across properties
272+
6. get_properties_by_agent(data, agent_url) - Get properties for specific agent
273+
7. domain_matches(prop_domain, pattern) - Domain matching rules
274+
8. identifiers_match(prop_ids, agent_ids) - Identifier matching
275+
276+
Use Cases:
277+
- Sales agents: Verify authorization before accepting media buys
278+
- Publishers: Test their adagents.json files are correctly formatted
279+
- Developer tools: Build validators and testing utilities
280+
281+
See the full API documentation for more details.
282+
""")
283+
284+
285+
if __name__ == "__main__":
286+
asyncio.run(main())

src/adcp/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,21 @@
77
Supports both A2A and MCP protocols with full type safety.
88
"""
99

10+
from adcp.adagents import (
11+
domain_matches,
12+
fetch_adagents,
13+
get_all_properties,
14+
get_all_tags,
15+
get_properties_by_agent,
16+
identifiers_match,
17+
verify_agent_authorization,
18+
verify_agent_for_property,
19+
)
1020
from adcp.client import ADCPClient, ADCPMultiAgentClient
1121
from adcp.exceptions import (
22+
AdagentsNotFoundError,
23+
AdagentsTimeoutError,
24+
AdagentsValidationError,
1225
ADCPAuthenticationError,
1326
ADCPConnectionError,
1427
ADCPError,
@@ -162,6 +175,15 @@
162175
"TaskResult",
163176
"TaskStatus",
164177
"WebhookMetadata",
178+
# Adagents validation
179+
"fetch_adagents",
180+
"verify_agent_authorization",
181+
"verify_agent_for_property",
182+
"domain_matches",
183+
"identifiers_match",
184+
"get_all_properties",
185+
"get_all_tags",
186+
"get_properties_by_agent",
165187
# Test helpers
166188
"test_agent",
167189
"test_agent_a2a",
@@ -185,6 +207,9 @@
185207
"ADCPToolNotFoundError",
186208
"ADCPWebhookError",
187209
"ADCPWebhookSignatureError",
210+
"AdagentsValidationError",
211+
"AdagentsNotFoundError",
212+
"AdagentsTimeoutError",
188213
# Request/Response types
189214
"ActivateSignalRequest",
190215
"ActivateSignalResponse",

0 commit comments

Comments
 (0)