adcp 1.4.1__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adcp/__init__.py +26 -1
- adcp/adagents.py +521 -0
- adcp/exceptions.py +30 -0
- adcp/types/__init__.py +2 -0
- adcp/types/base.py +26 -0
- adcp/types/generated.py +32 -1
- {adcp-1.4.1.dist-info → adcp-1.6.0.dist-info}/METADATA +45 -1
- {adcp-1.4.1.dist-info → adcp-1.6.0.dist-info}/RECORD +12 -10
- {adcp-1.4.1.dist-info → adcp-1.6.0.dist-info}/WHEEL +0 -0
- {adcp-1.4.1.dist-info → adcp-1.6.0.dist-info}/entry_points.txt +0 -0
- {adcp-1.4.1.dist-info → adcp-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {adcp-1.4.1.dist-info → adcp-1.6.0.dist-info}/top_level.txt +0 -0
adcp/__init__.py
CHANGED
|
@@ -7,8 +7,21 @@ Official Python client for the Ad Context Protocol (AdCP).
|
|
|
7
7
|
Supports both A2A and MCP protocols with full type safety.
|
|
8
8
|
"""
|
|
9
9
|
|
|
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
|
+
)
|
|
10
20
|
from adcp.client import ADCPClient, ADCPMultiAgentClient
|
|
11
21
|
from adcp.exceptions import (
|
|
22
|
+
AdagentsNotFoundError,
|
|
23
|
+
AdagentsTimeoutError,
|
|
24
|
+
AdagentsValidationError,
|
|
12
25
|
ADCPAuthenticationError,
|
|
13
26
|
ADCPConnectionError,
|
|
14
27
|
ADCPError,
|
|
@@ -150,7 +163,7 @@ from adcp.types.generated import (
|
|
|
150
163
|
TaskStatus as GeneratedTaskStatus,
|
|
151
164
|
)
|
|
152
165
|
|
|
153
|
-
__version__ = "1.
|
|
166
|
+
__version__ = "1.6.0"
|
|
154
167
|
|
|
155
168
|
__all__ = [
|
|
156
169
|
# Client classes
|
|
@@ -162,6 +175,15 @@ __all__ = [
|
|
|
162
175
|
"TaskResult",
|
|
163
176
|
"TaskStatus",
|
|
164
177
|
"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",
|
|
165
187
|
# Test helpers
|
|
166
188
|
"test_agent",
|
|
167
189
|
"test_agent_a2a",
|
|
@@ -185,6 +207,9 @@ __all__ = [
|
|
|
185
207
|
"ADCPToolNotFoundError",
|
|
186
208
|
"ADCPWebhookError",
|
|
187
209
|
"ADCPWebhookSignatureError",
|
|
210
|
+
"AdagentsValidationError",
|
|
211
|
+
"AdagentsNotFoundError",
|
|
212
|
+
"AdagentsTimeoutError",
|
|
188
213
|
# Request/Response types
|
|
189
214
|
"ActivateSignalRequest",
|
|
190
215
|
"ActivateSignalResponse",
|
adcp/adagents.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Utilities for fetching, parsing, and validating adagents.json files per the AdCP specification.
|
|
5
|
+
|
|
6
|
+
Publishers declare authorized sales agents via adagents.json files hosted at
|
|
7
|
+
https://{publisher_domain}/.well-known/adagents.json. This module provides utilities
|
|
8
|
+
for sales agents to verify they are authorized for specific properties.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from adcp.exceptions import AdagentsNotFoundError, AdagentsTimeoutError, AdagentsValidationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_domain(domain: str) -> str:
|
|
20
|
+
"""Normalize domain for comparison - strip, lowercase, remove trailing dots/slashes.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
domain: Domain to normalize
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Normalized domain string
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
AdagentsValidationError: If domain contains invalid patterns
|
|
30
|
+
"""
|
|
31
|
+
domain = domain.strip().lower()
|
|
32
|
+
# Remove both trailing slashes and dots iteratively
|
|
33
|
+
while domain.endswith("/") or domain.endswith("."):
|
|
34
|
+
domain = domain.rstrip("/").rstrip(".")
|
|
35
|
+
|
|
36
|
+
# Check for invalid patterns
|
|
37
|
+
if not domain or ".." in domain:
|
|
38
|
+
raise AdagentsValidationError(f"Invalid domain format: {domain!r}")
|
|
39
|
+
|
|
40
|
+
return domain
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate_publisher_domain(domain: str) -> str:
|
|
44
|
+
"""Validate and sanitize publisher domain for security.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
domain: Publisher domain to validate
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Validated and normalized domain
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
AdagentsValidationError: If domain is invalid or contains suspicious characters
|
|
54
|
+
"""
|
|
55
|
+
# Check for suspicious characters BEFORE stripping (to catch injection attempts)
|
|
56
|
+
suspicious_chars = ["\\", "@", "\n", "\r", "\t"]
|
|
57
|
+
for char in suspicious_chars:
|
|
58
|
+
if char in domain:
|
|
59
|
+
raise AdagentsValidationError(
|
|
60
|
+
f"Invalid character in publisher domain: {char!r}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
domain = domain.strip()
|
|
64
|
+
|
|
65
|
+
# Check basic constraints
|
|
66
|
+
if not domain:
|
|
67
|
+
raise AdagentsValidationError("Publisher domain cannot be empty")
|
|
68
|
+
if len(domain) > 253: # DNS maximum length
|
|
69
|
+
raise AdagentsValidationError(f"Publisher domain too long: {len(domain)} chars (max 253)")
|
|
70
|
+
|
|
71
|
+
# Check for spaces after stripping leading/trailing whitespace
|
|
72
|
+
if " " in domain:
|
|
73
|
+
raise AdagentsValidationError(
|
|
74
|
+
"Invalid character in publisher domain: ' '"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Remove protocol if present (common user error) - do this BEFORE checking for slashes
|
|
78
|
+
if "://" in domain:
|
|
79
|
+
domain = domain.split("://", 1)[1]
|
|
80
|
+
|
|
81
|
+
# Remove path if present (should only be domain) - do this BEFORE checking for slashes
|
|
82
|
+
if "/" in domain:
|
|
83
|
+
domain = domain.split("/", 1)[0]
|
|
84
|
+
|
|
85
|
+
# Normalize
|
|
86
|
+
domain = _normalize_domain(domain)
|
|
87
|
+
|
|
88
|
+
# Final validation - must look like a domain
|
|
89
|
+
if "." not in domain:
|
|
90
|
+
raise AdagentsValidationError(
|
|
91
|
+
f"Publisher domain must contain at least one dot: {domain!r}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return domain
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def normalize_url(url: str) -> str:
|
|
98
|
+
"""Normalize URL by removing protocol and trailing slash.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
url: URL to normalize
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Normalized URL (domain/path without protocol or trailing slash)
|
|
105
|
+
"""
|
|
106
|
+
parsed = urlparse(url)
|
|
107
|
+
normalized = parsed.netloc + parsed.path
|
|
108
|
+
return normalized.rstrip("/")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def domain_matches(property_domain: str, agent_domain_pattern: str) -> bool:
|
|
112
|
+
"""Check if domains match per AdCP rules.
|
|
113
|
+
|
|
114
|
+
Rules:
|
|
115
|
+
- Exact match always succeeds
|
|
116
|
+
- 'example.com' matches www.example.com, m.example.com (common subdomains)
|
|
117
|
+
- 'subdomain.example.com' matches that specific subdomain only
|
|
118
|
+
- '*.example.com' matches all subdomains
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
property_domain: Domain from property
|
|
122
|
+
agent_domain_pattern: Domain pattern from adagents.json
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if domains match per AdCP rules
|
|
126
|
+
"""
|
|
127
|
+
# Normalize both domains for comparison
|
|
128
|
+
try:
|
|
129
|
+
property_domain = _normalize_domain(property_domain)
|
|
130
|
+
agent_domain_pattern = _normalize_domain(agent_domain_pattern)
|
|
131
|
+
except AdagentsValidationError:
|
|
132
|
+
# Invalid domain format - no match
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Exact match
|
|
136
|
+
if property_domain == agent_domain_pattern:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
# Wildcard pattern (*.example.com)
|
|
140
|
+
if agent_domain_pattern.startswith("*."):
|
|
141
|
+
base_domain = agent_domain_pattern[2:]
|
|
142
|
+
return property_domain.endswith(f".{base_domain}")
|
|
143
|
+
|
|
144
|
+
# Bare domain matches common subdomains (www, m)
|
|
145
|
+
# If agent pattern is a bare domain (no subdomain), match www/m subdomains
|
|
146
|
+
if "." in agent_domain_pattern and not agent_domain_pattern.startswith("www."):
|
|
147
|
+
# Check if this looks like a bare domain (e.g., example.com)
|
|
148
|
+
parts = agent_domain_pattern.split(".")
|
|
149
|
+
if len(parts) == 2: # Looks like bare domain
|
|
150
|
+
common_subdomains = ["www", "m"]
|
|
151
|
+
for subdomain in common_subdomains:
|
|
152
|
+
if property_domain == f"{subdomain}.{agent_domain_pattern}":
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def identifiers_match(
|
|
159
|
+
property_identifiers: list[dict[str, str]],
|
|
160
|
+
agent_identifiers: list[dict[str, str]],
|
|
161
|
+
) -> bool:
|
|
162
|
+
"""Check if any property identifier matches agent's authorized identifiers.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
property_identifiers: Identifiers from property
|
|
166
|
+
(e.g., [{"type": "domain", "value": "cnn.com"}])
|
|
167
|
+
agent_identifiers: Identifiers from adagents.json
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if any identifier matches
|
|
171
|
+
|
|
172
|
+
Notes:
|
|
173
|
+
- Domain identifiers use AdCP domain matching rules
|
|
174
|
+
- Other identifiers (bundle_id, roku_store_id, etc.) require exact match
|
|
175
|
+
"""
|
|
176
|
+
for prop_id in property_identifiers:
|
|
177
|
+
prop_type = prop_id.get("type", "")
|
|
178
|
+
prop_value = prop_id.get("value", "")
|
|
179
|
+
|
|
180
|
+
for agent_id in agent_identifiers:
|
|
181
|
+
agent_type = agent_id.get("type", "")
|
|
182
|
+
agent_value = agent_id.get("value", "")
|
|
183
|
+
|
|
184
|
+
# Type must match
|
|
185
|
+
if prop_type != agent_type:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# Domain identifiers use special matching rules
|
|
189
|
+
if prop_type == "domain":
|
|
190
|
+
if domain_matches(prop_value, agent_value):
|
|
191
|
+
return True
|
|
192
|
+
else:
|
|
193
|
+
# Other identifier types require exact match
|
|
194
|
+
if prop_value == agent_value:
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def verify_agent_authorization(
|
|
201
|
+
adagents_data: dict[str, Any],
|
|
202
|
+
agent_url: str,
|
|
203
|
+
property_type: str | None = None,
|
|
204
|
+
property_identifiers: list[dict[str, str]] | None = None,
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""Check if agent is authorized for a property.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
adagents_data: Parsed adagents.json data
|
|
210
|
+
agent_url: URL of the sales agent to verify
|
|
211
|
+
property_type: Type of property (website, app, etc.) - optional
|
|
212
|
+
property_identifiers: List of identifiers to match - optional
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if agent is authorized, False otherwise
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
AdagentsValidationError: If adagents_data is malformed
|
|
219
|
+
|
|
220
|
+
Notes:
|
|
221
|
+
- If property_type/identifiers are None, checks if agent is authorized
|
|
222
|
+
for ANY property on this domain
|
|
223
|
+
- Implements AdCP domain matching rules
|
|
224
|
+
- Agent URLs are matched ignoring protocol and trailing slash
|
|
225
|
+
"""
|
|
226
|
+
# Validate structure
|
|
227
|
+
if not isinstance(adagents_data, dict):
|
|
228
|
+
raise AdagentsValidationError("adagents_data must be a dictionary")
|
|
229
|
+
|
|
230
|
+
authorized_agents = adagents_data.get("authorized_agents")
|
|
231
|
+
if not isinstance(authorized_agents, list):
|
|
232
|
+
raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")
|
|
233
|
+
|
|
234
|
+
# Normalize the agent URL for comparison
|
|
235
|
+
normalized_agent_url = normalize_url(agent_url)
|
|
236
|
+
|
|
237
|
+
# Check each authorized agent
|
|
238
|
+
for agent in authorized_agents:
|
|
239
|
+
if not isinstance(agent, dict):
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
agent_url_from_json = agent.get("url", "")
|
|
243
|
+
if not agent_url_from_json:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Match agent URL (protocol-agnostic)
|
|
247
|
+
if normalize_url(agent_url_from_json) != normalized_agent_url:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
# Found matching agent - now check properties
|
|
251
|
+
properties = agent.get("properties")
|
|
252
|
+
|
|
253
|
+
# If properties field is missing or empty, agent is authorized for all properties
|
|
254
|
+
if properties is None or (isinstance(properties, list) and len(properties) == 0):
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# If no property filters specified, we found the agent - authorized
|
|
258
|
+
if property_type is None and property_identifiers is None:
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
# Check specific property authorization
|
|
262
|
+
if isinstance(properties, list):
|
|
263
|
+
for prop in properties:
|
|
264
|
+
if not isinstance(prop, dict):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
# Check property type if specified
|
|
268
|
+
if property_type is not None:
|
|
269
|
+
prop_type = prop.get("property_type", "")
|
|
270
|
+
if prop_type != property_type:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Check identifiers if specified
|
|
274
|
+
if property_identifiers is not None:
|
|
275
|
+
prop_identifiers = prop.get("identifiers", [])
|
|
276
|
+
if not isinstance(prop_identifiers, list):
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
if identifiers_match(property_identifiers, prop_identifiers):
|
|
280
|
+
return True
|
|
281
|
+
else:
|
|
282
|
+
# Property type matched and no identifier check needed
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def fetch_adagents(
|
|
289
|
+
publisher_domain: str,
|
|
290
|
+
timeout: float = 10.0,
|
|
291
|
+
user_agent: str = "AdCP-Client/1.0",
|
|
292
|
+
client: httpx.AsyncClient | None = None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
"""Fetch and parse adagents.json from publisher domain.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
publisher_domain: Domain hosting the adagents.json file
|
|
298
|
+
timeout: Request timeout in seconds
|
|
299
|
+
user_agent: User-Agent header for HTTP request
|
|
300
|
+
client: Optional httpx.AsyncClient for connection pooling.
|
|
301
|
+
If provided, caller is responsible for client lifecycle.
|
|
302
|
+
If None, a new client is created for this request.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Parsed adagents.json data
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
AdagentsNotFoundError: If adagents.json not found (404)
|
|
309
|
+
AdagentsValidationError: If JSON is invalid or malformed
|
|
310
|
+
AdagentsTimeoutError: If request times out
|
|
311
|
+
|
|
312
|
+
Notes:
|
|
313
|
+
For production use with multiple requests, pass a shared httpx.AsyncClient
|
|
314
|
+
to enable connection pooling and improve performance.
|
|
315
|
+
"""
|
|
316
|
+
# Validate and normalize domain for security
|
|
317
|
+
publisher_domain = _validate_publisher_domain(publisher_domain)
|
|
318
|
+
|
|
319
|
+
# Construct URL
|
|
320
|
+
url = f"https://{publisher_domain}/.well-known/adagents.json"
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# Use provided client or create a new one
|
|
324
|
+
if client is not None:
|
|
325
|
+
# Reuse provided client (connection pooling)
|
|
326
|
+
response = await client.get(
|
|
327
|
+
url,
|
|
328
|
+
headers={"User-Agent": user_agent},
|
|
329
|
+
timeout=timeout,
|
|
330
|
+
follow_redirects=True,
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
# Create new client for single request
|
|
334
|
+
async with httpx.AsyncClient() as new_client:
|
|
335
|
+
response = await new_client.get(
|
|
336
|
+
url,
|
|
337
|
+
headers={"User-Agent": user_agent},
|
|
338
|
+
timeout=timeout,
|
|
339
|
+
follow_redirects=True,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Process response (same for both paths)
|
|
343
|
+
if response.status_code == 404:
|
|
344
|
+
raise AdagentsNotFoundError(publisher_domain)
|
|
345
|
+
|
|
346
|
+
if response.status_code != 200:
|
|
347
|
+
raise AdagentsValidationError(
|
|
348
|
+
f"Failed to fetch adagents.json: HTTP {response.status_code}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Parse JSON
|
|
352
|
+
try:
|
|
353
|
+
data = response.json()
|
|
354
|
+
except Exception as e:
|
|
355
|
+
raise AdagentsValidationError(f"Invalid JSON in adagents.json: {e}") from e
|
|
356
|
+
|
|
357
|
+
# Validate basic structure
|
|
358
|
+
if not isinstance(data, dict):
|
|
359
|
+
raise AdagentsValidationError("adagents.json must be a JSON object")
|
|
360
|
+
|
|
361
|
+
if "authorized_agents" not in data:
|
|
362
|
+
raise AdagentsValidationError(
|
|
363
|
+
"adagents.json must have 'authorized_agents' field"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if not isinstance(data["authorized_agents"], list):
|
|
367
|
+
raise AdagentsValidationError("'authorized_agents' must be an array")
|
|
368
|
+
|
|
369
|
+
return data
|
|
370
|
+
|
|
371
|
+
except httpx.TimeoutException as e:
|
|
372
|
+
raise AdagentsTimeoutError(publisher_domain, timeout) from e
|
|
373
|
+
except httpx.RequestError as e:
|
|
374
|
+
raise AdagentsValidationError(f"Failed to fetch adagents.json: {e}") from e
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
async def verify_agent_for_property(
|
|
378
|
+
publisher_domain: str,
|
|
379
|
+
agent_url: str,
|
|
380
|
+
property_identifiers: list[dict[str, str]],
|
|
381
|
+
property_type: str | None = None,
|
|
382
|
+
timeout: float = 10.0,
|
|
383
|
+
client: httpx.AsyncClient | None = None,
|
|
384
|
+
) -> bool:
|
|
385
|
+
"""Convenience wrapper to fetch adagents.json and verify authorization in one call.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
publisher_domain: Domain hosting the adagents.json file
|
|
389
|
+
agent_url: URL of the sales agent to verify
|
|
390
|
+
property_identifiers: List of identifiers to match
|
|
391
|
+
property_type: Type of property (website, app, etc.) - optional
|
|
392
|
+
timeout: Request timeout in seconds
|
|
393
|
+
client: Optional httpx.AsyncClient for connection pooling
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
True if agent is authorized, False otherwise
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
AdagentsNotFoundError: If adagents.json not found (404)
|
|
400
|
+
AdagentsValidationError: If JSON is invalid or malformed
|
|
401
|
+
AdagentsTimeoutError: If request times out
|
|
402
|
+
"""
|
|
403
|
+
adagents_data = await fetch_adagents(publisher_domain, timeout=timeout, client=client)
|
|
404
|
+
return verify_agent_authorization(
|
|
405
|
+
adagents_data=adagents_data,
|
|
406
|
+
agent_url=agent_url,
|
|
407
|
+
property_type=property_type,
|
|
408
|
+
property_identifiers=property_identifiers,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_all_properties(adagents_data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
413
|
+
"""Extract all properties from adagents.json data.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
adagents_data: Parsed adagents.json data
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
List of all properties across all authorized agents, with agent_url added
|
|
420
|
+
|
|
421
|
+
Raises:
|
|
422
|
+
AdagentsValidationError: If adagents_data is malformed
|
|
423
|
+
"""
|
|
424
|
+
if not isinstance(adagents_data, dict):
|
|
425
|
+
raise AdagentsValidationError("adagents_data must be a dictionary")
|
|
426
|
+
|
|
427
|
+
authorized_agents = adagents_data.get("authorized_agents")
|
|
428
|
+
if not isinstance(authorized_agents, list):
|
|
429
|
+
raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")
|
|
430
|
+
|
|
431
|
+
properties = []
|
|
432
|
+
for agent in authorized_agents:
|
|
433
|
+
if not isinstance(agent, dict):
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
agent_url = agent.get("url", "")
|
|
437
|
+
if not agent_url:
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
agent_properties = agent.get("properties", [])
|
|
441
|
+
if not isinstance(agent_properties, list):
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
# Add each property with the agent URL for reference
|
|
445
|
+
for prop in agent_properties:
|
|
446
|
+
if isinstance(prop, dict):
|
|
447
|
+
# Create a copy and add agent_url
|
|
448
|
+
prop_with_agent = {**prop, "agent_url": agent_url}
|
|
449
|
+
properties.append(prop_with_agent)
|
|
450
|
+
|
|
451
|
+
return properties
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def get_all_tags(adagents_data: dict[str, Any]) -> set[str]:
|
|
455
|
+
"""Extract all unique tags from properties in adagents.json data.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
adagents_data: Parsed adagents.json data
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Set of all unique tags across all properties
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
AdagentsValidationError: If adagents_data is malformed
|
|
465
|
+
"""
|
|
466
|
+
properties = get_all_properties(adagents_data)
|
|
467
|
+
tags = set()
|
|
468
|
+
|
|
469
|
+
for prop in properties:
|
|
470
|
+
prop_tags = prop.get("tags", [])
|
|
471
|
+
if isinstance(prop_tags, list):
|
|
472
|
+
for tag in prop_tags:
|
|
473
|
+
if isinstance(tag, str):
|
|
474
|
+
tags.add(tag)
|
|
475
|
+
|
|
476
|
+
return tags
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) -> list[dict[str, Any]]:
|
|
480
|
+
"""Get all properties authorized for a specific agent.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
adagents_data: Parsed adagents.json data
|
|
484
|
+
agent_url: URL of the agent to filter by
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
List of properties for the specified agent (empty if agent not found or no properties)
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
AdagentsValidationError: If adagents_data is malformed
|
|
491
|
+
"""
|
|
492
|
+
if not isinstance(adagents_data, dict):
|
|
493
|
+
raise AdagentsValidationError("adagents_data must be a dictionary")
|
|
494
|
+
|
|
495
|
+
authorized_agents = adagents_data.get("authorized_agents")
|
|
496
|
+
if not isinstance(authorized_agents, list):
|
|
497
|
+
raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")
|
|
498
|
+
|
|
499
|
+
# Normalize the agent URL for comparison
|
|
500
|
+
normalized_agent_url = normalize_url(agent_url)
|
|
501
|
+
|
|
502
|
+
for agent in authorized_agents:
|
|
503
|
+
if not isinstance(agent, dict):
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
agent_url_from_json = agent.get("url", "")
|
|
507
|
+
if not agent_url_from_json:
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
# Match agent URL (protocol-agnostic)
|
|
511
|
+
if normalize_url(agent_url_from_json) != normalized_agent_url:
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
# Found the agent - return their properties
|
|
515
|
+
properties = agent.get("properties", [])
|
|
516
|
+
if not isinstance(properties, list):
|
|
517
|
+
return []
|
|
518
|
+
|
|
519
|
+
return [p for p in properties if isinstance(p, dict)]
|
|
520
|
+
|
|
521
|
+
return []
|
adcp/exceptions.py
CHANGED
|
@@ -153,3 +153,33 @@ class ADCPSimpleAPIError(ADCPError):
|
|
|
153
153
|
f" # Handle error with full TaskResult context"
|
|
154
154
|
)
|
|
155
155
|
super().__init__(message, agent_id, None, suggestion)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class AdagentsValidationError(ADCPError):
|
|
159
|
+
"""Base error for adagents.json validation issues."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class AdagentsNotFoundError(AdagentsValidationError):
|
|
163
|
+
"""adagents.json file not found (404)."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, publisher_domain: str):
|
|
166
|
+
"""Initialize not found error."""
|
|
167
|
+
message = f"adagents.json not found for domain: {publisher_domain}"
|
|
168
|
+
suggestion = (
|
|
169
|
+
"Verify that the publisher has deployed adagents.json to:\n"
|
|
170
|
+
f" https://{publisher_domain}/.well-known/adagents.json"
|
|
171
|
+
)
|
|
172
|
+
super().__init__(message, None, None, suggestion)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AdagentsTimeoutError(AdagentsValidationError):
|
|
176
|
+
"""Request for adagents.json timed out."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, publisher_domain: str, timeout: float):
|
|
179
|
+
"""Initialize timeout error."""
|
|
180
|
+
message = f"Request to fetch adagents.json timed out after {timeout}s"
|
|
181
|
+
suggestion = (
|
|
182
|
+
"The publisher's server may be slow or unresponsive.\n"
|
|
183
|
+
" Try increasing the timeout value or check the domain is correct."
|
|
184
|
+
)
|
|
185
|
+
super().__init__(message, None, None, suggestion)
|
adcp/types/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
"""Type definitions for AdCP client."""
|
|
4
4
|
|
|
5
|
+
from adcp.types.base import AdCPBaseModel
|
|
5
6
|
from adcp.types.core import (
|
|
6
7
|
Activity,
|
|
7
8
|
ActivityType,
|
|
@@ -14,6 +15,7 @@ from adcp.types.core import (
|
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
18
|
+
"AdCPBaseModel",
|
|
17
19
|
"AgentConfig",
|
|
18
20
|
"Protocol",
|
|
19
21
|
"TaskResult",
|
adcp/types/base.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Base model for AdCP types with spec-compliant serialization."""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AdCPBaseModel(BaseModel):
|
|
11
|
+
"""Base model for AdCP types with spec-compliant serialization.
|
|
12
|
+
|
|
13
|
+
AdCP JSON schemas use additionalProperties: false and do not allow null
|
|
14
|
+
for optional fields. Therefore, optional fields must be omitted entirely
|
|
15
|
+
when not present (not sent as null).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
19
|
+
if "exclude_none" not in kwargs:
|
|
20
|
+
kwargs["exclude_none"] = True
|
|
21
|
+
return super().model_dump(**kwargs)
|
|
22
|
+
|
|
23
|
+
def model_dump_json(self, **kwargs: Any) -> str:
|
|
24
|
+
if "exclude_none" not in kwargs:
|
|
25
|
+
kwargs["exclude_none"] = True
|
|
26
|
+
return super().model_dump_json(**kwargs)
|
adcp/types/generated.py
CHANGED
|
@@ -14,7 +14,9 @@ from __future__ import annotations
|
|
|
14
14
|
import re
|
|
15
15
|
from typing import Any, Literal
|
|
16
16
|
|
|
17
|
-
from pydantic import
|
|
17
|
+
from pydantic import ConfigDict, Field, field_validator
|
|
18
|
+
|
|
19
|
+
from adcp.types.base import AdCPBaseModel as BaseModel
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
|
|
@@ -158,6 +160,8 @@ class Targeting(BaseModel):
|
|
|
158
160
|
geo_region_any_of: list[str] | None = Field(None, description="Restrict delivery to specific regions/states. Use for regulatory compliance or RCT testing.")
|
|
159
161
|
geo_metro_any_of: list[str] | None = Field(None, description="Restrict delivery to specific metro areas (DMA codes). Use for regulatory compliance or RCT testing.")
|
|
160
162
|
geo_postal_code_any_of: list[str] | None = Field(None, description="Restrict delivery to specific postal/ZIP codes. Use for regulatory compliance or RCT testing.")
|
|
163
|
+
axe_include_segment: str | None = Field(None, description="AXE segment ID to include for targeting")
|
|
164
|
+
axe_exclude_segment: str | None = Field(None, description="AXE segment ID to exclude from targeting")
|
|
161
165
|
frequency_cap: FrequencyCap | None = None
|
|
162
166
|
|
|
163
167
|
|
|
@@ -635,6 +639,7 @@ class ActivateSignalRequest(BaseModel):
|
|
|
635
639
|
|
|
636
640
|
signal_agent_segment_id: str = Field(description="The universal identifier for the signal to activate")
|
|
637
641
|
destinations: list[Destination] = Field(description="Target destination(s) for activation. If the authenticated caller matches one of these destinations, activation keys will be included in the response.")
|
|
642
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
638
643
|
|
|
639
644
|
|
|
640
645
|
class BuildCreativeRequest(BaseModel):
|
|
@@ -643,6 +648,7 @@ class BuildCreativeRequest(BaseModel):
|
|
|
643
648
|
message: str | None = Field(None, description="Natural language instructions for the transformation or generation. For pure generation, this is the creative brief. For transformation, this provides guidance on how to adapt the creative.")
|
|
644
649
|
creative_manifest: CreativeManifest | None = Field(None, description="Creative manifest to transform or generate from. For pure generation, this should include the target format_id and any required input assets (e.g., promoted_offerings for generative formats). For transformation (e.g., resizing, reformatting), this is the complete creative to adapt.")
|
|
645
650
|
target_format_id: FormatId = Field(description="Format ID to generate. The format definition specifies required input assets and output structure.")
|
|
651
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
646
652
|
|
|
647
653
|
|
|
648
654
|
class CreateMediaBuyRequest(BaseModel):
|
|
@@ -655,6 +661,7 @@ class CreateMediaBuyRequest(BaseModel):
|
|
|
655
661
|
start_time: StartTiming
|
|
656
662
|
end_time: str = Field(description="Campaign end date/time in ISO 8601 format")
|
|
657
663
|
reporting_webhook: Any | None = None
|
|
664
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
658
665
|
|
|
659
666
|
|
|
660
667
|
class GetMediaBuyDeliveryRequest(BaseModel):
|
|
@@ -665,6 +672,7 @@ class GetMediaBuyDeliveryRequest(BaseModel):
|
|
|
665
672
|
status_filter: Any | None = Field(None, description="Filter by status. Can be a single status or array of statuses")
|
|
666
673
|
start_date: str | None = Field(None, description="Start date for reporting period (YYYY-MM-DD)")
|
|
667
674
|
end_date: str | None = Field(None, description="End date for reporting period (YYYY-MM-DD)")
|
|
675
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
668
676
|
|
|
669
677
|
|
|
670
678
|
class GetProductsRequest(BaseModel):
|
|
@@ -673,6 +681,7 @@ class GetProductsRequest(BaseModel):
|
|
|
673
681
|
brief: str | None = Field(None, description="Natural language description of campaign requirements")
|
|
674
682
|
brand_manifest: BrandManifestRef | None = Field(None, description="Brand information manifest providing brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest.")
|
|
675
683
|
filters: dict[str, Any] | None = Field(None, description="Structured filters for product discovery")
|
|
684
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
676
685
|
|
|
677
686
|
|
|
678
687
|
class GetSignalsRequest(BaseModel):
|
|
@@ -682,12 +691,14 @@ class GetSignalsRequest(BaseModel):
|
|
|
682
691
|
deliver_to: dict[str, Any] = Field(description="Destination platforms where signals need to be activated")
|
|
683
692
|
filters: dict[str, Any] | None = Field(None, description="Filters to refine results")
|
|
684
693
|
max_results: int | None = Field(None, description="Maximum number of results to return")
|
|
694
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
685
695
|
|
|
686
696
|
|
|
687
697
|
class ListAuthorizedPropertiesRequest(BaseModel):
|
|
688
698
|
"""Request parameters for discovering which publishers this agent is authorized to represent"""
|
|
689
699
|
|
|
690
700
|
publisher_domains: list[str] | None = Field(None, description="Filter to specific publisher domains (optional). If omitted, returns all publishers this agent represents.")
|
|
701
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
691
702
|
|
|
692
703
|
|
|
693
704
|
class ListCreativeFormatsRequest(BaseModel):
|
|
@@ -702,6 +713,7 @@ class ListCreativeFormatsRequest(BaseModel):
|
|
|
702
713
|
min_height: int | None = Field(None, description="Minimum height in pixels (inclusive). Returns formats with height >= this value.")
|
|
703
714
|
is_responsive: bool | None = Field(None, description="Filter for responsive formats that adapt to container size. When true, returns formats without fixed dimensions.")
|
|
704
715
|
name_search: str | None = Field(None, description="Search for formats by name (case-insensitive partial match)")
|
|
716
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
705
717
|
|
|
706
718
|
|
|
707
719
|
class ListCreativesRequest(BaseModel):
|
|
@@ -714,6 +726,7 @@ class ListCreativesRequest(BaseModel):
|
|
|
714
726
|
include_performance: bool | None = Field(None, description="Include aggregated performance metrics in response")
|
|
715
727
|
include_sub_assets: bool | None = Field(None, description="Include sub-assets (for carousel/native formats) in response")
|
|
716
728
|
fields: list[Literal["creative_id", "name", "format", "status", "created_date", "updated_date", "tags", "assignments", "performance", "sub_assets"]] | None = Field(None, description="Specific fields to include in response (omit for all fields)")
|
|
729
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
717
730
|
|
|
718
731
|
|
|
719
732
|
class PackageRequest(BaseModel):
|
|
@@ -741,6 +754,7 @@ class ProvidePerformanceFeedbackRequest(BaseModel):
|
|
|
741
754
|
creative_id: str | None = Field(None, description="Specific creative asset (if feedback is creative-specific)")
|
|
742
755
|
metric_type: Literal["overall_performance", "conversion_rate", "brand_lift", "click_through_rate", "completion_rate", "viewability", "brand_safety", "cost_efficiency"] | None = Field(None, description="The business metric being measured")
|
|
743
756
|
feedback_source: Literal["buyer_attribution", "third_party_measurement", "platform_analytics", "verification_partner"] | None = Field(None, description="Source of the performance data")
|
|
757
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agentsmust echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
744
758
|
|
|
745
759
|
|
|
746
760
|
class SyncCreativesRequest(BaseModel):
|
|
@@ -753,6 +767,7 @@ class SyncCreativesRequest(BaseModel):
|
|
|
753
767
|
dry_run: bool | None = Field(None, description="When true, preview changes without applying them. Returns what would be created/updated/deleted.")
|
|
754
768
|
validation_mode: Literal["strict", "lenient"] | None = Field(None, description="Validation strictness. 'strict' fails entire sync on any validation error. 'lenient' processes valid creatives and reports errors.")
|
|
755
769
|
push_notification_config: PushNotificationConfig | None = Field(None, description="Optional webhook configuration for async sync notifications. Publisher will send webhook when sync completes if operation takes longer than immediate response time (typically for large bulk operations or manual approval/HITL).")
|
|
770
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
756
771
|
|
|
757
772
|
|
|
758
773
|
class TasksGetRequest(BaseModel):
|
|
@@ -760,6 +775,7 @@ class TasksGetRequest(BaseModel):
|
|
|
760
775
|
|
|
761
776
|
task_id: str = Field(description="Unique identifier of the task to retrieve")
|
|
762
777
|
include_history: bool | None = Field(None, description="Include full conversation history for this task (may increase response size)")
|
|
778
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
763
779
|
|
|
764
780
|
|
|
765
781
|
class TasksListRequest(BaseModel):
|
|
@@ -769,6 +785,7 @@ class TasksListRequest(BaseModel):
|
|
|
769
785
|
sort: dict[str, Any] | None = Field(None, description="Sorting parameters")
|
|
770
786
|
pagination: dict[str, Any] | None = Field(None, description="Pagination parameters")
|
|
771
787
|
include_history: bool | None = Field(None, description="Include full conversation history for each task (may significantly increase response size)")
|
|
788
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
772
789
|
|
|
773
790
|
|
|
774
791
|
class UpdateMediaBuyRequest(BaseModel):
|
|
@@ -781,6 +798,7 @@ class UpdateMediaBuyRequest(BaseModel):
|
|
|
781
798
|
end_time: str | None = Field(None, description="New end date/time in ISO 8601 format")
|
|
782
799
|
packages: list[dict[str, Any]] | None = Field(None, description="Package-specific updates")
|
|
783
800
|
push_notification_config: PushNotificationConfig | None = Field(None, description="Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.")
|
|
801
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context included in the request payload. Agents must echo this value back unchanged in responses and webhooks. Use for UI/session hints, correlation tokens, or tracking metadata.")
|
|
784
802
|
|
|
785
803
|
|
|
786
804
|
# Response containing the transformed or generated creative manifest, ready for use with preview_creative or sync_creatives. Returns either the complete creative manifest OR error information, never both.
|
|
@@ -791,6 +809,7 @@ class BuildCreativeResponseVariant1(BaseModel):
|
|
|
791
809
|
model_config = ConfigDict(extra="forbid")
|
|
792
810
|
|
|
793
811
|
creative_manifest: CreativeManifest = Field(description="The generated or transformed creative manifest")
|
|
812
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
794
813
|
|
|
795
814
|
|
|
796
815
|
class BuildCreativeResponseVariant2(BaseModel):
|
|
@@ -799,6 +818,7 @@ class BuildCreativeResponseVariant2(BaseModel):
|
|
|
799
818
|
model_config = ConfigDict(extra="forbid")
|
|
800
819
|
|
|
801
820
|
errors: list[Error] = Field(description="Array of errors explaining why creative generation failed")
|
|
821
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
802
822
|
|
|
803
823
|
|
|
804
824
|
# Union type for Build Creative Response
|
|
@@ -818,6 +838,7 @@ class GetMediaBuyDeliveryResponse(BaseModel):
|
|
|
818
838
|
aggregated_totals: dict[str, Any] | None = Field(None, description="Combined metrics across all returned media buys. Only included in API responses (get_media_buy_delivery), not in webhook notifications.")
|
|
819
839
|
media_buy_deliveries: list[dict[str, Any]] = Field(description="Array of delivery data for media buys. When used in webhook notifications, may contain multiple media buys aggregated by publisher. When used in get_media_buy_delivery API responses, typically contains requested media buys.")
|
|
820
840
|
errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., missing delivery data, reporting platform issues)")
|
|
841
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
821
842
|
|
|
822
843
|
|
|
823
844
|
class GetProductsResponse(BaseModel):
|
|
@@ -825,6 +846,7 @@ class GetProductsResponse(BaseModel):
|
|
|
825
846
|
|
|
826
847
|
products: list[Product] = Field(description="Array of matching products")
|
|
827
848
|
errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., product filtering issues)")
|
|
849
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
828
850
|
|
|
829
851
|
|
|
830
852
|
class GetSignalsResponse(BaseModel):
|
|
@@ -832,6 +854,7 @@ class GetSignalsResponse(BaseModel):
|
|
|
832
854
|
|
|
833
855
|
signals: list[dict[str, Any]] = Field(description="Array of matching signals")
|
|
834
856
|
errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., signal discovery or pricing issues)")
|
|
857
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
835
858
|
|
|
836
859
|
|
|
837
860
|
class ListAuthorizedPropertiesResponse(BaseModel):
|
|
@@ -844,6 +867,7 @@ class ListAuthorizedPropertiesResponse(BaseModel):
|
|
|
844
867
|
advertising_policies: str | None = Field(None, description="Publisher's advertising content policies, restrictions, and guidelines in natural language. May include prohibited categories, blocked advertisers, restricted tactics, brand safety requirements, or links to full policy documentation.")
|
|
845
868
|
last_updated: str | None = Field(None, description="ISO 8601 timestamp of when the agent's publisher authorization list was last updated. Buyers can use this to determine if their cached publisher adagents.json files might be stale.")
|
|
846
869
|
errors: list[Error] | None = Field(None, description="Task-specific errors and warnings (e.g., property availability issues)")
|
|
870
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
847
871
|
|
|
848
872
|
|
|
849
873
|
class ListCreativeFormatsResponse(BaseModel):
|
|
@@ -852,6 +876,7 @@ class ListCreativeFormatsResponse(BaseModel):
|
|
|
852
876
|
formats: list[Format] = Field(description="Full format definitions for all formats this agent supports. Each format's authoritative source is indicated by its agent_url field.")
|
|
853
877
|
creative_agents: list[dict[str, Any]] | None = Field(None, description="Optional: Creative agents that provide additional formats. Buyers can recursively query these agents to discover more formats. No authentication required for list_creative_formats.")
|
|
854
878
|
errors: list[Error] | None = Field(None, description="Task-specific errors and warnings")
|
|
879
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
855
880
|
|
|
856
881
|
|
|
857
882
|
class ListCreativesResponse(BaseModel):
|
|
@@ -862,6 +887,7 @@ class ListCreativesResponse(BaseModel):
|
|
|
862
887
|
creatives: list[dict[str, Any]] = Field(description="Array of creative assets matching the query")
|
|
863
888
|
format_summary: dict[str, Any] | None = Field(None, description="Breakdown of creatives by format type")
|
|
864
889
|
status_summary: dict[str, Any] | None = Field(None, description="Breakdown of creatives by status")
|
|
890
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
865
891
|
|
|
866
892
|
|
|
867
893
|
# Response payload for provide_performance_feedback task. Returns either success confirmation OR error information, never both.
|
|
@@ -872,6 +898,7 @@ class ProvidePerformanceFeedbackResponseVariant1(BaseModel):
|
|
|
872
898
|
model_config = ConfigDict(extra="forbid")
|
|
873
899
|
|
|
874
900
|
success: Literal[True] = Field(description="Whether the performance feedback was successfully received")
|
|
901
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
875
902
|
|
|
876
903
|
|
|
877
904
|
class ProvidePerformanceFeedbackResponseVariant2(BaseModel):
|
|
@@ -880,6 +907,7 @@ class ProvidePerformanceFeedbackResponseVariant2(BaseModel):
|
|
|
880
907
|
model_config = ConfigDict(extra="forbid")
|
|
881
908
|
|
|
882
909
|
errors: list[Error] = Field(description="Array of errors explaining why feedback was rejected (e.g., invalid measurement period, missing campaign data)")
|
|
910
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
883
911
|
|
|
884
912
|
|
|
885
913
|
# Union type for Provide Performance Feedback Response
|
|
@@ -900,6 +928,7 @@ class TasksGetResponse(BaseModel):
|
|
|
900
928
|
progress: dict[str, Any] | None = Field(None, description="Progress information for long-running tasks")
|
|
901
929
|
error: dict[str, Any] | None = Field(None, description="Error details for failed tasks")
|
|
902
930
|
history: list[dict[str, Any]] | None = Field(None, description="Complete conversation history for this task (only included if include_history was true in request)")
|
|
931
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
903
932
|
|
|
904
933
|
|
|
905
934
|
class TasksListResponse(BaseModel):
|
|
@@ -908,6 +937,7 @@ class TasksListResponse(BaseModel):
|
|
|
908
937
|
query_summary: dict[str, Any] = Field(description="Summary of the query that was executed")
|
|
909
938
|
tasks: list[dict[str, Any]] = Field(description="Array of tasks matching the query criteria")
|
|
910
939
|
pagination: dict[str, Any] = Field(description="Pagination information")
|
|
940
|
+
context: dict[str, Any] | None = Field(None, description="Initiator-provided context echoed inside the task payload. Opaque metadata such as UI/session hints, correlation tokens, or tracking identifiers.")
|
|
911
941
|
|
|
912
942
|
|
|
913
943
|
|
|
@@ -917,6 +947,7 @@ class TasksListResponse(BaseModel):
|
|
|
917
947
|
# The simple code generator produces type aliases (e.g., PreviewCreativeRequest = Any)
|
|
918
948
|
# for complex schemas that use oneOf. We override them here with proper Pydantic classes
|
|
919
949
|
# to maintain type safety and enable batch API support.
|
|
950
|
+
# Note: All classes inherit from BaseModel (which is aliased to AdCPBaseModel for exclude_none).
|
|
920
951
|
|
|
921
952
|
|
|
922
953
|
class FormatId(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adcp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Official Python client for the Ad Context Protocol (AdCP)
|
|
5
5
|
Author-email: AdCP Community <maintainers@adcontextprotocol.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -461,6 +461,50 @@ auth = index.get_agent_authorizations("https://agent-x.com")
|
|
|
461
461
|
premium = index.find_agents_by_property_tags(["premium", "ctv"])
|
|
462
462
|
```
|
|
463
463
|
|
|
464
|
+
## Publisher Authorization Validation
|
|
465
|
+
|
|
466
|
+
Verify sales agents are authorized to sell publisher properties via adagents.json:
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
from adcp import (
|
|
470
|
+
fetch_adagents,
|
|
471
|
+
verify_agent_authorization,
|
|
472
|
+
verify_agent_for_property,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Fetch and parse adagents.json from publisher
|
|
476
|
+
adagents_data = await fetch_adagents("publisher.com")
|
|
477
|
+
|
|
478
|
+
# Verify agent authorization for a property
|
|
479
|
+
is_authorized = verify_agent_authorization(
|
|
480
|
+
adagents_data=adagents_data,
|
|
481
|
+
agent_url="https://sales-agent.example.com",
|
|
482
|
+
property_type="website",
|
|
483
|
+
property_identifiers=[{"type": "domain", "value": "publisher.com"}]
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Or use convenience wrapper (fetch + verify in one call)
|
|
487
|
+
is_authorized = await verify_agent_for_property(
|
|
488
|
+
publisher_domain="publisher.com",
|
|
489
|
+
agent_url="https://sales-agent.example.com",
|
|
490
|
+
property_identifiers=[{"type": "domain", "value": "publisher.com"}],
|
|
491
|
+
property_type="website"
|
|
492
|
+
)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Domain Matching Rules:**
|
|
496
|
+
- Exact match: `example.com` matches `example.com`
|
|
497
|
+
- Common subdomains: `www.example.com` matches `example.com`
|
|
498
|
+
- Wildcards: `api.example.com` matches `*.example.com`
|
|
499
|
+
- Protocol-agnostic: `http://agent.com` matches `https://agent.com`
|
|
500
|
+
|
|
501
|
+
**Use Cases:**
|
|
502
|
+
- Sales agents verify authorization before accepting media buys
|
|
503
|
+
- Publishers test their adagents.json files
|
|
504
|
+
- Developer tools build authorization validators
|
|
505
|
+
|
|
506
|
+
See `examples/adagents_validation.py` for complete examples.
|
|
507
|
+
|
|
464
508
|
## CLI Tool
|
|
465
509
|
|
|
466
510
|
The `adcp` command-line tool provides easy interaction with AdCP agents without writing code.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
adcp/__init__.py,sha256=
|
|
1
|
+
adcp/__init__.py,sha256=AssQu9RgVTit-LqDxtQ-pgKuxjoDQr67HhiIxKMmSFA,7612
|
|
2
2
|
adcp/__main__.py,sha256=Avy_C71rruh2lOuojvuXDj09tkFOaek74nJ-dbx25Sw,12838
|
|
3
|
+
adcp/adagents.py,sha256=thq6qZScRHLWrRR4DjGlZzHpC1ZssQqhc5l0pbKSa-Q,17483
|
|
3
4
|
adcp/client.py,sha256=4qoFNDT5swzi4w5bnWJ5nVTG5JIL0xnLJOJMGeMyci4,28412
|
|
4
5
|
adcp/config.py,sha256=Vsy7ZPOI8G3fB_i5Nk-CHbC7wdasCUWuKlos0fwA0kY,2017
|
|
5
|
-
adcp/exceptions.py,sha256=
|
|
6
|
+
adcp/exceptions.py,sha256=1aZEWpaM92OxD2jl9yKsqJp5ReSWaj0S0DFhxChhLlA,6732
|
|
6
7
|
adcp/simple.py,sha256=FgPYWT32BNXkQz07r2x2gXgOmOikWLi88SzN5UIVSiU,10440
|
|
7
8
|
adcp/protocols/__init__.py,sha256=6UFwACQ0QadBUzy17wUROHqsJDp8ztPW2jzyl53Zh_g,262
|
|
8
9
|
adcp/protocols/a2a.py,sha256=FHgc6G_eU2qD0vH7_RyS1eZvUFSb2j3-EsceoHPi384,12467
|
|
@@ -10,17 +11,18 @@ adcp/protocols/base.py,sha256=vBHD23Fzl_CCk_Gy9nvSbBYopcJlYkYyzoz-rhI8wHg,5214
|
|
|
10
11
|
adcp/protocols/mcp.py,sha256=d9uSpGd0BKvQ0JxztkfDvHwoDrDYhuiw5oivpYOAbmM,16647
|
|
11
12
|
adcp/testing/__init__.py,sha256=ZWp_floWjVZfy8RBG5v_FUXQ8YbN7xjXvVcX-_zl_HU,1416
|
|
12
13
|
adcp/testing/test_helpers.py,sha256=4n8fZYy1cVpjZpFW2SxBzpC8fmY-MBFrzY4tIPqe4rQ,10028
|
|
13
|
-
adcp/types/__init__.py,sha256=
|
|
14
|
+
adcp/types/__init__.py,sha256=FXm4210pkzOIQQEgpe-EeLLd7mxofzEgKLGl1r8fj4o,465
|
|
15
|
+
adcp/types/base.py,sha256=QoEuVfI4yzefup0dc2KN11AcJTbcGxRep7xOw5hXfs8,837
|
|
14
16
|
adcp/types/core.py,sha256=RXkKCWCXS9BVJTNpe3Opm5O1I_LaQPMUuVwa-ipvS1Q,4839
|
|
15
|
-
adcp/types/generated.py,sha256=
|
|
17
|
+
adcp/types/generated.py,sha256=NY6A6eBw8wscSqJlLhW4OFvhTaZegcNR1hQ5VU51IFM,81526
|
|
16
18
|
adcp/types/tasks.py,sha256=Ae9TSwG2F7oWXTcl4TvLhAzinbQkHNGF1Pc0q8RMNNM,23424
|
|
17
19
|
adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
|
|
18
20
|
adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
|
|
19
21
|
adcp/utils/preview_cache.py,sha256=8_2qs5CgrHv1_WOnD4bs43VWueu-rcZRu5PZMQ_lyuE,17573
|
|
20
22
|
adcp/utils/response_parser.py,sha256=uPk2vIH-RYZmq7y3i8lC4HTMQ3FfKdlgXKTjgJ1955M,6253
|
|
21
|
-
adcp-1.
|
|
22
|
-
adcp-1.
|
|
23
|
-
adcp-1.
|
|
24
|
-
adcp-1.
|
|
25
|
-
adcp-1.
|
|
26
|
-
adcp-1.
|
|
23
|
+
adcp-1.6.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
|
|
24
|
+
adcp-1.6.0.dist-info/METADATA,sha256=9p6HjEycLX_f45n5H1l_gsDlLU67EpDJM3jBWZhLcqk,21345
|
|
25
|
+
adcp-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
26
|
+
adcp-1.6.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
|
|
27
|
+
adcp-1.6.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
|
|
28
|
+
adcp-1.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|