Skip to content

bug(adagents): fetch_agent_authorizations_from_directory uses ?since= for cursor; spec says ?cursor= #809

Description

@bokelley

Symptom

fetch_agent_authorizations_from_directory (adcp/adagents.py) sends pagination cursors via ?since=<cursor>. The spec at docs/aao/directory-api.mdx defines two distinct query parameters:

param type purpose
since ISO 8601 Return only publishers whose last_verified_atsince. Incremental sync.
cursor opaque string Pagination cursor returned by a prior response.

The SDK's docstring conflates them:

since: Optional opaque cursor or RFC 3339 timestamp from a prior
    ``directory_indexed_at`` — passed through as ``?since=...``

Production server enforces the spec — ?since=<cursor> returns 400:

$ curl -s -o /dev/null -w "%{http_code}\n" \
    "https://agenticadvertising.org/api/v1/agents/https%3A%2F%2Finterchange.io/publishers?since=YWxsbnVyc2VzLmNvbQ"
400

$ curl -s -o /dev/null -w "%{http_code}\n" \
    "https://agenticadvertising.org/api/v1/agents/https%3A%2F%2Finterchange.io/publishers?cursor=YWxsbnVyc2VzLmNvbQ"
200

Impact

Pagination is broken end-to-end for the Python SDK. Caught against Raptive's ~6,800-publisher set:

result = await fetch_agent_authorizations_from_directory(
    "https://interchange.io",
    directory_url="https://agenticadvertising.org/api",
    since=cursor_from_previous_page,
)
# AdagentsValidationError: Agent-publishers directory returned HTTP 400

First page works (no cursor passed). Second page onward 400s. So today the SDK can fetch the first 200 publishers of a 6,800-publisher network and then fails.

Reproduction

import asyncio, urllib.parse, httpx

async def main():
    base = "https://agenticadvertising.org/api/v1/agents"
    agent = urllib.parse.quote("https://interchange.io", safe="")
    url = f"{base}/{agent}/publishers"

    async with httpx.AsyncClient(timeout=30) as c:
        page1 = (await c.get(url)).json()
        cursor = page1["next_cursor"]

        # SDK behavior — fails
        r = await c.get(url, params={"since": cursor})
        print(f"  since=cursor → HTTP {r.status_code}")  # 400

        # Spec behavior — works
        r = await c.get(url, params={"cursor": cursor})
        print(f"  cursor=cursor → HTTP {r.status_code}")  # 200

asyncio.run(main())

Fix

Add a distinct cursor: str | None = None keyword to fetch_agent_authorizations_from_directory, separate from since:

async def fetch_agent_authorizations_from_directory(
    agent_url: str,
    *,
    directory_url: str,
    since: str | None = None,          # ISO 8601 timestamp — incremental sync
    cursor: str | None = None,         # opaque pagination cursor
    include: list[str] | None = None,
    timeout: float = 10.0,
    client: httpx.AsyncClient | None = None,
) -> AgentAuthorizationsDirectoryResult:
    ...

Send since and cursor as separate query params. Update docstring to stop conflating them. Treat as a v5.7.0 follow-on bugfix.

Companion server bug

The same investigation found that production mounts the endpoint at /api/v1/... instead of the spec's /v1/.... Filed at adcontextprotocol/adcp#4924. Independent — fixing either does not fix the other.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions