Skip to content

Commit 7f45742

Browse files
bokelleysangilish
andauthored
feat(adagents): add permissive property resolver (#863)
* feat(adagents): add permissive property resolver * fix(adagents): fail closed for permissive property fallback --------- Co-authored-by: sangilish <56685007+sangilish@users.noreply.github.com>
1 parent 28c4f64 commit 7f45742

4 files changed

Lines changed: 333 additions & 1 deletion

File tree

src/adcp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
get_all_tags,
3737
get_properties_by_agent,
3838
identifiers_match,
39+
resolve_properties_for_agent,
3940
validate_adagents_domain,
4041
validate_adagents_structure,
4142
verify_agent_authorization,
@@ -923,6 +924,7 @@ def get_adcp_version() -> str:
923924
"get_all_properties",
924925
"get_all_tags",
925926
"get_properties_by_agent",
927+
"resolve_properties_for_agent",
926928
# Test helpers
927929
"test_agent",
928930
"test_agent_a2a",

src/adcp/adagents.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
logger = logging.getLogger(__name__)
3535

3636
DiscoveryMethod = Literal["direct", "authoritative_location", "ads_txt_managerdomain"]
37+
PropertyResolutionMode = Literal["strict", "permissive"]
38+
_BARE_AUTHORIZED_AGENT_KEYS = {"url", "authorized_for"}
3739

3840

3941
# authorization_type discriminator -> required selector field, per the AdCP
@@ -1296,6 +1298,17 @@ def _resolve_agent_properties(
12961298
return []
12971299

12981300

1301+
def _is_bare_authorized_agent_entry(agent: dict[str, Any]) -> bool:
1302+
"""Return True only for the exact legacy bare-entry shape."""
1303+
return (
1304+
set(agent).issubset(_BARE_AUTHORIZED_AGENT_KEYS)
1305+
and isinstance(agent.get("url"), str)
1306+
and bool(agent["url"])
1307+
and isinstance(agent.get("authorized_for"), str)
1308+
and bool(agent["authorized_for"])
1309+
)
1310+
1311+
12991312
def _build_domain_index(
13001313
properties: list[dict[str, Any]],
13011314
) -> dict[str, list[dict[str, Any]]]:
@@ -1610,6 +1623,65 @@ def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) -> li
16101623
Raises:
16111624
AdagentsValidationError: If adagents_data is malformed
16121625
"""
1626+
return _resolve_properties_for_agent(adagents_data, agent_url, permissive_bare_top_level=False)
1627+
1628+
1629+
def resolve_properties_for_agent(
1630+
adagents_data: dict[str, Any],
1631+
agent_url: str,
1632+
*,
1633+
mode: PropertyResolutionMode = "strict",
1634+
) -> list[dict[str, Any]]:
1635+
"""Resolve properties for an agent with an explicit strict/permissive mode.
1636+
1637+
``mode="strict"`` is identical to :func:`get_properties_by_agent` and
1638+
only honors schema-conformant authorization selectors plus the historical
1639+
inline ``properties`` legacy shape.
1640+
1641+
``mode="permissive"`` keeps every strict selector behavior unchanged, but
1642+
treats one exact matching bare ``authorized_agents`` entry
1643+
(``{"url": ..., "authorized_for": ...}``) as authorizing the file's
1644+
top-level ``properties[]``. This is for operational binding of
1645+
non-conformant publisher files that list an agent URL without an
1646+
``authorization_type`` or selector. If the agent is not listed, has any
1647+
explicit or unknown selector field, or has multiple same-URL entries, the
1648+
resolver still returns the strict result.
1649+
1650+
Args:
1651+
adagents_data: Parsed adagents.json data
1652+
agent_url: URL of the agent to filter by
1653+
mode: ``"strict"`` for spec-conformant resolution, ``"permissive"``
1654+
to opt into bare-entry top-level property fallback.
1655+
1656+
Returns:
1657+
List of properties for the specified agent.
1658+
1659+
Raises:
1660+
AdagentsValidationError: If adagents_data is malformed
1661+
ValueError: If mode is not ``"strict"`` or ``"permissive"``
1662+
"""
1663+
if mode == "strict":
1664+
return _resolve_properties_for_agent(
1665+
adagents_data,
1666+
agent_url,
1667+
permissive_bare_top_level=False,
1668+
)
1669+
if mode == "permissive":
1670+
return _resolve_properties_for_agent(
1671+
adagents_data,
1672+
agent_url,
1673+
permissive_bare_top_level=True,
1674+
)
1675+
raise ValueError("mode must be 'strict' or 'permissive'")
1676+
1677+
1678+
def _resolve_properties_for_agent(
1679+
adagents_data: dict[str, Any],
1680+
agent_url: str,
1681+
*,
1682+
permissive_bare_top_level: bool,
1683+
) -> list[dict[str, Any]]:
1684+
"""Implementation shared by strict and opt-in permissive property resolution."""
16131685
if not isinstance(adagents_data, dict):
16141686
raise AdagentsValidationError("adagents_data must be a dictionary")
16151687

@@ -1636,6 +1708,7 @@ def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) -> li
16361708

16371709
domain_index = _build_domain_index(revoked_top_level)
16381710

1711+
matches: list[dict[str, Any]] = []
16391712
for agent in authorized_agents:
16401713
if not isinstance(agent, dict):
16411714
continue
@@ -1646,12 +1719,26 @@ def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) -> li
16461719

16471720
if normalize_url(agent_url_from_json) != normalized_agent_url:
16481721
continue
1722+
matches.append(agent)
16491723

1724+
resolved: list[dict[str, Any]] = []
1725+
for agent in matches:
16501726
# revoked_top_level pre-filters revoked domains from the per-domain
16511727
# index, so inline resolution honors revocation transparently.
1652-
resolved = _resolve_agent_properties(agent, revoked_top_level, domain_index)
1728+
for prop in _resolve_agent_properties(agent, revoked_top_level, domain_index):
1729+
if prop not in resolved:
1730+
resolved.append(prop)
1731+
1732+
if resolved:
16531733
return resolved
16541734

1735+
if (
1736+
permissive_bare_top_level
1737+
and len(matches) == 1
1738+
and _is_bare_authorized_agent_entry(matches[0])
1739+
):
1740+
return [p for p in revoked_top_level if isinstance(p, dict)]
1741+
16551742
return []
16561743

16571744

tests/fixtures/public_api_snapshot.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@
424424
"has_assets",
425425
"identifiers_match",
426426
"normalize_assets_required",
427+
"resolve_properties_for_agent",
427428
"sign_legacy_webhook",
428429
"sign_webhook",
429430
"test_agent",

0 commit comments

Comments
 (0)