@@ -456,17 +456,60 @@ def _resolve_well_known_redirect_url(
456456
457457
458458def normalize_url (url : str ) -> str :
459- """Normalize URL by removing protocol and trailing slash .
459+ """Return a protocol-agnostic URL key with host case/default ports normalized .
460460
461- Args:
462- url: URL to normalize
463-
464- Returns:
465- Normalized URL (domain/path without protocol or trailing slash)
461+ Paths remain case-sensitive. Query strings and fragments are ignored, and
462+ trailing path slashes are removed, preserving the existing matching rules.
463+ Only the default port for the supplied scheme is removed (80 for HTTP,
464+ 443 for HTTPS); non-default ports remain significant.
466465 """
467466 parsed = urlparse (url )
468- normalized = parsed .netloc + parsed .path
469- return normalized .rstrip ("/" )
467+ authority = parsed .netloc
468+ if parsed .hostname is not None :
469+ host = parsed .hostname .lower ()
470+ if ":" in host :
471+ host = f"[{ host } ]"
472+ port = parsed .port
473+ if port is not None and (parsed .scheme .lower (), port ) not in {("http" , 80 ), ("https" , 443 )}:
474+ host += f":{ port } "
475+ userinfo = authority .rsplit ("@" , 1 )[0 ] + "@" if "@" in authority else ""
476+ authority = userinfo + host
477+ return (authority + parsed .path ).rstrip ("/" )
478+
479+
480+ def find_authorized_agent_entries (
481+ adagents_data : dict [str , Any ], agent_url : str
482+ ) -> list [dict [str , Any ]]:
483+ """Return all entries listing an agent, independently of property binding.
484+
485+ A non-empty result distinguishes a listed agent whose selectors resolve to
486+ no properties from an unlisted agent. Entries are returned in document order
487+ without copying or modifying them. Malformed sibling entries and URL values
488+ are skipped. Matching preserves HTTP/HTTPS equivalence and path semantics.
489+
490+ Raises AdagentsValidationError if the document is not an object with an
491+ ``authorized_agents`` array.
492+ """
493+ if not isinstance (adagents_data , dict ):
494+ raise AdagentsValidationError ("adagents_data must be a dictionary" )
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+ normalized_agent_url = normalize_url (agent_url )
499+ matches : list [dict [str , Any ]] = []
500+ for agent in authorized_agents :
501+ if not isinstance (agent , dict ):
502+ continue
503+ entry_url = agent .get ("url" )
504+ if not isinstance (entry_url , str ) or not entry_url .strip ():
505+ continue
506+ try :
507+ normalized_entry_url = normalize_url (entry_url )
508+ except ValueError :
509+ continue
510+ if normalized_entry_url == normalized_agent_url :
511+ matches .append (agent )
512+ return matches
470513
471514
472515def domain_matches (property_domain : str , agent_domain_pattern : str ) -> bool :
@@ -584,30 +627,7 @@ def verify_agent_authorization(
584627 - Implements AdCP domain matching rules
585628 - Agent URLs are matched ignoring protocol and trailing slash
586629 """
587- # Validate structure
588- if not isinstance (adagents_data , dict ):
589- raise AdagentsValidationError ("adagents_data must be a dictionary" )
590-
591- authorized_agents = adagents_data .get ("authorized_agents" )
592- if not isinstance (authorized_agents , list ):
593- raise AdagentsValidationError ("adagents.json must have 'authorized_agents' array" )
594-
595- # Normalize the agent URL for comparison
596- normalized_agent_url = normalize_url (agent_url )
597-
598- # Check each authorized agent
599- for agent in authorized_agents :
600- if not isinstance (agent , dict ):
601- continue
602-
603- agent_url_from_json = agent .get ("url" , "" )
604- if not agent_url_from_json :
605- continue
606-
607- # Match agent URL (protocol-agnostic)
608- if normalize_url (agent_url_from_json ) != normalized_agent_url :
609- continue
610-
630+ for agent in find_authorized_agent_entries (adagents_data , agent_url ):
611631 # Found matching agent - now check properties
612632 properties = agent .get ("properties" )
613633
@@ -1915,12 +1935,7 @@ def _resolve_properties_for_agent(
19151935 permissive_bare_top_level : bool ,
19161936) -> list [dict [str , Any ]]:
19171937 """Implementation shared by strict and opt-in permissive property resolution."""
1918- if not isinstance (adagents_data , dict ):
1919- raise AdagentsValidationError ("adagents_data must be a dictionary" )
1920-
1921- authorized_agents = adagents_data .get ("authorized_agents" )
1922- if not isinstance (authorized_agents , list ):
1923- raise AdagentsValidationError ("adagents.json must have 'authorized_agents' array" )
1938+ matches = find_authorized_agent_entries (adagents_data , agent_url )
19241939
19251940 top_level_properties = adagents_data .get ("properties" , [])
19261941 if not isinstance (top_level_properties , list ):
@@ -1937,23 +1952,8 @@ def _resolve_properties_for_agent(
19371952 )
19381953 ]
19391954
1940- normalized_agent_url = normalize_url (agent_url )
1941-
19421955 domain_index = _build_domain_index (revoked_top_level )
19431956
1944- matches : list [dict [str , Any ]] = []
1945- for agent in authorized_agents :
1946- if not isinstance (agent , dict ):
1947- continue
1948-
1949- agent_url_from_json = agent .get ("url" , "" )
1950- if not agent_url_from_json :
1951- continue
1952-
1953- if normalize_url (agent_url_from_json ) != normalized_agent_url :
1954- continue
1955- matches .append (agent )
1956-
19571957 resolved : list [dict [str , Any ]] = []
19581958 for agent in matches :
19591959 # revoked_top_level pre-filters revoked domains from the per-domain
0 commit comments