Agentic advertising picked up a governance layer this year. PubMatic shipped one on AgenticOS: platform-wide constraints plus per-buyer policies, pre-approved creative and inventory libraries, human approval workflows with authenticated approvers, full audit logging, and drift detection that flags anomalous agent behaviour before it executes. For buyers on the Ad Context Protocol, it runs an independent check on transactions touching the platform, whichever agent initiated them.
Every one of those controls answers the same question: was this agent allowed to do that?
None of them answers a different one: is the thing it produced actually valid?
Budget caps, inventory allowlists, and geo constraints say nothing about whether the VAST document that ends up in the bid response has a valid Impression node, HTTPS media files, or a wrapper chain under the depth limit. Drift detection is anomaly detection over agent behaviour, not schema conformance. Policy governance and payload conformance are different layers, and the second one has been mostly empty.
I maintain vastlint, a VAST validator, and it exposes an AdCP governance surface: create content standards, calibrate creative against them before launch, validate that delivered creative met them. So I'd been quietly claiming to fill that gap.
Then I actually ran the conformance suite against it.
Declaration is not conformance
An AdCP agent declares what it supports in get_adcp_capabilities:
{
"supported_protocols": ["governance"],
"specialisms": ["content-standards"]
}
That's a claim, not a credential. The protocol treats it as one: declaring a protocol or specialism commits you to passing the matching storyboards. A storyboard is a scripted sequence of tool calls with a response-schema assertion on every step, plus cross-step invariants. The runner reads your declaration, selects the storyboards it obligates, and executes them against your live endpoint.
The whole thing ships as a CLI:
npx @adcp/sdk@latest storyboard run https://your-agent.example.com/mcp \
--test-kit dist/compliance/3.1.1/test-kits/acme-outdoor.yaml
I expected a clean run. The tools worked. I'd tested them. I'd been using them.
Eight things it found
1. Unauthenticated calls returned HTTP 200. Protected tools answered an anonymous caller with a 200 carrying {"adcp_error": {"code": "REQUIRES_AUTH"}} in the body. There was even a comment explaining why, so runners could branch on the error code instead of treating it as a protocol failure. The security baseline is explicit that anything other than 401 or 403 is non-conformant, and 401 has to carry WWW-Authenticate. My reasoning had been confidently backwards, in writing, for months.
2. The creative library was readable anonymously. list_creatives was excluded from the protected set because it always returned an empty collection, so what was the harm. The spec designates it the default probe target for exactly this test. It was the one endpoint guaranteed to be checked.
3. A capability filter could violate its own schema. get_adcp_capabilities takes an optional protocols filter. Mine intersected it with what the agent supports, so filtering for something unsupported returned "supported_protocols": []. The response schema sets minItems: 1. The filter is meant to scope which capability blocks come back, not to narrow the declaration of what the agent implements.
4. No release-precision version anywhere. The protocol moved to MAJOR.MINOR version negotiation, with sellers advertising adcp.supported_versions and echoing adcp_version on each response. I emitted neither. Advisory at 3.1, required at 3.2, so this was a deadline I hadn't noticed.
5. A response shape that had drifted. list_creatives was missing the required query_summary and pagination.has_more, and returned two pagination fields the schema forbids. It had been valid once.
6. An enum hole that broke reads permanently. create_content_standards accepted a scope with any channel value. Store {"channels": ["video"]}, which is not in the AdCP channel enum, and every subsequent list_content_standards for that brand fails schema validation forever, because the bad value gets replayed on read. Write-side validation was missing, so one bad request poisoned a tenant. This one was found by accident: a record from an old test run was sitting in my local database and blocked the first run I attempted.
7. A table that grew without bound. Idempotency replay rows had a TTL that was only ever checked on read. Nothing deleted expired rows. Fine at my traffic, until I registered for a compliance heartbeat that mutates with a fresh idempotency key every hour, forever.
8. A plain GET that never terminated. This is my favourite. GET /mcp opened the legacy MCP HTTP+SSE stream: correct behaviour for that transport, 200 with text/event-stream, keepalive pings every 25 seconds, connection held open indefinitely. The conformance runner never touched it, because it POSTs over Streamable HTTP. The registry's capability crawler does issue a plain GET, and reads the response to completion. So it hung, timed out, and recorded the agent as offline while the compliance suite reported everything passing on the same URL. Two probes, two transports, opposite verdicts.
The fix was to serve SSE only to clients that actually negotiate it:
if (request.method === "GET") {
const accept = request.headers.get("Accept") ?? "";
if (accept.includes("text/event-stream")) {
return handleMcpSse(request);
}
return mcpEndpointDescriptor(request); // bounded JSON, ~100ms
}
What I take from this
Every one of these was invisible to me. Not hard to find, not subtle, invisible. My own tests passed because they tested the behaviour I'd implemented, against the shape I believed was correct. Six of the eight were failures of my belief about the spec, not failures of code against my intent. Tests you write yourself cannot catch that class of error, because the same misunderstanding authors both the code and the test.
That is the entire argument for machine-checkable conformance, and I've been making it in the abstract for a while. Making it about my own code is considerably more persuasive, mostly to me.
It generalises past this one agent. The advertising stack runs on specs that are enormous, widely implemented, and almost entirely unenforced. VAST, OpenRTB, adagents.json, brand.json: all of them documents people implement from, none of them documents anything checks you against, unless somebody builds the checker. Where a conformance suite exists, running it against a well-intentioned implementation still finds eight things. Where one doesn't exist, nobody finds anything, and everyone assumes it's fine.
The governance layer being built for agentic buying inherits that problem directly. You can enforce that an agent stayed inside its budget and its allowlist and still ship a creative that no player will render, because nothing in the policy layer parses the payload. Those are complementary jobs and only one of them is currently being built.
Running it yourself
If you operate an AdCP agent, the suite takes minutes:
# see what your declarations actually obligate you to
npx @adcp/sdk@latest storyboard show --specialism content-standards
# run it
npx @adcp/sdk@latest storyboard run <your-url> --test-kit <kit.yaml>
Two traps worth knowing. --test-kit is required for the security baseline to grade at all, and it's missing from the CLI's help output. Without it the auth phases skip silently and the storyboard fails with "no auth mechanism verified", which reads like an auth bug in your agent when nothing is wrong. And local runs need --allow-http, which stamps the result as not publishable, so anything you intend to cite has to come from an HTTPS endpoint.
After the eight fixes, vastlint passes 28 steps with 0 failures: Core Protocol 34 of 34 scenarios, Governance 8 of 8, on the AdCP 3.1.1 bundle. There's a longer writeup of how the grading works and what the content-standards specialism covers in the docs.
I'd genuinely like to know whether other people's agents fare better than mine did. My guess is that the ones nobody has graded are in roughly the same shape, and simply don't know it yet.
Top comments (0)