diff --git a/ROADMAP.md b/ROADMAP.md index 92d3d83..09e7729 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,36 +17,70 @@ Decisions made post-RFC that supersede or refine the merged text. The table belo | D5 | Reference mock-server | The `@adcp/sdk/mock-server` package — same mock-server TS uses. See D8 for CI deployment shape. | Specifies RFC §`comply_test_controller` "shared reference mock-server" | | D6 | Maven Central publish cadence | **Hold first publish until v0.3 alpha.** v0.1 and v0.2 ship as local Gradle artifacts / SNAPSHOT only. Sonatype OSSRH namespace claim + GPG key setup still happens harness Week 1 (slow path; 1–5 business-day ticket) — we just don't push artifacts until v0.3. | RFC §Build, distribution, governance (RFC said "Maven Central alpha from v0.1") | | D7 | `javax`/`jakarta` floor | **`jakarta` only**, Spring Boot 3.x floor. Single `adcp-spring-boot-starter` artifact, no compat starter, no community 2.7 port. Spring Boot 2.7 OSS support ended Nov 2025; anyone still on it has a vendor relationship. | Resolves RFC Open Question 6 in favor of option (a) | -| D8 | Mock-server CI deployment | **Sidecar via `npx adcp mock-server`.** GitHub Actions Node step installs a pinned `@adcp/sdk` version, backgrounds one mock-server per specialism on a port range, Java tests hit `localhost`. The pinned `@adcp/sdk` version is the conformance oracle — bumping it is a deliberate PR. Promote to a published Docker image if multi-specialism orchestration becomes unwieldy. | Specifies D5's deployment | +| D8 | Mock-server CI deployment | **Sidecar via `npx adcp mock-server`.** GitHub Actions Node step installs a pinned `@adcp/sdk` version, backgrounds one mock-server per specialism on a port range, Java tests hit `localhost`. The pinned `@adcp/sdk` version is the CI conformance oracle for executable behavior, but never overrides D23's signed protocol bundle for schema/API truth. If they diverge, the release-blocking fix is to bump/patch the CI mock-server pin; a temporary exception requires WG maintainer approval in the same PR, an issue link, and an explicit statement that storyboard CI still passes or which compatibility assertion is intentionally skipped. Promote to a published Docker image if multi-specialism orchestration becomes unwieldy. | Specifies D5's deployment | | D9 | MCP Java SDK | **`io.modelcontextprotocol.sdk:mcp-core:1.1.2` + `mcp-json-jackson2:1.1.2`** at the core (not the `mcp` bundle artifact, which pulls jackson3). Used by `adcp` (caller) and `adcp-server` (agent). The Spring AI MCP SDK was donated to the `modelcontextprotocol` org in Feb 2025 and rebranded as the official Java SDK; current `spring-ai-mcp-*` artifacts are now thin Spring Boot wrappers on top of it — no parallel implementation. **License: MIT** (compatible, flagged for foundation position). Both prototype questions closed in [`specs/mcp-prototype-findings.md`](specs/mcp-prototype-findings.md): (a) `HttpServletStreamableServerTransportProvider` in `mcp-core` is framework-neutral — no Jetty/Tomcat dep at compile time, adopter brings their own servlet container at runtime; (b) `mcp-json-jackson2` and `mcp-json-jackson3` are at identical 1.1.2 cadence with the same surface — we pin to jackson2 to match the rest of the SDK's Jackson tree. | Resolves RFC Open Question 2 | | D10 | A2A pre-1.0 type strategy | **Keep A2A types in-tree until `a2aproject/a2a-java` cuts a stable ≥ 1.0.0 release**, then migrate to the upstream client in one shot and deprecate the in-tree fallback in the next minor. As of the latest check, `a2a-java` is at `1.0.0.Beta1` (Apr 2026) — package layout still churning, so we don't hard-depend on it yet. | RFC default for Open Question 3 | | D11 | `TransitionGuard` narrowing protection | **Guards declare which spec edges they touch.** Conformance harness fails if a sandbox account's guards narrow any edge the storyboards exercise. Guards run after the spec edge check and can never relax a spec edge. | Resolves RFC Open Question 7 | | D12 | Spring Security integration depth | **Recipes-only at v1.0.** No separate `adcp-spring-boot-starter-security` artifact. Auth models vary too much to pre-bake; recipes age better than autoconfig. Revisit if v0.3 design-partner feedback demands it. | RFC default for Open Question 5 | | D13 | Reactor + Mutiny adapters | **At GA, not fast-follow.** `adcp-reactor` and `adcp-mutiny` both ship in v1.0. WebFlux shops left to wrap the sync API would own that complexity forever and we'd lose the canonical surface. | Confirms RFC §Async model | | D14 | Kotlin co-release | **At v1.0, thin extension artifact** (`adcp-kotlin`). Coroutine `suspend fun` wrappers + DSL builders. Not a parallel SDK. Defer and Kotlin shops fork. | Confirms RFC §Kotlin positioning | -| D15 | Spec-rev tracking cadence | **≤ 2 weeks from AdCP spec rev to a Java SDK release that consumes it.** Same SLO TS and Python hold to. Slower than 2 weeks and JVM teams stop trusting the parity claim. | Specifies RFC §What kills adoption (item 1) | +| D15 | Spec-rev tracking cadence | **≤ 2 weeks from AdCP spec rev to a Java SDK release that consumes it.** Same SLO TS and Python hold to. Slower than 2 weeks and JVM teams stop trusting the parity claim. The release must track the signed bundle pin for build/cache inputs and emit the release-precision `adcp_version` wire token after normalization. Before D6's first Maven Central publish, this SLO is satisfied by a tagged local/SNAPSHOT alpha; if 3.1 GA cuts between v0.2 and v0.3, cut an off-cycle v0.2.x local/SNAPSHOT rather than waiting for v0.3, unless the WG explicitly records a D15 exception. | Specifies RFC §What kills adoption (item 1); refined by D23 | | D16 | Design-decision filing convention | **Match AdCP convention.** Longer-form design / decision docs live in `specs/.md` (the same place the Java SDK RFC itself lives in the spec repo). The Confirmed-decisions table in this ROADMAP is the at-a-glance index; a decision spins up its own `specs/` doc only when the table row isn't enough. No `docs/adr/`, no MADR template, no per-decision file by default. | (No RFC §; sets a repo convention) | | D17 | Branching model | **Trunk-based.** Short-lived feature branches → PR to `main`. Semver tags from `main`. No long-lived release branches. | (Sets a repo convention; matches TS SDK) | | D18 | Commits + changelog | **Conventional Commits enforced via commitlint; [Changesets](https://github.com/changesets/changesets) for the CHANGELOG.** Matches the TS SDK's workflow shape (same `.changeset/` directory pattern) so humans and tools read both SDKs' release notes the same way. | (Sets a repo convention; matches TS SDK) | | D19 | Contributor IPR | **Replicate the AAO IPR Bot pattern** used by adcp / adcp-client / adcp-client-python / adcp-go. Contributors agree by commenting `I have read the IPR Policy` on their first PR; the AAO IPR Bot (a GitHub App) enforces via a required status check. Harness Week 1 actions: (1) foundation admin adds `adcontextprotocol/adcp-sdk-java` to the App's installation scope and the `IPR_APP_ID` / `IPR_APP_PRIVATE_KEY` org-secret scope; (2) add the ~15-line caller workflow at `.github/workflows/ipr.yml` invoking `adcontextprotocol/adcp/.github/workflows/ipr-check-callable.yml@main`; (3) `CONTRIBUTING.md` mirrors the adcp wording and links to `IPR_POLICY.md` in the spec repo. **No DCO. No CLA.** | (Matches AAO standard) | | D20 | Sonatype OSSRH namespace claim | **Claim `org.adcontextprotocol` via DNS TXT verification.** Maven Central confirms the namespace is unclaimed (zero artifacts today). Requires the foundation to add one `TXT` record to `adcontextprotocol.org` proving control. Sonatype ticket + DNS record both kicked off harness Week 1; first publish waits for v0.3 (per D6). | Specifies D6 | | D21 | Branch protection on `main` | **Required: 1 code-owner approving review, required CI checks (`build`, `test`, `storyboard`, `IPR Policy / Signature`), no force-push, no direct push, no admin bypass.** Dependabot patch PRs auto-merge after green CI. Two-reviewer gate doesn't add safety with a founder pair; revisit when contributor base grows. | (Sets a repo convention) | +| D22 | Multi-tenant signing keys | **Signing key selection is tenant-aware, not just `adcp_use`-aware.** The L1 signing SPI must select by purpose plus caller/resolved publisher tenant context, a JOSE-compatible pattern where one operator can publish multiple tenant keys on a single JWKS endpoint: same `adcp_use`, distinct `kid` per tenant. v0.2 freezes the explicit parameter set for `SigningContext` — `AdcpUse use()`, nullable tenant identity, nullable principal reference — and the shipped API is `SigningContext`-based with no single-arg `SigningProvider.forUse(AdcpUse)` short form. v0.3 wires tenant resolution from `AccountStore` / `adagents.json` into that selector. Longer-form shape lives in [`specs/signing-context.md`](specs/signing-context.md). | Refines RFC §L1 signing and §L2 multi-tenant principal resolution | +| D23 | AdCP 3.1 beta parity target | **Track the signed protocol bundle as the source of truth; use TS/Python as implementation references.** For this roadmap update the pinned target is `3.1.0-beta.5` (2026-05-26). If 3.1 GA cuts before the v0.1 codegen PR, bump to GA deliberately in that PR; otherwise beta.5 is the target. `@adcp/sdk@8.1.0-beta.13` is aligned to beta.5; Python `6.3.0b4` is on beta.4 and is a reference, not a blocker. Java's local `ADCP_VERSION` remains `3.0.11`; v0.1 must include wire-version negotiation, multi-bundle schema loading, and 3.1 beta/GA codegen coverage before claiming cross-SDK parity. | Updates RFC parity baseline and D15 spec-rev SLO | -## Parity baseline (as of 2026-05-13) +## Parity baseline (as of 2026-05-26) -The RFC tracks `@adcp/sdk` 6.x. Current state of the world: +The RFC tracks `@adcp/sdk` 6.x and the first Java scaffold still pins `ADCP_VERSION=3.0.11`. Current upstream parity moved again. Precedence for Java work is: signed protocol bundle first, protocol release notes/spec docs second, pinned mock-server behavior for CI conformance third, TS/Python implementation behavior fourth. A mock-server mismatch blocks release by forcing a mock-server pin/patch decision; it does not redefine the protocol. -| SDK | Version | Notes | +| Source | Version checked | Java parity read | |---|---|---| -| `@adcp/sdk` (TS) | 7.2.0 | RFC was authored against 6.x; 7.x added `upstream-recorder` and other surface | -| `adcp` (Python) | 4.x (beta) | Subpackages: `compat`, `decisioning`, `migrate`, `protocols`, `schemas`, `server`, `signing`, `testing`, `types`, `utils`, `validation` | -| `adcp-go` | v1.x (dev) | Reference for a third-language take | +| AdCP protocol bundle | `3.1.0-beta.5` | Java is behind on schema pin and 3.1 compliance bundle coverage. The next codegen bump should target beta.5 unless 3.1 GA has cut by then. | +| `@adcp/sdk` (TS) | `8.1.0-beta.13`, `adcp_version=3.1.0-beta.5` | Best reference for 3.1 caller/server ergonomics, conformance runner behavior, canonical-format helpers, cache-scope guards, and beta migration shims. Aligned to the target bundle. | +| `adcp` (Python) | `6.3.0b4`, packaged `ADCP_VERSION=3.1.0-beta.4` | Best reference for Pydantic/server semantics, request-scoped capabilities, externally-managed webhook signing capabilities, resolver hardening, and version-routed validation. One beta behind the target bundle, so use for API shape, not as the normative schema source. | +| `adcp-go` | v1.x (dev) | Still useful as a third-language sanity check, especially for typed version pins and signer/verifier shapes. | -**Action: T0** — re-verify the RFC's parity table against `@adcp/sdk` 7.2.0 exports before v0.1 cut. The 5-artifact target still holds; the 7.x additions collapse into existing artifacts. Specifics below. +**Net: Java is not fully up to date.** The roadmap now treats 3.1 beta parity as a v0.1 input, not a post-v0.1 cleanup. The remaining gap is not just generated types: Java needs version negotiation, multi-bundle validators, cache-scope semantics, 3.1 compliance bundle execution, and a handful of SDK helper surfaces that TS/Python have already exposed. -### 7.x deltas since the RFC was written +### 7.x / 8.x deltas since the RFC was written -Read from the TS SDK CHANGELOG. Each delta lists the Java track it folds into. None invalidate the RFC; all bump scope inside an existing track. +Read from the TS and Python SDK changelogs plus the AdCP 3.1 beta release notes. Each delta lists the Java track it folds into. None invalidate the RFC; all bump scope inside existing tracks. + +**AdCP 3.1 protocol surfaces** + +- **Wire-version negotiation** (`adcp_version`, `supported_versions`, response echo, `VERSION_UNSUPPORTED`). Keep two tiers distinct: bundle pin (`3.1.0-beta.5`, full semver, build/cache key) and release-precision wire token normalized per spec (`3.1` for stable releases; beta wire tokens round-trip what the seller emits after normalizing full prerelease pins, e.g. `3.1.0-beta.5` → `3.1-beta.5`). Current Java code only models the stable `major.minor` form, so v0.1 must expand `AdcpVersion` to expose both `.bundle()` and `.wire()` before 3.1 beta traffic is supported. Cold-start callers send the SDK default wire token for the target bundle, then honor `VERSION_UNSUPPORTED.supported_versions` if the seller asks for downgrade/upgrade. Java must emit both `adcp_version` and legacy `adcp_major_version` through 3.x, reject major-level disagreement between the two, pick response validators by served version, route unversioned legacy traffic to `3.0`, and collapse prerelease handling back to stable `3.1` when 3.1 GA cuts. → [`transport`](#track-3--l0-transport-mcp--a2a) + [`codegen`](#track-2--l0-types--codegen) + [`testing`](#track-9--testing--conformance). +- **Multi-bundle schema loading.** TS ships 3.0 and 3.1-beta caches and can run external 3.0 compliance bundles from a 3.1-beta runner. Python ships 2.5, 3.0, and 3.1 beta caches. Java needs the same loader shape, not a single `ADCP_VERSION` global. → [`codegen`](#track-2--l0-types--codegen) + [`testing`](#track-9--testing--conformance). +- **Protocol envelope status is required.** Every task response, including sync reads like `get_adcp_capabilities`, carries top-level envelope `status`. Server helpers should stamp it centrally; domain payload aliases should not force adopter handlers to return envelope fields by hand. → [`transport`](#track-3--l0-transport-mcp--a2a) + [`codegen`](#track-2--l0-types--codegen). +- **Media-buy lifecycle status split.** `create_media_buy` and `update_media_buy` success payloads use `media_buy_status`; legacy top-level body `status` is deprecated because `status` now belongs to the task envelope. Java must read canonical first, tolerate legacy during the 3.1 window, and avoid confusing A2A task artifact status with domain status. → [`codegen`](#track-2--l0-types--codegen) + [`transport`](#track-3--l0-transport-mcp--a2a) + [`testing`](#track-9--testing--conformance). +- **Governance status renames.** Experimental governance responses moved root body status to `verdict` / `outcome_state`, and audit entries use `entries[].verdict`. → [`codegen`](#track-2--l0-types--codegen). +- **Open error-code decoding.** As SDK forward-compatibility practice, receivers should not fail closed on unknown `error.code`; classify from `recovery` and default conservatively when absent. Java generated enums need an unknown-value strategy, not a hard enum parse failure. → [`codegen`](#track-2--l0-types--codegen) + [`transport`](#track-3--l0-transport-mcp--a2a). +- **Advisory `errors[]` on success payloads.** `STALE_RESPONSE`, canonical-format projection warnings, pixel-tracker downgrade/upgrade warnings, and similar advisories ride in payload `errors[]` while transport/task success remains success. Java callers and validators must not promote advisory payload errors to thrown failures. → [`transport`](#track-3--l0-transport-mcp--a2a) + [`testing`](#track-9--testing--conformance). +- **Universal request `idempotency_key`.** 3.1 read tools accept every-request envelope fields and the compliance suite probes this. Java request builders and MCP wrapper validation must tolerate and emit envelope fields on every task request, not only write calls. → [`codegen`](#track-2--l0-types--codegen) + [`transport`](#track-3--l0-transport-mcp--a2a) + [`async-l3`](#track-6--l3-idempotency-async-tasks-webhooks). +- **Webhook token round-trip.** `McpWebhookPayload.token` is typed and must echo through webhook dispatch/receipt paths. → [`signing`](#track-4--l1-signing) + [`async-l3`](#track-6--l3-idempotency-async-tasks-webhooks). +- **Endpoint proof-of-control.** Durable account-level webhook configs require proof-of-control semantics and stable `subscriber_id` replace/upsert behavior. → [`async-l3`](#track-6--l3-idempotency-async-tasks-webhooks) + [`multitenant`](#track-5--l2-account-store-registry-multi-tenant). + +**3.1 buying, catalog, and signal surface** + +- **Wholesale feed mirroring.** `get_products` / `get_signals` support `wholesale_feed_version`, `if_wholesale_feed_version`, optional pricing version, `unchanged`, and required `cache_scope` (`public` / `account`). Account-level wholesale feed webhooks deliver `product.*`, `signal.*`, and `wholesale_feed.bulk_change` with repair through read tools. → [`multitenant`](#track-5--l2-account-store-registry-multi-tenant) + [`async-l3`](#track-6--l3-idempotency-async-tasks-webhooks) + [`testing`](#track-9--testing--conformance). +- **Cache-scope guardrails.** TS now fails closed when server payloads omit `cache_scope` on product responses. Java server builders should make this hard to omit on account-scoped product/signal responses, because cache-scope is the safety property that prevents leaking account overlays through shared caches. → [`transport`](#track-3--l0-transport-mcp--a2a) + [`multitenant`](#track-5--l2-account-store-registry-multi-tenant). +- **Wholesale signals and `SignalRef`.** `get_signals discovery_mode=wholesale`, canonical `signal_ref` identities, product-local / data-provider / signal-source scopes, selectable `signal_targeting_options`, `signal_targeting_rules`, and package-level `targeting_overlay.signal_targeting_groups`. Legacy `signal_id` remains compatibility input. → [`codegen`](#track-2--l0-types--codegen) + [`multitenant`](#track-5--l2-account-store-registry-multi-tenant). +- **Canonical creative formats.** `format_options[]`, `format_option_refs`, `format_option_id`, `v1_format_ref`, named canonical format helpers, v1↔v2 projection, pixel-tracker advisory downgrades, and cache-backed canonical registries. The beta.2 `capability_ids` write path was removed before beta.5; Java should model the beta.5 `format_option_*` names from the start. → [`codegen`](#track-2--l0-types--codegen) + [`transport`](#track-3--l0-transport-mcp--a2a). +- **Public placement catalogs.** `adagents.json` can publish placement catalogs and publisher-scoped `placement_refs`; seller-private routing stays out of public placement schemas. → [`multitenant`](#track-5--l2-account-store-registry-multi-tenant). +- **Vendor-attested measurement.** `vendor_metric` optimization goals, per-product `vendor_metric_optimization`, reporting-coherence preconditions, and compliance coverage. → [`codegen`](#track-2--l0-types--codegen) + [`testing`](#track-9--testing--conformance). +- **Delivery and billing finality.** `reach_window`, `viewability.viewed_seconds`, windowed pull recovery, row-level delivery finality, `report_usage` finality, and `BILLING_OUT_OF_BAND` as an error-code surface. → [`codegen`](#track-2--l0-types--codegen) + [`testing`](#track-9--testing--conformance). +- **Action discovery.** `allowed_actions[]`, `available_actions[]`, finer media-buy action enum values, `ACTION_NOT_ALLOWED`, and helper-level request decomposition in TS/Python. Java should expose typed helpers around `update_media_buy` mutations instead of forcing every adopter to re-parse action intent. → [`lifecycle`](#track-7--l3-lifecycle--transitions) + [`transport`](#track-3--l0-transport-mcp--a2a). + +**Cross-SDK helper surface to match** + +TS/Python helper names are references, not Java naming requirements. Java idioms win: `*Request` builders, records/sealed types, instance or namespaced helpers where clearer than free functions, and `@Nullable` rather than `Optional` on public model fields. + +- **TS `@adcp/sdk@8.1` helper additions.** Root exports now include canonical creative format helpers, format projection/write-side helpers, `ensureGetProductsCacheScope()` / `validateGetProductsCacheScope()`, `parseWholesaleFeedWebhookNotification()` / `normalizeWholesaleFeedWebhookNotification()`, signal discovery helpers, `decomposeUpdateMediaBuy()` / `assertUpdateMediaBuyAllowed()`, per-tool type slices, SSRF-safe networking helpers, typed server `*Payload` aliases, and 3.1 compliance/cache selection in the runner. Java equivalents belong in the main `adcp`, `adcp-server`, and `adcp-testing` artifacts rather than new artifacts. +- **Python `adcp@6.x` helper additions.** Python now has version-routed validation, 2.5/3.0/3.1 beta schema caches, canonical-format projection, webhook proof-of-control helpers, wholesale feed sender, request-scoped capabilities hooks, unknown-field policy and hook composition, media-buy version handling/update actions, externally managed webhook signing capabilities, and permissive property resolution. Java server APIs should mirror the capability/hook seams even if implementation names differ. **Authentication / discovery** @@ -92,14 +126,14 @@ Read from the TS SDK CHANGELOG. Each delta lists the Java track it folds into. N - **`tasks/cancel` fire-and-forget on buyer abort** (6.16.0). Aborted poll must POST a real-UUID `tasks/cancel` with `AbortSignal.timeout(5000)`, silently catch rejection (an aborted buyer must not have observability dependencies on cancel success). `signed-requests` sellers will 401 unsigned cancels — so this path threads through outbound signing. → [`transport`](#track-3--l0-transport-mcp--a2a) + [`signing`](#track-4--l1-signing). -### 7.x impact on the milestone calendar +### 3.1 beta impact on the milestone calendar -The 7.x deltas don't move milestones, but they do tighten the v0.1 gate: +The 7.x / 8.x / 3.1 beta deltas don't move the M+12 GA line, but they do tighten the early release gates: -- v0.1 release gate **adds**: SSRF baseline (DNS pin, address-guards, redirect:manual, body cap) on all discovery probes. `WWW-Authenticate`-aware error envelope. HTTP Basic config support. The `storyboards_missing_tools` / `storyboards_not_applicable` split in the storyboard runner output. -- v0.2 gate **adds**: nothing new from 7.x — the L1 signing surface didn't grow. -- v0.3 gate **adds**: `IDEMPOTENCY_IN_FLIGHT` wire code with claim-age-derived `retry_after` cap. `resolveAgentProperties` / `validateAdAgents` / `MANAGERDOMAIN` fallback. -- v0.4 gate **adds**: `upstream-recorder` SPI, `query_upstream_traffic` controller scenario, `parallel_dispatch` storyboard step, full `RunnerNotice` taxonomy. +- v0.1 release gate **adds**: D23 target bundle ingestion (`3.1.0-beta.5`, or 3.1 GA if cut before the codegen PR), multi-bundle validator selection, wire `adcp_version` support for stable and prerelease tokens, envelope `status` stamping, SSRF baseline (DNS pin, address-guards, redirect:manual, body cap) on all discovery probes, `WWW-Authenticate`-aware error envelope, HTTP Basic config support, and the `storyboards_missing_tools` / `storyboards_not_applicable` / `steps_not_selected` split in runner output. This accepts a realistic v0.1 slip from M+2 to M+3 rather than deferring the security/auth baseline. +- v0.2 gate **adds**: typed webhook token echo, endpoint proof-of-control foundations, and enough signing metadata to support externally managed webhook-signing capabilities without assuming the SDK owns every key. +- v0.3 gate **adds**: `IDEMPOTENCY_IN_FLIGHT` wire code with claim-age-derived `retry_after` cap, universal request `idempotency_key` handling, `resolveAgentProperties` / `validateAdAgents` / `MANAGERDOMAIN` fallback, wholesale feed versioning/cache-scope semantics, canonical format projection helpers, and `SignalRef` / product-scoped signal targeting. +- v0.4 gate **adds**: durable account-level webhook configs with proof-of-control, wholesale feed webhook parsing/emission, `upstream-recorder` SPI, `query_upstream_traffic` controller scenario, `parallel_dispatch` storyboard step, full `RunnerNotice` taxonomy, advisory `errors[]` handling, and the 3.1 compliance storyboard additions. ## Harness first — what lands before contributors are pulled in @@ -118,7 +152,7 @@ Hard line: **scaffold the build, leave the rooms empty.** Don't pre-build L1 / L | Codegen MVP: emit records + builder records for **one or two** request/response pairs (e.g. `GetProductsRequest` / `GetProductsResponse`) | Proves the generator architecture, locks in the `*Request`/`*Response` naming invariant, gives contributors real Java to import. Full coverage stays in [`codegen`](#track-2--l0-types--codegen). | [`codegen`](#track-2--l0-types--codegen) | | Prototype the two open MCP-SDK questions on `io.modelcontextprotocol.sdk:mcp:1.1.2` (per D9): can `mcp-core`'s servlet streamable-HTTP server transport run without Jetty/Tomcat? Is `mcp-json-jackson2` feature-equivalent to the Jackson 3 variant? | D9 picked the SDK; these two are the only unresolved bits before [`transport`](#track-3--l0-transport-mcp--a2a) opens for claim. | [`transport`](#track-3--l0-transport-mcp--a2a) | | Sonatype OSSRH namespace claim for `org.adcontextprotocol` + foundation GPG key + key-server publication | Slow-path ticket (1–5 business days); start Week 1 even though first publish waits for v0.3 (per D6). Don't block v0.3 on a stalled OSSRH ticket. | [`infra`](#track-1--build-repo-release-infra) | -| SSRF-safe `HttpClient` wrapper skeleton (DNS pin, address-guards, redirect:manual, body cap) | Baseline 7.x security posture. JDK `HttpClient` doesn't pin natively; this needs a design doc + skeleton before contributors touch outbound HTTP. | [`transport`](#track-3--l0-transport-mcp--a2a) | +| SSRF-safe `HttpClient` wrapper skeleton (DNS pin, address-guards, redirect:manual, body cap) | Baseline TS/Python security posture. JDK `HttpClient` doesn't pin natively; this needs a design doc + skeleton before contributors touch outbound HTTP. | [`transport`](#track-3--l0-transport-mcp--a2a) | | Storyboard CI gate shell: GitHub Actions on JDK 21, runs the runner against the `@adcp/sdk/mock-server`, even if the runner currently asserts only "we reached the server" | The v0.1 release gate is "storyboards green in CI." Standing it up empty and having it pass keeps contributors honest as L0 fills in — every PR is measured against the gate. | [`infra`](#track-1--build-repo-release-infra) + [`testing`](#track-9--testing--conformance) | | Repo conventions: `CONTRIBUTING.md` (track-claim flow + IPR pointer per D19), `.github/ISSUE_TEMPLATE/track-claim.md`, PR template, `CLAUDE.md` for agent contributors | The track-claim issue template is the actual contributor onboarding doc | [`docs`](#track-14--docs-migration-troubleshooting) | | AAO IPR caller workflow at `.github/workflows/ipr.yml` (per D19) + foundation admin installs the IPR Bot on this repo + adds it to the `IPR_APP_ID` / `IPR_APP_PRIVATE_KEY` org-secret scope | Required-status check `IPR Policy / Signature` is the gate on every PR (per D21) — must be working before contributor PRs arrive | [`docs`](#track-14--docs-migration-troubleshooting) + [`infra`](#track-1--build-repo-release-infra) | @@ -138,8 +172,8 @@ These look tempting to "get started on" but pre-building them locks in design th ### Ordering -- **Week 1 (founder pair):** Gradle skeleton, schema fetcher, codegen MVP, repo conventions, ADR directory, CI shell. -- **Week 2 (founder pair + advisors):** MCP SDK pick + ADR. SSRF wrapper skeleton + design doc. First storyboard CI run green-against-empty. Open the first 3–4 track-claim issues publicly. +- **Week 1 (founder pair):** Gradle skeleton, schema fetcher, codegen MVP, repo conventions, confirmed-decision index, CI shell. +- **Week 2 (founder pair + advisors):** MCP SDK pick recorded in the decision index. SSRF wrapper skeleton + `specs/.md` design doc. First storyboard CI run green-against-empty. Open the first 3–4 track-claim issues publicly. - **Week 3+:** Contributors arrive against a repo where `./gradlew check` passes and CI tells them whether their PR broke conformance. Tracks land in dependency order. This means the founder pair owns the [`infra`](#track-1--build-repo-release-infra) track end-to-end and the first slice of [`codegen`](#track-2--l0-types--codegen) and [`transport`](#track-3--l0-transport-mcp--a2a). Everything else opens for claim once the harness is green. @@ -165,10 +199,10 @@ Each track entry has: | Milestone | Target | Release gate | |---|---|---| -| v0.1 alpha | M+2 | L0 surface compiles, storyboards green against reference mock-server in CI. Local Gradle artifacts only (per D6 — first Maven Central publish at v0.3). | -| v0.2 alpha | M+4 | L1: RFC 9421 signing/verification, AWS+GCP KMS providers (lazy-init, per-`adcp_use`), webhook signing | -| v0.3 alpha | M+6 | L2 + partial L3: account store, idempotency, async tasks, Spring Boot starter alpha. **First Maven Central publish** (per D6). | -| v0.4 beta | M+9 | Full L3: transition validators, webhook emission, `comply_test_controller`, A2A transport | +| v0.1 alpha | M+3 | D23 target bundle compiles (`3.1.0-beta.5`, or 3.1 GA if cut before the codegen PR).
Wire-version negotiation works for stable and prerelease tokens, including `adcp_version` / `adcp_major_version` major mismatch rejection and `VERSION_UNSUPPORTED` data with `supported_versions` plus deprecated `supported_majors` through 3.x.
Multi-bundle validators can serve 3.0 and 3.1 traffic.
SSRF/auth discovery baseline lands.
Storyboards green against reference mock-server in CI.
Local Gradle artifacts only (per D6 — first Maven Central publish at v0.3). | +| v0.2 alpha | M+4 | L1: RFC 9421 signing/verification, AWS+GCP KMS providers (lazy-init, tenant-aware per-`adcp_use` key selector shape per D22; tenant resolver wiring lands in v0.3), webhook signing, typed webhook token / proof-of-control foundations | +| v0.3 alpha | M+6 | L2 + partial L3: account store, idempotency, async tasks, wholesale feed cache-scope semantics, canonical-format / signal-targeting helpers, Spring Boot starter alpha. **First Maven Central publish** (per D6). | +| v0.4 beta | M+9 | Full L3: transition validators, webhook emission, wholesale feed webhooks, `comply_test_controller`, A2A transport, 3.1 compliance bundle parity | | v1.0 GA | M+12 | L0–L3 parity, Reactor + Mutiny adapters, Kotlin co-release, Maven Central GA | The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is worse than committing M+12 and beating it. Slippage concentrates on: MCP Java SDK churn, RFC 9421 canonicalization edge cases, shared lifecycle YAML coordination, Spring Boot starter scope creep. @@ -200,30 +234,36 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is ### Track 2 — L0 types & codegen -**ID:** `codegen` | **Owner:** TBD | **Size:** 2.0 person-months +**ID:** `codegen` | **Owner:** TBD | **Size:** 2.5 person-months **Scope:** - Custom codegen on Eclipse JDT or JavaPoet, emitting Java records for value/response types and builder-records for request types. - Generator invariant: `*Request` types always have builders; `*Response` types are records and never do (RFC §Type generation). +- Generator invariant: open protocol vocabularies emit an unknown-safe shape (`sealed Known / Unknown(rawValue)` by default), not strict enums. See [`specs/codegen-open-enums.md`](specs/codegen-open-enums.md). - Polymorphic envelope handling (Jackson `@JsonTypeInfo` / `@JsonSubTypes`). - `x-adcp-*` annotation post-processors mirroring `scripts/generate-types.ts` in `adcp-client`. -- Version pinning support (`adcp-v2-5` co-existence namespace): generates frozen v2.5.1 types under `org.adcontextprotocol.adcp.generated.v2_5.*` alongside the primary v3.x types. Both namespaces coexist on the classpath. v2.5.1 schemas fetched from the GitHub source archive (`adcontextprotocol/adcp@v2.5.1/static/schemas/source`) since pre-3.0 bundles were never published to the CDN. +- Version pinning support (`adcp-v2-5` co-existence namespace): generates frozen v2.5.1 types under `org.adcontextprotocol.adcp.generated.v2_5.*` alongside primary v3.0 and v3.1 namespaces. All namespaces coexist on the classpath. v2.5.1 schemas fetched from the GitHub source archive (`adcontextprotocol/adcp@v2.5.1/static/schemas/source`) since pre-3.0 bundles were never published to the CDN. +- Multi-bundle schema registry: runtime validator lookup by served wire version (`3.0`, `3.1`, and 3.1 beta tokens), with the full bundle pin stored separately from the wire value. `ADCP_VERSION` cannot remain the only schema selector. +- Unknown-safe enums for open protocol vocabularies, especially `ErrorCode`, media-buy / creative status values, `recovery`, and action-discovery enums; strict Java enums are acceptable only where the spec vocabulary is intentionally closed. +- Generated response payload aliases for server handlers, so framework adapters stamp protocol-envelope fields centrally instead of making adopter code return top-level `status`, `adcp_version`, and related envelope fields by hand. +- 3.1 shape changes: required envelope `status`, `media_buy_status`, governance `verdict` / `outcome_state`, `SignalRef`, `format_option_refs`, product-scoped signal targeting groups, wholesale feed version/cache fields, delivery/billing finality, vendor-metric goals, action-discovery enums, and typed webhook token payloads. - JSpecify `@Nullable` annotations on every public type. No `Optional` returns. - Schema validator wrapper around `com.networknt:json-schema-validator`. - Schema-bundle accessor (runtime, resources jar; build-time loader lives in [`infra`](#track-1--build-repo-release-infra)). +- Agent-surface emission helpers for MCP and A2A. Generated MCP tool input descriptions consume spec text, A2A skill IDs follow a cross-SDK convention, and universal `idempotency_key` feeds `idempotentHint: true` where MCP supports it. See [`specs/agent-surface-emission.md`](specs/agent-surface-emission.md). **Out of scope:** Kotlin source generation (handled by [`kotlin`](#track-11--kotlin-extensions)). **Depends on:** `infra` (codegen Gradle task hookpoint). -**Milestone targets:** v0.1 needs full generated type coverage for the L0 surface and validator wired into transport. +**Milestone targets:** v0.1 needs full generated type coverage for the L0 surface, D23 target-bundle coverage (`3.1.0-beta.5`, or 3.1 GA if it cuts first), multi-bundle validator selection, and stable/prerelease wire version normalization wired into transport. --- ### Track 3 — L0 transport: MCP + A2A -**ID:** `transport` | **Owner:** @MichielDean (#17) | **Size:** 1.5 person-months +**ID:** `transport` | **Owner:** @MichielDean (#17) | **Size:** 2.0 person-months **Scope:** @@ -232,13 +272,17 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is - **A2A post-1.0:** swap transport to `a2aproject/a2a-java`; deprecate the in-tree fallback in the next minor. - HTTP transport on `java.net.http.HttpClient`. No third-party HTTP client in the core. - Jackson `ObjectMapper` with `StreamReadConstraints` / `StreamWriteConstraints` widened to AdCP-shaped defaults (RFC §JSON). +- Wire `adcp_version` on every request/response, accepting and echoing release-precision stable and prerelease values normalized per spec while storing the full bundle pin separately from the wire token. Keep the legacy `adcp_major_version` mirror through 3.x. Server dispatch validates requested version before handler execution, rejects major-level `adcp_version` / `adcp_major_version` disagreement, and returns typed `VERSION_UNSUPPORTED` with `supported_versions` plus deprecated `supported_majors` through 3.x. +- Central response-envelope stamping for sync success, submitted, failed, and advisory-success responses. The seam is a `ResponseEnvelope

` record materialized by the server adapter after adopter handler payloads return and before signing/serialization; outbound signing canonicalizes the stamped envelope. Payload `errors[]` advisories like `STALE_RESPONSE` must not be thrown as transport failures. +- Auth/discovery parity with TS/Python: Basic/Bearer config, `WWW-Authenticate` challenge parsing, private-agent-card auth forwarding, and the standard A2A Agent Card path. +- Server handler payload aliases and unknown-field policy hooks matching the Python/TS ergonomics: adopters return domain payloads; the framework owns envelope fields and strictness policy. - **No `*Async` mirror methods.** With JDK 21 as baseline, virtual threads make the sync API scale natively; the RFC's 12-method `*Async` mirror surface is dropped (see [Confirmed decisions](#confirmed-decisions)). Adopters who explicitly want `CompletableFuture` wrap individual calls themselves. **Out of scope:** OkHttp / Apache HttpClient 5 adapters (post-v1.0 on demand). **Depends on:** `codegen` for the request/response types. -**Milestone targets:** v0.1 needs MCP transport. v0.4 swaps in upstream `a2a-java` if its 1.0 has cut by then; otherwise the in-tree fallback ships at v1.0 with the swap-trigger documented. +**Milestone targets:** v0.1 needs MCP transport plus version negotiation, envelope stamping, SSRF baseline on discovery probes, `WWW-Authenticate` challenge parsing, Basic auth config, and private-agent-card auth forwarding. v0.4 swaps in upstream `a2a-java` if its 1.0 has cut by then; otherwise the in-tree fallback ships at v1.0 with the swap-trigger documented. --- @@ -249,7 +293,8 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is **Scope:** - Hand-rolled RFC 9421 canonicalizer (it's small and spec-tight; `org.tomitribe:http-signatures` is the wrong spec). Verifier test harness mirrors the TS one. -- `SigningProvider` SPI via `META-INF/services/`. API shape: `SigningProvider.forUse(AdcpUse.WEBHOOK)` returns a distinct provider from `.forUse(AdcpUse.REQUEST)` — receivers enforce purpose at JWK `adcp_use`. +- `SigningProvider` + `VerificationKeyResolver` SPIs via `META-INF/services/`. Signing takes explicit `SigningContext` rather than a single `AdcpUse`; verification starts from inbound `kid` and only then maps to tenant/principal context. Receivers enforce purpose at JWK `adcp_use`. +- Tenant-aware key selection at the signing boundary (D22). The API cannot model one global key per `adcp_use`: multi-tenant operators need one JWKS endpoint with distinct `kid` values per publisher tenant under the same `adcp_use`. The v0.2 signing surface freezes `SigningContext` in [`specs/signing-context.md`](specs/signing-context.md); v0.3 connects it to `AccountStore` tenant resolution. - In-process provider via JCA Ed25519 / ECDSA. **No Bouncy Castle in core** — JDK 21 has Ed25519 natively. - AWS KMS provider via `software.amazon.awssdk:kms`. Lazy-init. - GCP KMS provider via `com.google.cloud:google-cloud-kms`. Lazy-init. @@ -261,7 +306,7 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is **Depends on:** `transport` (signing wraps HTTP-level requests). -**Milestone targets:** v0.2 ships RFC 9421 + AWS+GCP KMS + webhook outbound signing. +**Milestone targets:** v0.2 ships RFC 9421 + AWS+GCP KMS + webhook outbound signing, including typed webhook token echo, endpoint proof-of-control foundations, and externally managed webhook-signing metadata. --- @@ -277,6 +322,11 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is - Optional `JpaAccountStore` if Spring Data JPA shops claim it. - `RegistryClient` SPI for agent-registry / brand-resolution lookup. Default impl points at the public AAO registry. - Multi-tenant principal resolution wired through the request handler. +- Per-tenant signing context export for D22: the resolved account/principal carries enough stable tenant identity for the L1 signer to choose the correct `kid` when multiple publisher tenants share one operator JWKS endpoint. +- `adagents.json` resolver parity with TS/Python: authorization-type dispatch over `property_ids`, `property_tags`, `inline_properties`, `publisher_properties`, `signal_ids`, and `signal_tags`; one-hop ads.txt `MANAGERDOMAIN` fallback; revoked-domain semantics; permissive property resolution mode for operators that need diagnostics instead of hard fail. +- Public placement catalog support from `adagents.json`, including publisher-scoped `placement_refs` and same-file `format_option_id` validation. +- Wholesale product/signal cache model: `cache_scope`, `wholesale_feed_version`, `if_wholesale_feed_version`, pricing-version tokens, and public-vs-account overlay invalidation. +- Product-scoped signal targeting support: canonical `SignalRef`, `included_signals`, `signal_targeting_options`, `signal_targeting_rules`, and package-level `targeting_overlay.signal_targeting_groups`. - Sandbox/live boundary enforcement at the `AccountStore` (so `comply_test_controller` calls return `COMPLY_NOT_AVAILABLE` on production accounts per spec). - Agent-card publication helper. @@ -284,13 +334,13 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is **Depends on:** `codegen` for principal / account types. -**Milestone targets:** v0.3 alpha ships full L2. +**Milestone targets:** v0.3 alpha ships full L2 plus wholesale feed cache-scope semantics. --- ### Track 6 — L3 idempotency, async tasks, webhooks -**ID:** `async-l3` | **Owner:** TBD | **Size:** 2.0 person-months +**ID:** `async-l3` | **Owner:** TBD | **Size:** 2.5 person-months **Scope:** @@ -304,12 +354,17 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is - Both executors injectable on `WebhookEmitter.builder()`. - Async-result polling shape on the caller side. - Error-recovery classification consumed from the spec's `error-code.json` `enumMetadata` (PR #3738). SDK consumes; doesn't re-derive. +- `IDEMPOTENCY_IN_FLIGHT` with transient recovery and claim-age-derived `retry_after`; Java callers must retry without minting a new key. +- Universal `idempotency_key` handling for read and write tasks, including byte-identical replay and `replayed` advisory fields on cached read responses. +- Durable account-level webhook subscriptions from `sync_accounts.accounts[].notification_configs[]`, with `subscriber_id` stable replace/upsert semantics, endpoint proof-of-control, paused-config behavior, and typed webhook token echo. +- Wholesale feed webhook parse/normalize/send helpers for `product.*`, `signal.*`, and `wholesale_feed.bulk_change`; repair path is always `get_products` / `get_signals`. +- Advisory-success payload handling for stale cached upstreams (`STALE_RESPONSE`) and SDK-generated projection warnings. **Out of scope:** Persistence migrations beyond reference schemas (adopter responsibility). **Depends on:** `codegen`, `signing` (webhook outbound), `multitenant` (sandbox boundary). -**Milestone targets:** Partial in v0.3 (idempotency + async tasks). Full webhook emitter in v0.4. +**Milestone targets:** Partial in v0.3 (idempotency + async tasks + cache-scope). Full webhook emitter and wholesale feed webhooks in v0.4. --- @@ -322,6 +377,7 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is - Decide between RFC paths 1 and 2 (RFC §Lifecycle and transition validation). Recommendation in RFC: path 2 (lead the cross-SDK shared YAML lifecycle source). Decision depends on TS + Python maintainer commitment — see [Decisions wanted](#decisions-wanted). - If path 2: author lifecycle YAMLs in the spec repo for the 7 resources (`MediaBuy`, `Creative`, `Account`, `SISession`, `CatalogItem`, `Proposal`, `Audience`). Wire all three SDKs to consume them. - Transition validator API takes `(action, from, to)`, not `(from, to)` — `NOT_CANCELLABLE` precedence over `INVALID_STATE` requires the action. +- 3.1 action-discovery helper layer: parse `allowed_actions[]` and `available_actions[]`, model `update_media_buy` mutations as sealed request subtypes where possible, expose `decomposeUpdateMediaBuy()` only as a compatibility fallback for legacy/non-sealed shapes, and surface `ACTION_NOT_ALLOWED` with structured details before handler side effects. - `TransitionGuard` SPI for adopter preconditions. Guards run **after** the spec edge check; can never relax a spec edge. - Guard narrowing protection: guards declare which edges they touch; conformance harness fails if a sandbox account's guards narrow any edge the storyboards exercise (RFC Open Question 7). @@ -341,6 +397,7 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is - `seed_*` / `force_*` / `simulate_*` controller surface matching `@adcp/sdk`'s `/conformance` and `/compliance`. - Sandbox-only enforcement wired at the `AccountStore` boundary (production → `COMPLY_NOT_AVAILABLE`). +- 3.1 controller additions: `force_upstream_unavailable`, typed `simulate_delivery` params for reach/frequency/reach_window/viewability, webhook proof-of-control probes, stale-response exercises, and product-signal / canonical-format coverage. - Storyboard hint fix-plan format (`Diagnose / Locate / Fix / Verify`) surfaced in adopter-facing test reports. **Depends on:** `multitenant` (sandbox boundary), `codegen`. @@ -359,6 +416,10 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is - `AdcpAgentExtension` — JUnit 5 extension that boots an in-process agent (or wraps an adopter's agent) for storyboard runs. - `StoryboardRunner` — Java port of TS `runStoryboard`. Reads YAML storyboards from the protocol bundle, runs them against an agent under test, asserts wire conformance. - **Mock-server forwarding adapter** (critical — RFC §`comply_test_controller`): storyboards certify against the shared reference mock-server, not an in-process Java mock. Without this, storyboards run against the SDK's own L4 stub instead of the spec-compliance oracle, and certification fails. +- 3.1 runner output shape: `steps_not_selected`, `not_selected_by_reason`, selected-but-skipped separation, `storyboards_missing_tools` / `storyboards_not_applicable`, `RunnerNotice`, and structured response-schema validation errors. +- 3.1 storyboard input gating: consume `required_any_of_tools` as a storyboard-schema gate and surface failures via `requirement_unmet` detail. Keep account-discovery synthesized gating until the upstream bundle fully migrates. +- Multi-bundle validator selection for compliance runs: a 3.1 runner can execute supplied 3.0 bundles with matching schema cache instead of assuming the installed SDK's primary pin. +- 3.1 storyboard primitives: `parallel_dispatch`, `cross_response_*` checks, advisory `errors[]` assertions, stale-response placement, read-tool idempotency envelope fields, canonical-format projection, product-signal targeting, vendor-metric optimization, reach-window/viewability delivery reporting, billing finality, and action discovery. - `MockAgent` for callers under test (buyer-side mirror). - `Personas` port of `/testing/personas`. - Signing test fixtures (port of `/signing/testing`). @@ -367,7 +428,7 @@ The RFC's M+12 target is the realistic line. Pre-committing M+9 and slipping is **Depends on:** `codegen`, `transport`. The mock-server forwarding adapter is the v0.1 release gate — without it, CI claims conformance it doesn't have. -**Milestone targets:** v0.1 ships the storyboard runner + forwarding adapter. Conformance test surface expands at each subsequent milestone as L1/L2/L3 land. +**Milestone targets:** v0.1 ships the storyboard runner, mock-server forwarding adapter, 3.1 output splits/notices, and multi-bundle validator selection for compliance runs. Conformance test surface expands at each subsequent milestone as L1/L2/L3 land. --- @@ -510,7 +571,7 @@ Additional decisions added post-RFC that remain open: 9. **MIT-licensed dependency position.** D9 picked the MIT-licensed `io.modelcontextprotocol.sdk`. License is compatible with Apache 2.0 downstream use, but the foundation may want an explicit position on accepting MIT deps in officially supported SDKs. 10. **Funding model shape.** RFC framing (contributed engineer at 50%+ for ~12 months + named maintainer + 2–3 design partners) is the right ask; whether it's pooled member funding, single-anchor-org contribution, or foundation grant is open. -11. **Design partner outreach.** Anchor candidates by audience segment: one publisher running Spring Boot, one SSP, one broadcaster middleware team. Specific shops TBD. +11. **Design partner outreach.** Anchor candidates by audience segment: one publisher running Spring Boot, one SSP, one broadcaster middleware team, and one EU ad-server vendor on Spring Boot 3.x / OAuth 2.1 / multi-tenant signing shape. Concrete company names belong in a private outreach tracker until they commit engineering time or an LOI; still need 2–3 committed design partners before scaling. 12. **WG vote timing.** Recommendation: hold the vote at v0.1 alpha milestone (concrete working code) rather than now (abstract commitment). ## What's not in this plan (yet) @@ -526,7 +587,8 @@ Additional decisions added post-RFC that remain open: | RFC merged on adcontextprotocol/adcp | ✅ (PR #4279, 2026-05-13) | | RFC imported to this repo | ✅ ([docs/rfc/java-sdk-rfc.md](docs/rfc/java-sdk-rfc.md)) | | Implementation plan drafted | ✅ (this doc) | -| Confirmed decisions D1–D21 locked | ✅ | +| Confirmed decisions | ✅ D1–D21 locked; D22 + D23 framing locked, shape/value details captured in this roadmap and linked specs. | +| Protocol parity check | 🟡 Behind — local Java pin is `3.0.11`; latest upstream check is AdCP `3.1.0-beta.5`, TS `@adcp/sdk@8.1.0-beta.13`, Python `adcp@6.3.0b4` on `3.1.0-beta.4`. | | Funding / staffing confirmed | ⏳ Decision pending | | Tracks claimed | 3 / 14 — `infra` (Track 1, #2), `codegen` (Track 2, #11), `transport` (Track 3, #17) | | Pre-contributor harness | 🟡 In progress — Gradle skeleton, codegen MVP, SSRF skeleton, schema fetcher, mock-server CI gate, IPR workflow, commitlint, changesets, MCP prototype findings all landed. Foundation admin actions outstanding: IPR Bot install, DNS TXT for Sonatype, @MichielDean collaborator. | diff --git a/specs/agent-surface-emission.md b/specs/agent-surface-emission.md new file mode 100644 index 0000000..69575cb --- /dev/null +++ b/specs/agent-surface-emission.md @@ -0,0 +1,20 @@ +# Agent surface emission + +**Status:** Design spec for D23 codegen/tooling parity +**Tracks:** [`codegen`](../ROADMAP.md#track-2--l0-types--codegen), [`transport`](../ROADMAP.md#track-3--l0-transport-mcp--a2a), [`testing`](../ROADMAP.md#track-9--testing--conformance) +**Decisions referenced:** D9, D23 + +## Why this exists + +D23 pins Java to the signed AdCP bundle for schema/API truth. That affects more than Java records and validators: generated MCP tools and A2A agent metadata must expose the same semantics that TS/Python expose to clients, registries, and conformance storyboards. + +## Emission rules + +- **MCP tool descriptions:** generated descriptions consume the protocol bundle's tool and field text. The Java SDK may tighten grammar for JavaDoc, but must not invent semantics that are absent from the bundle. +- **MCP input annotations:** universal request fields such as `idempotency_key` are represented on every tool input. When MCP supports idempotency annotations, generated tools set `idempotentHint: true` for tools whose AdCP request surface accepts idempotency. +- **A2A skill IDs:** generated A2A skill IDs follow a cross-SDK stable convention so Java and TS agents dedupe cleanly in registries. The convention is owned by the protocol bundle or a follow-up cross-SDK spec, not by local Java names. +- **Java names are not wire names:** Java method/class names may follow Java idiom, but emitted MCP tool names, A2A skill IDs, and JSON schema names stay protocol-stable. + +## Test contract + +The v0.1 conformance runner should snapshot generated MCP tool descriptors and A2A skill metadata from the target bundle. Snapshot diffs require either a bundle bump or an explicit roadmap/spec update. diff --git a/specs/codegen-open-enums.md b/specs/codegen-open-enums.md new file mode 100644 index 0000000..bffb20b --- /dev/null +++ b/specs/codegen-open-enums.md @@ -0,0 +1,64 @@ +# Open enum generation + +**Status:** Design spec for Track 2 generator invariants +**Tracks:** [`codegen`](../ROADMAP.md#track-2--l0-types--codegen), [`transport`](../ROADMAP.md#track-3--l0-transport-mcp--a2a) +**Decisions referenced:** D23 + +## Why this exists + +Some AdCP vocabularies are intentionally open from an SDK compatibility point of view. A Java strict enum parse would turn a newly added protocol value into a deserialization failure before caller code can inspect recovery hints or raw values. That is wrong for values like `error.code`, where older SDKs should remain able to receive, log, and conservatively classify new codes. + +Closed spec vocabularies can still generate Java `enum` types. Open vocabularies use an unknown-safe shape. + +## Default shape + +```java +public sealed interface ErrorCode permits ErrorCode.Known, ErrorCode.Unknown { + String rawValue(); + + record Known(KnownErrorCode value) implements ErrorCode { + @Override + public String rawValue() { + return value.wireValue(); + } + } + + record Unknown(String rawValue) implements ErrorCode { + } +} +``` + +The generated JSON adapter maps recognized wire values to `Known` and unrecognized wire values to `Unknown`. Serialization preserves `rawValue()` exactly. + +## Jackson binding + +Open enum wrappers do not use Jackson `@JsonTypeInfo`; they serialize as the flat protocol string. The generator emits a custom `JsonDeserializer` for each open vocabulary that maps the incoming string to `Known` or `Unknown`, plus a matching `JsonSerializer` that writes `rawValue()`. + +This is separate from polymorphic envelope handling in Track 2. Envelope types may use discriminator-based Jackson handling; open vocabularies must not, because the wire value is a scalar string. + +## Generator rules + +- Open vocabularies generate a sealed wrapper with `Known` and `Unknown`. +- The nested known-value type may be a Java enum when the known value set is useful for switch exhaustiveness. +- Unknown raw values are never rewritten, lowercased, uppercased, or mapped to a generic `UNKNOWN` sentinel that loses the original string. +- Each open vocabulary emits Jackson serializer/deserializer bindings that preserve the flat scalar wire shape. +- Closed vocabularies may generate plain Java enums. +- The schema post-processor owns the open/closed classification from spec metadata such as `enumMetadata`; contributors must not infer it from value count. + +## Error-code handling + +`ErrorCode` is open. Transport and caller helpers classify known values directly, and classify unknown values from recovery metadata when present. If no recovery metadata is present, callers default conservatively and preserve the raw code for logs and telemetry. + +## Initial open vocabulary list + +The first generator pass treats at least these vocabularies as open: + +- `ErrorCode` +- `media_buy_status` +- `creative_status` +- `recovery` +- action-discovery enums used in `allowed_actions[]` and `available_actions[]` +- notification types, including wholesale-feed webhook events +- task status values + +The schema post-processor may mark more vocabularies open as the protocol evolves. Contributors should not treat this list as exhaustive. diff --git a/specs/signing-context.md b/specs/signing-context.md new file mode 100644 index 0000000..a3d66f7 --- /dev/null +++ b/specs/signing-context.md @@ -0,0 +1,79 @@ +# Signing context for tenant-aware key selection + +**Status:** Design spec for D22 +**Tracks:** [`signing`](../ROADMAP.md#track-4--l1-signing), [`multitenant`](../ROADMAP.md#track-5--l2-account-store-registry-multi-tenant) +**Decisions referenced:** D2, D22 + +## Why this exists + +The Java signing surface cannot assume one signing key per `adcp_use`. Multi-tenant operators need to select a key by purpose plus resolved tenant/principal context, so a single JWKS endpoint can expose multiple keys with the same purpose and distinct `kid` values. + +D22 freezes the direction before the `signing` track opens: key selection is explicit-parameter based. Tenant context is passed as data, not hidden in `ScopedValue`, so callers, test fixtures, and framework adapters can see which tenant drives `kid` selection. `ScopedValue` remains available for request-scoped recorder/account context elsewhere, but not as the primary signing API. + +## API shape + +```java +package org.adcontextprotocol.adcp.signing; + +import org.jspecify.annotations.Nullable; + +public interface SigningContext { + AdcpUse use(); + @Nullable TenantId tenant(); + @Nullable PrincipalRef principal(); + + static Builder builder(AdcpUse use) { ... } +} +``` + +`tenant` is the operator-side tenant identity used for key selection. `principal` is the on-behalf-of account/principal identity, such as an advertiser or buyer principal represented by an agency/DSP caller. They may be equal in simple deployments, but the API keeps them separate. + +The public signing SPI takes `SigningContext` on the outbound signing path: + +```java +public interface SigningProvider { + Signature sign(SigningContext context, SigningInput input); +} +``` + +Inbound verification starts from the signed request, especially the `kid` header. The resolver maps the inbound key id to a verification key and any resolved tenant/principal metadata; the verifier then checks the signature and `adcp_use` purpose. + +```java +public interface VerificationKeyResolver { + VerificationKeyLookup resolve(VerificationInput input); +} + +public record VerificationInput( + AdcpUse expectedUse, + String kid, + SignedInput input) { +} + +public sealed interface VerificationKeyLookup { + record Found(VerificationKey key, @Nullable TenantId tenant, @Nullable PrincipalRef principal) + implements VerificationKeyLookup {} + record Missing(String kid) implements VerificationKeyLookup {} +} +``` + +There is no shipped API shaped as `SigningProvider.forUse(AdcpUse)`. v0.2 ships the `SigningContext`-based surface only. + +## Selection rules + +- `use` is required and maps to the JWK `adcp_use` purpose check. +- `tenant` is nullable for single-tenant deployments and caller-side signing where no publisher account has been resolved yet. +- `principal` is nullable and carries the resolved account/principal when available. +- Providers may use `tenant`, `principal`, or both to select `kid`; they must not ignore `use`. +- `kid` is an opaque lookup key into a pre-provisioned key set. Resolver implementations must not parse authority, tenant, or URL semantics out of `kid`. +- Resolver implementations must not dereference attacker-controlled URLs from an inbound signed object. If a resolver refreshes a JWKS or other key source over HTTP, that fetch uses the strict SSRF-safe HTTP client from [`ssrf-baseline.md`](ssrf-baseline.md). +- Verification starts from inbound `kid`; tenant context is derived after key lookup and never assumed before signature verification. +- Verification still enforces the key purpose at JWK `adcp_use`. +- If verification returns `Found(key, null, null)`, receivers treat the request as untenanted and reject tenant-scoped operations. They must not recover tenant identity from unsigned body, header, or query fields after the fact. + +## Release notes + +The v0.2 release notes must warn `SigningContext.tenant()` may be null until v0.3 wires tenant resolution from `AccountStore` / `adagents.json`. Provider implementations must not bake in a single-tenant assumption just because early alpha contexts are null. + +## Milestone contract + +v0.2 freezes the `SigningContext` parameter set and the `SigningProvider` / `VerificationKeyResolver` SPI direction. v0.3 wires `AccountStore` / `adagents.json` principal resolution into the context passed to webhook and signed-request providers.