Skip to content

feat: add RC2 governance support and full spec coverage#161

Merged
bokelley merged 6 commits intomainfrom
bokelley/governance-sdk
Mar 16, 2026
Merged

feat: add RC2 governance support and full spec coverage#161
bokelley merged 6 commits intomainfrom
bokelley/governance-sdk

Conversation

@bokelley
Copy link
Contributor

Summary

  • Integrate AdCP 3.0.0-rc.2 schemas and generated types
  • Add governance protocol surface: sync_plans, check_governance, report_plan_outcome, get_plan_audit_logs, get_creative_features
  • Wire RC2 tasks (get_media_buys, get_account_financials, report_usage, sync_audiences, sync_catalogs) through client, adapters, server handlers, MCP tools, and CLI
  • Add GovernanceHandler with input validation, delegation, and domain-specific not-supported stubs
  • Add consistent not-supported stubs for all RC2 tasks across ContentStandardsHandler and SponsoredIntelligenceHandler
  • Fix RootModel unwrap for 7 new union types with multi-line parenthesized wrapping
  • Fix TypeAdapter usage for GetPlanAuditLogsRequest union validation
  • Fix $ref resolution in extra policy test for versioned schema paths
  • Fix mypy builtin shadowing in get_property_list_response.py
  • Add registry slug path-traversal validation
  • Add test_spec_coverage.py regression test ensuring every task in schemas/cache/index.json is wired into client, handler, adapters, CLI, and MCP tools
  • Add governance test coverage: client adapter delegation, handler validation, property list CRUD, validation error paths, RC2 not-supported stubs

Test plan

  • ruff check src/ tests/ — all checks passed
  • mypy src/adcp/ — 0 errors (453 files)
  • pytest tests/ — 720 passed, 0 failed
  • Code review (3 rounds) — all must-fix items addressed
  • Security review — must-fix (TypeAdapter annotation) and should-fix (slug path traversal) addressed
  • No merge conflicts with main

🤖 Generated with Claude Code

Add governance protocol (sync_plans, check_governance, report_plan_outcome,
get_plan_audit_logs, get_creative_features) with GovernanceHandler, wire
RC2 tasks through client/server/adapters/CLI/MCP, add spec coverage tests,
and integrate PR #160's oneOf flattening. Remove stale SubAsset types
(schema removed in RC2), fix list field shadowing in
GetPropertyListResponse, add path-traversal validation to registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/governance-sdk branch from a53bfc1 to 096f693 Compare March 16, 2026 16:27
@bokelley
Copy link
Contributor Author

Review: AdCP 3.0 RC2 — Feedback from Salesagent Consumer

We reviewed this from the perspective of a downstream consumer (Prebid Sales Agent) that extends library types via inheritance and uses the client/MCP surface extensively. Overall this is solid work — test_spec_coverage.py is a great structural guard, the registry client is clean, and backward-compat aliases are appreciated. A few items to address:


Must Fix

1. FieldModel silently rebound to a different type

On main, FieldModel (exported from adcp.types) refers to the get_products field selection enum (media_buy.get_products_request.FieldModel). On this PR, it now points to brand.get_brand_identity_request.FieldModel — a completely different type. The original was renamed to Field1 internally but that Field1 is not re-exported.

Any consumer doing from adcp.types import FieldModel to build get_products requests will silently get the wrong enum — no import error, just wrong behavior at runtime. Needs a disambiguated export (e.g., GetProductsField / GetBrandIdentityField) or at minimum preserve the original binding.

2. Snapshot gains required fields — breaks constructors

Snapshot (in GetMediaBuysResponse) now requires staleness_seconds, impressions, and spend — previously only as_of was required. Any code constructing Snapshot objects (e.g., mock adapters, test fixtures) will get ValidationError without providing these. This is a forward-incompatible change that should either (a) make these optional with defaults, or (b) be called out prominently in migration notes.

3. Product.delivery_measurement changed from required to optional

The field went from required (DeliveryMeasurement) to optional (DeliveryMeasurement | None = None). The spec description changed from "REQUIRED for all products" to "When absent, buyers should apply their own measurement defaults." This is a significant spec change — downstream code doing product.delivery_measurement.provider without a None check will now AttributeError. Should be highlighted in release notes.


Should Fix

4. ~1500 lines of duplicated not-supported boilerplate across handlers

GovernanceHandler, ContentStandardsHandler, and SponsoredIntelligenceHandler each override 25+ methods with identical not_supported() calls that only differ in the agent type string. The base class ADCPHandler already returns not_supported() for everything.

Fix: Override not_supported() at the class level to inject the agent type name:

class GovernanceHandler(ADCPHandler):
    _agent_type = "Governance"
    # Base class already returns not_supported() for unimplemented methods

Then in ADCPHandler.not_supported(), use self._agent_type in the message. Eliminates ~1500 lines and means new spec operations don't require updating every handler subclass.

5. MCP tool schemas are hand-crafted and can drift from generated types

