Skip to content

Commit 5160ab0

Browse files
bokelleyclaude
andcommitted
feat(adagents): publisher_domains compact form, revoked_publisher_domains, streaming fetch caps (#729)
Implements full scope of issue #729 plus the scope-expansion comment: - publisher-property-selector schema gains optional publisher_domains[] compact form (XOR with publisher_domain; by_id selectors excluded from compact form). - adagents.json gains top-level revoked_publisher_domains[] with Reason enum. - Resolver: _fanout_publisher_properties expands compact entries one-per-domain; filter_revoked_selectors applies revocation precedence over both publisher_properties[] selectors and top-level properties[]. - Fetch path rewritten onto httpx.AsyncClient.stream with two-tier size caps (MAX_POINTER_BYTES=5MiB first hop, MAX_AUTHORITATIVE_BYTES=20MiB second hop) and Content-Length pre-check. New fetch_adagents_with_cache sends If-None-Match / If-Modified-Since; 304 treated as cache-lifetime refresh. - Runtime validation: two XORs enforced on publisher_properties[] entries; by_id rejects publisher_domains; validate_revoked_publisher_domain_entry plumbed into validate_adagents. - 24 new tests across four classes; 122 existing adagents tests ported onto the new streaming mock helper. Cached schemas under schemas/cache/3.0/ are hand-patched and will be overwritten by the next \`make regenerate-schemas\` once upstream 3.0.10+ ships and ADCP_VERSION is bumped — that's fine; the runtime/test changes are independent of the cached schema text. Per-authorized_agents[] last_updated (also discussed in the spec PR thread) is intentionally not included — the merged upstream spec PR does not carry that field, confirmed against the latest schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 12f8ffe commit 5160ab0

10 files changed

Lines changed: 1331 additions & 279 deletions

File tree

schemas/cache/3.0/adagents.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,40 @@
8989
},
9090
"minItems": 1
9191
},
92+
"revoked_publisher_domains": {
93+
"type": "array",
94+
"description": "Publisher domains explicitly removed from this managed network. Validators MUST treat any publisher domain listed here as no-longer-authorized, taking precedence over any appearance of the same domain in `authorized_agents[].publisher_properties[].publisher_domain` / `.publisher_domains[]` or in top-level `properties[].publisher_domain`. Lets a network propagate per-publisher revocations on the next refresh instead of waiting for the file-level 7-day cache cap. Validators MUST hold previously-observed `(publisher_domain, revoked_at)` tuples for 7 days from the validator's first observation, even if the entry vanishes from a subsequent fetch — this closes the rollback gap where an attacker re-serves a stale file with the revocation removed. Networks SHOULD retain entries for at least 7 days after `revoked_at` so validators that didn't observe the original entry still pick it up on refresh.",
95+
"items": {
96+
"type": "object",
97+
"properties": {
98+
"publisher_domain": {
99+
"type": "string",
100+
"description": "Publisher domain being revoked. Matches against the same canonicalized form used in `publisher_properties[].publisher_domain`.",
101+
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
102+
},
103+
"revoked_at": {
104+
"type": "string",
105+
"format": "date-time",
106+
"description": "ISO 8601 timestamp when this publisher was revoked. Validators MAY use this to order revocations against their own cached state."
107+
},
108+
"reason": {
109+
"type": "string",
110+
"enum": [
111+
"relationship_ended",
112+
"compliance_violation",
113+
"publisher_request",
114+
"other"
115+
],
116+
"description": "Reason for revocation. Operator-internal self-classification for review routing — not a public accusation. `relationship_ended` is the routine commercial case. `compliance_violation` SHOULD be used only when the network has itself determined the publisher is out of policy; for un-adjudicated third-party allegations (regulator inquiries, advertiser complaints, ongoing investigations), use `other` to avoid making a discoverable adverse statement. `publisher_request` is for publisher-initiated exits."
117+
}
118+
},
119+
"required": [
120+
"publisher_domain",
121+
"revoked_at"
122+
],
123+
"additionalProperties": true
124+
}
125+
},
92126
"collections": {
93127
"type": "array",
94128
"description": "Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.",

schemas/cache/3.0/core/publisher-property-selector.json

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,72 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
33
"title": "Publisher Property Selector",
4-
"description": "Selects properties from a publisher's adagents.json. Used for both product definitions and agent authorization. Supports three selection patterns: all properties, specific IDs, or by tags.",
4+
"description": "Selects properties from a publisher's adagents.json. Used for both product definitions and agent authorization. Supports three selection patterns: all properties, specific IDs, or by tags. Each selector targets one publisher via `publisher_domain` (string) or a fan-out across many publishers that share the same selector via `publisher_domains` (array). Exactly one of `publisher_domain` or `publisher_domains` MUST be present. When `publisher_domains` is used, the selector is logically equivalent to repeating the same entry once per listed domain.",
55
"discriminator": {
66
"propertyName": "selection_type"
77
},
88
"oneOf": [
99
{
1010
"type": "object",
11-
"description": "Select all properties from the publisher domain",
11+
"description": "Select all properties from one publisher domain, or from each publisher domain when `publisher_domains` is used.",
1212
"properties": {
1313
"publisher_domain": {
1414
"type": "string",
15-
"description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com')",
15+
"description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.",
1616
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
1717
},
18+
"publisher_domains": {
19+
"type": "array",
20+
"description": "Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.",
21+
"items": {
22+
"type": "string",
23+
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
24+
},
25+
"minItems": 1,
26+
"uniqueItems": true
27+
},
1828
"selection_type": {
1929
"type": "string",
2030
"const": "all",
21-
"description": "Discriminator indicating all properties from this publisher are included"
31+
"description": "Discriminator indicating all properties from each addressed publisher are included"
2232
}
2333
},
2434
"required": [
25-
"publisher_domain",
2635
"selection_type"
2736
],
37+
"allOf": [
38+
{
39+
"not": {
40+
"required": [
41+
"publisher_domain",
42+
"publisher_domains"
43+
]
44+
}
45+
},
46+
{
47+
"anyOf": [
48+
{
49+
"required": [
50+
"publisher_domain"
51+
]
52+
},
53+
{
54+
"required": [
55+
"publisher_domains"
56+
]
57+
}
58+
]
59+
}
60+
],
2861
"additionalProperties": true
2962
},
3063
{
3164
"type": "object",
32-
"description": "Select specific properties by ID",
65+
"description": "Select specific properties by ID. Single-publisher only — property IDs are publisher-scoped, so the compact `publisher_domains[]` form is intentionally NOT available for this selector. Use multiple `publisher_properties[]` entries (one per publisher) when each publisher's ID set differs.",
3366
"properties": {
3467
"publisher_domain": {
3568
"type": "string",
36-
"description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com')",
69+
"description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com').",
3770
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
3871
},
3972
"selection_type": {
@@ -59,33 +92,66 @@
5992
},
6093
{
6194
"type": "object",
62-
"description": "Select properties by tag membership",
95+
"description": "Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json — the common managed-network case where every represented site tags inventory with a shared label.",
6396
"properties": {
6497
"publisher_domain": {
6598
"type": "string",
66-
"description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com')",
99+
"description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.",
67100
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
68101
},
102+
"publisher_domains": {
103+
"type": "array",
104+
"description": "Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.",
105+
"items": {
106+
"type": "string",
107+
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
108+
},
109+
"minItems": 1,
110+
"uniqueItems": true
111+
},
69112
"selection_type": {
70113
"type": "string",
71114
"const": "by_tag",
72115
"description": "Discriminator indicating selection by property tags"
73116
},
74117
"property_tags": {
75118
"type": "array",
76-
"description": "Property tags from the publisher's adagents.json. Selector covers all properties with these tags",
119+
"description": "Property tags resolved against each addressed publisher's adagents.json. Selector covers all properties carrying any of these tags.",
77120
"items": {
78121
"$ref": "property-tag.json"
79122
},
80123
"minItems": 1
81124
}
82125
},
83126
"required": [
84-
"publisher_domain",
85127
"selection_type",
86128
"property_tags"
87129
],
130+
"allOf": [
131+
{
132+
"not": {
133+
"required": [
134+
"publisher_domain",
135+
"publisher_domains"
136+
]
137+
}
138+
},
139+
{
140+
"anyOf": [
141+
{
142+
"required": [
143+
"publisher_domain"
144+
]
145+
},
146+
{
147+
"required": [
148+
"publisher_domains"
149+
]
150+
}
151+
]
152+
}
153+
],
88154
"additionalProperties": true
89155
}
90156
]
91-
}
157+
}

src/adcp/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111
from importlib.metadata import version as _pkg_version
1212

1313
from adcp.adagents import (
14+
AdagentsCacheEntry,
1415
AdagentsEntryError,
16+
AdagentsFetchResult,
1517
AdagentsValidationReport,
1618
AdAgentsValidationResult,
1719
AuthorizationContext,
1820
DiscoveryMethod,
1921
EntryErrorKind,
2022
domain_matches,
2123
fetch_adagents,
24+
fetch_adagents_with_cache,
2225
fetch_agent_authorizations,
26+
filter_revoked_selectors,
2327
get_all_properties,
2428
get_all_tags,
2529
get_properties_by_agent,
@@ -813,13 +817,17 @@ def get_adcp_version() -> str:
813817
"PushNotificationConfig",
814818
# Adagents validation
815819
"AdAgentsValidationResult",
820+
"AdagentsCacheEntry",
816821
"AdagentsEntryError",
822+
"AdagentsFetchResult",
817823
"AdagentsValidationReport",
818824
"AuthorizationContext",
819825
"DiscoveryMethod",
820826
"EntryErrorKind",
821827
"fetch_adagents",
828+
"fetch_adagents_with_cache",
822829
"fetch_agent_authorizations",
830+
"filter_revoked_selectors",
823831
"validate_adagents_domain",
824832
"validate_adagents_structure",
825833
"verify_agent_authorization",

0 commit comments

Comments
 (0)