3434logger = logging .getLogger (__name__ )
3535
3636DiscoveryMethod = 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+
12991312def _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
0 commit comments