ADCP_TOOL_DEFINITIONS contains 35+ hand-written JSON schemas that are not generated from or validated against the Pydantic models. test_spec_coverage.py only checks tool names exist, not schema accuracy. If a field is added to CreateMediaBuyRequest, the MCP tool definition won't reflect it.

Fix: Either generate inputSchema from model_json_schema(), or add a test that compares each tool's inputSchema fields against the corresponding Pydantic model.

6. 35 tools on a single MCP server — context window cost

All 35 tools are registered regardless of handler type. A GovernanceHandler doesn't need create_media_buy. Filter ADCP_TOOL_DEFINITIONS based on handler type to reduce context window consumption for agent consumers.

7. Five types removed without deprecation aliases

Disclaimer, Performance, ProductCatalog, MediaSubAsset, TextSubAsset are removed from __init__.py with no backward-compat aliases. Salesagent doesn't import these, but other consumers might get surprise ImportErrors. Consider adding deprecation aliases for one release cycle.

8. Inconsistent validation: TypeAdapter vs model_validate

get_plan_audit_logs and si_send_message use TypeAdapter.validate_python() while all other handler methods use Model.model_validate(). If there's a reason (RootModel unions?), it should be documented. Otherwise, use model_validate() consistently.

9. ADCPClient method boilerplate

Every client method follows the identical pattern: create operation_id → emit activity → call adapter → emit activity → parse response. This could be extracted to _execute_operation() and each method becomes a one-liner. Same applies to RegistryClient error handling (copy-pasted 10+ times).


Consider

10. Tool descriptions are too terse for agent consumption

Descriptions like "List accounts" and "Sync accounts" don't tell an LLM when to use one vs the other. Per agentic API design best practices, descriptions should say what the tool does AND when/why to use it.

11. No timestamp validation in webhook signature verification

_verify_webhook_signature checks the HMAC but doesn't reject stale timestamps, making it vulnerable to replay attacks. Standard practice: reject if timestamp > 5 minutes old.

12. ADCPHandler inherits from ABC but has no abstract methods

Every method has a default implementation (not_supported()), and tests instantiate ADCPHandler() directly. The ABC inheritance is misleading — either remove it or add abstract methods.


Impact on Salesagent (our upgrade plan)

When we upgrade to this version, we'll need to:

  1. Add None-checks for Product.delivery_measurement (now optional)
  2. Update Snapshot construction in mock adapter to include new required fields
  3. Re-run test_adcp_contract.py — field reordering may affect schema assertions
  4. Evaluate new fields: plan_id on CreateMediaBuyRequest, planned_delivery on response, shows/incomplete on GetProductsResponse

The structural changes (brand submodule split, SyncCreatives move to creative/) don't affect us since we import from adcp.types, not internal module paths.


Overall: good progress toward RC2. The must-fix items (#1-3) are the main blockers from our perspective. The handler boilerplate (#4) and MCP schema drift (#5) are significant tech debt that will compound as the spec grows. Happy to discuss any of these.

…plate, deprecation aliases)

- Fix FieldModel export collision: add GetProductsField/GetBrandIdentityField
  semantic aliases, restore backward-compat FieldModel → get_products version
- Remove ~1100 lines of duplicated not-supported boilerplate from handlers by
  using _agent_type class attribute and base class _not_supported() helper
- Add deprecation aliases for removed types (Disclaimer, Performance,
  ProductCatalog, MediaSubAsset, TextSubAsset)
- Add MCP schema drift detection test comparing inputSchema vs Pydantic models
- Document TypeAdapter usage for Union type aliases, fix TypeAdapter[Any] annotation
- Fix TypeAdapter[Any] → TypeAdapter[SiSendMessageRequest] in SI handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bokelley
Copy link
Contributor Author

Response to Review

Thanks for the thorough review. Pushed fixes in b9eac61 — net reduction of 741 lines.


Must Fix

1. FieldModel silently rebound — Fixed. Added semantic aliases GetProductsField and GetBrandIdentityField in aliases.py. Restored backward-compat FieldModel = GetProductsField in __init__.py so existing from adcp.types import FieldModel consumers get the original get_products enum.

2. Snapshot gains required fields — Investigated: staleness_seconds, impressions, and spend are required on origin/main too (same 4 required fields in both the schema and generated code). This is not a new change from this PR. Happy to double-check if you're seeing something different.

3. Product.delivery_measurement → optional — This is an RC2 spec change (removed from required array in core/product.json, description changed to "When absent, buyers should apply their own measurement defaults"). Our generated code correctly reflects the schema. Will add to release notes.


Should Fix

4. ~1500 lines of duplicated not-supported boilerplate — Fixed. Added _agent_type class attribute and _not_supported() helper to ADCPHandler. Removed all stub overrides from the three handler subclasses. Net -1100 lines.

5. MCP schema drift — Added test_mcp_tool_input_schema_matches_pydantic_models to test_spec_coverage.py. Compares each tool's inputSchema properties against the Pydantic model's json_schema(). Maintains a KNOWN_GAPS dict for intentional omissions — any new model field not in MCP or KNOWN_GAPS fails the test.

6. 35 tools on a single MCP server — Agree this is worth doing. Deferring to a follow-up since it requires changes to the MCP adapter's tool registration flow, and this PR is already large.

7. Five types removed without deprecation aliases — Fixed. Added backward-compat aliases:

  • Disclaimer, ProductCatalog → imported from generated_poc.brand
  • Performance → aliased to PerformanceFeedback
  • MediaSubAsset, TextSubAsset → stub classes that raise TypeError with removal message (schema removed in RC2)

8. TypeAdapter vs model_validate — Documented with inline comments. Both GetPlanAuditLogsRequest and SiSendMessageRequest are Union type aliases (not classes), so they have no .model_validate(). TypeAdapter is the correct Pydantic pattern for Union validation. Also fixed TypeAdapter[Any]TypeAdapter[SiSendMessageRequest] for type safety.

9. ADCPClient method boilerplate — Deferring to follow-up. Agree the pattern could be extracted, but it's a refactor of the entire client surface that should be its own PR.


Consider

10. Tool descriptions — Agree, tracking for follow-up.

11. Webhook timestamp validation — Good catch, tracking for follow-up (security hardening).

12. ABC without abstract methods — Intentional design: ABC signals "subclass me" to consumers even though all methods have default implementations. The alternative (abstract methods) would force implementors to override all 35+ methods, which is worse ergonomics. Open to removing ABC if you feel strongly.

Copy link
Collaborator

@KonstantinMirin KonstantinMirin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work here — the spec coverage test is a particularly smart addition, makes it impossible to forget wiring a new task through all five layers. The handler boilerplate consolidation with _not_supported() is clean too.

Two small things I'd want to tighten before merge:

TypeAdapter comments are stalegovernance.py:39 and sponsored_intelligence.py:27 say GetPlanAuditLogsRequest and SiSendMessageRequest are Union type aliases, but after the oneOf flattening in c242a7d they're regular classes with model_validate(). The TypeAdapter works, but the comments will mislead the next person adding a handler. Quick swap to model_validate() and delete the comments.

Registry slug validation — the get_member() check blocks / and \ but an allowlist regex ([a-zA-Z0-9_-]+) would be tighter. Minor but it's the kind of thing that's easier to get right now than patch later.

Optional: the 5 governance campaign methods in client.py use single-line docstrings while the property list methods right below them use multi-line with Args/Returns — might want to match them up.

Rest looks solid. The full-stack wiring across client/handlers/adapters/CLI/MCP is consistent and complete.

- After oneOf flattening (c242a7d), GetPlanAuditLogsRequest and
  SiSendMessageRequest are regular Pydantic classes. Replace TypeAdapter
  with model_validate() for consistency across all handlers.
- Tighten registry slug validation from blocklist (/ and \) to allowlist
  regex [a-zA-Z0-9_-]+.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor Author

@bokelley bokelley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @KonstantinMirin — both fixed in 103d616:

  1. TypeAdapter → model_validate() — You're right, after the oneOf flattening both GetPlanAuditLogsRequest and SiSendMessageRequest are regular Pydantic classes now. Replaced TypeAdapter.validate_python() with model_validate() and removed the stale comments.

  2. Slug validation — Tightened from blocklist (/ and \) to allowlist regex [a-zA-Z0-9_-]+.

Skipping the docstring alignment for now — can do in a follow-up if you'd like.

bokelley and others added 3 commits March 16, 2026 18:53
- MCP tool filtering: specialized handlers (Governance, Content Standards,
  Sponsored Intelligence) now only register their relevant tools plus
  get_adcp_capabilities, reducing context window cost for agent consumers.
  ADCPHandler (sales agents) still gets all tools.
- Webhook replay protection: reject timestamps older than 5 minutes
  (configurable via webhook_timestamp_tolerance). Also rejects future
  timestamps for clock skew protection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… handler tools

Security fixes from review:
- Webhook auth bypass: when webhook_secret is configured, require both
  signature and timestamp headers. Previously, omitting headers bypassed
  verification entirely.
- MCP tool filtering: unknown handler types now get protocol-only tools
  (minimum privilege) instead of all tools. ADCPHandler is explicitly
  listed with all tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- get_tools_for_handler now accepts instance or class, walks MRO to find
  the matching handler base class. Fixes filtering for user subclasses
  (e.g. MyGovernanceAgent(GovernanceHandler) now correctly gets only
  governance tools).
- Add module-level assertion that all tool names in _HANDLER_TOOLS
  reference real tools in ADCP_TOOL_DEFINITIONS.
- Add test for subclass MRO-based filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/governance-sdk branch from adbf51c to 39985e5 Compare March 16, 2026 23:04
@bokelley bokelley merged commit 523ea20 into main Mar 16, 2026
8 checks passed
@bokelley bokelley deleted the bokelley/governance-sdk branch March 16, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants