Repository navigation
feat(reporting): settle reporting-subscription ambiguities found by the SDK implementations - #8585
Conversation
…he SDK implementations Offerings publish standard_dimensions / unmapped_dimensions so match is resolvable from the wire; exact without dimensions, period_timezone matching, civil vs elapsed period arithmetic, per-offering cutover E, authorization chains for authorized_at, reactivation as a fresh start with inactive_intervals readback, scope_resolved_at as period end, and per-offering gaps / timezone cutovers with previous_timezone. Repins the worst-case capabilities size (91,804,421 bytes). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Escalate to human review
Escalate — gated schema paths require human/CODEOWNERS approval.
This PR modifies published protocol source under static/schemas/source/** (6 files), which is a deterministic hard approval gate (gated_paths: true), and review_decision is REVIEW_REQUIRED (not APPROVED). Per decision rule row 2, this must escalate until a real human/CODEOWNERS approval lands.
The reviewer found no Critical/High/Medium findings: schema↔docs coherence holds across sync_principal.mdx and get_reporting_status.mdx, the changeset is present and correctly scoped, the x-added-in sweep and worst-case-bytes tests are updated/pinned, no new undiscriminated oneOf, and no dist/ artifact edits. These are beta.2 clarifications/additions to the experimental media_buy.reporting_subscriptions surface. The diff is mechanically clean — but the gate on published schema source is non-overridable without an APPROVED review.
Escalation reasons
- Modifies gated schema source files (
static/schemas/source/core/reporting-subscription.jsonand 5 sibling files) —review_decisionis REVIEW_REQUIRED; human/CODEOWNERS approval required before merge.
Why human review
- Modifies gated schema source under static/schemas/source/core/ (reporting-subscription.json, reporting-subscription-state.json, reporting-subscription-coverage.json, reporting-schedule.json, reporting-delivery-offering.json, account-authorization.json); gated_paths is true and review_decision is REVIEW_REQUIRED (not APPROVED) — human/CODEOWNERS approval required.
- This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/account-authorization.json (modified) matches
static/schemas/source/**; static/schemas/source/core/reporting-delivery-offering.json (modified) matchesstatic/schemas/source/**; static/schemas/source/core/reporting-schedule.json (modified) matchesstatic/schemas/source/**; static/schemas/source/core/reporting-subscription-coverage.json (modified) matchesstatic/schemas/source/**; static/schemas/source/core/reporting-subscription-state.json (modified) matchesstatic/schemas/source/**; static/schemas/source/core/reporting-subscription.json (modified) matchesstatic/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.
…cp#8585 adcontextprotocol/adcp#8585 settles the ambiguities both SDKs hit. - Offerings publish standard_dimensions and unmapped_dimensions. SubscriptionOffering.from_offering reads them from the wire, and sellers use with_subscription_dimensions() to publish them. subscription_capabilities() refuses an offering without them. - Coverage consumes the readback's inactive_intervals. - timezone_cutovers[] carry previous_timezone and an optional offering_id, and gaps[] carry an optional offering_id. An entry with an offering_id applies to that offering only, and previous_timezone supplies the zone before the first cutover. - period_timezone follows the normative text: a utc offering matches only UTC or Etc/UTC, a source_timezone offering only its published zone (never an account-resolved one), and an account_timezone offering takes the override. The beta.1 models close these objects, so _reporting_subscription_preview adds the fields to the generated classes until a 3.3.0-beta.2 bundle carries them. The vendored TypeScript parity table keeps a scope valid under maxProperties 1 in three cases, matching the fix pending upstream. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adopt the rules adcontextprotocol/adcp#8585 settled: - Selection reads each offering's published standard_dimensions and unmapped_dimensions, and fails closed on an offering without them. - period_timezone: a utc offering matches only UTC or Etc/UTC; a source_timezone offering only its own published zone (never an account-resolved one). - The coverage function takes the readback's inactive_intervals in wire shape, per-offering timezone_cutovers with previous_timezone (account.timezone is now the current zone), and per-offering gaps. - The parity table fixes the billing-cycle case's scope (both keys, which maxProperties 1 forbids), adds cases for the settled rules, and its subscriptions are now schema-validated. The new wire fields are hand-modeled in subscriptions/wire.ts until 3.3.0-beta.2 ships. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r capabilities (#1696) * feat(reporting): compute owed reporting-subscription periods and their capabilities Add adcp.reporting.subscriptions, the shared half of AdCP 3.3 principal-level reporting subscriptions (media_buy.reporting_subscriptions): - expected_subscription_periods(): the coverage rule from core/reporting-subscription.json as a pure function over plain data. It owes each confirmed buy and resolved offering the complete periods that start at or after the latest of the version's accepted_at, the buy's confirmed_at and the account's authorized_at, and that overlap the flight as of each period's scope_resolved_at. It applies cutovers for versions, deactivation, resolution changes and account timezone changes, removes exceptions and gaps over [from, until), and handles revocation and re-grant with and without backfill_on_regrant. Results are ExpectedReportingPeriod values, so they feed evaluate_reporting_ledger and the consumer-status planner unchanged. - resolve_subscription_offerings(): exact / finest / all selection with ties to the lowest offering_id in byte order. - subscription_capabilities() and advertise_reporting_subscriptions(): configuration_tasks, the deprecated configuration_task only when sync_accounts is listed, the subscriptions block, the feature id and the principal section. Civil-time schedules carry the normative local-midnight origin explicitly, so they do not depend on the ledger's legacy anchor default. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): derive subscription periods exactly as the TypeScript SDK does Align expected_subscription_periods with adcontextprotocol/adcp-client#3505 so that both SDKs owe identical periods. The spec leaves each of these choices open: - A dedicated period calendar. Years, months and days are local civil arithmetic with the day clamped from the origin. Hours, minutes and seconds are exact elapsed time. Calendar months (P1M billing cycles) are supported, and numeric UTC offsets are refused. - Cutovers are paired per buy over the union of its packages. A continuing offering hands off at E of its own outgoing calendar. A newly resolved one starts at the latest E of everything the previous regime resolved. - A period_timezone override moves account_timezone and account-resolved schedules into its zone, while utc and fixed-zone schedules match only their own zone. - Versions may carry in-place inactive intervals and their destination method. The coverage entry's timezone_cutovers are applied. - A re-grant continues authority only before unauthorized_at plus status_retention_days, and a re-grant without retention is an error. The TypeScript SDK's 46-case table is vendored and run as a parity test. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(reporting): adopt the reporting-subscription rules settled in adcp#8585 adcontextprotocol/adcp#8585 settles the ambiguities both SDKs hit. - Offerings publish standard_dimensions and unmapped_dimensions. SubscriptionOffering.from_offering reads them from the wire, and sellers use with_subscription_dimensions() to publish them. subscription_capabilities() refuses an offering without them. - Coverage consumes the readback's inactive_intervals. - timezone_cutovers[] carry previous_timezone and an optional offering_id, and gaps[] carry an optional offering_id. An entry with an offering_id applies to that offering only, and previous_timezone supplies the zone before the first cutover. - period_timezone follows the normative text: a utc offering matches only UTC or Etc/UTC, a source_timezone offering only its published zone (never an account-resolved one), and an account_timezone offering takes the override. The beta.1 models close these objects, so _reporting_subscription_preview adds the fields to the generated classes until a 3.3.0-beta.2 bundle carries them. The vendored TypeScript parity table keeps a scope valid under maxProperties 1 in three cases, matching the fix pending upstream. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): take each offering's prior zone from its own timezone cutover Follow adcp#8585: the account's timezone is its current zone, as list_accounts shows it. Before a timezone change, an offering's account-timezone periods use that change's previous_timezone, considering only the changes that apply to the offering. An offering with no applicable change uses the current zone throughout. A timezone change without previous_timezone is now refused, and so is a wire offering without standard_dimensions. Previously the first cutover's previous_timezone became the base zone for every offering, so a cutover scoped to one offering could shift another offering's boundaries. Re-vendor the TypeScript parity table at f8b2fddd (54 cases, with the scope fix now upstream). Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(reporting): serve adcp#8585 subscription fields only to 3.3.0-beta.2 callers The server validates responses against the release it serves, and the pinned 3.3.0-beta.1 schemas close the objects adcp#8585 extends. The new wire module gates the new fields on 3.3.0-beta.2 or later, as the TypeScript SDK does: - standard_dimensions and unmapped_dimensions on offerings; - inactive_intervals on the readback; - previous_timezone and offering_id on timezone_cutovers[] and gaps[]. For older callers, per-offering gaps collapse into one account-wide gap spanning them, and per-offering cutovers into one entry at the latest effective_at. The parity test now also validates every subscription in the TypeScript table against core/reporting-subscription.json. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): address the coverage-rule review and adopt adcp#8588 periods Fixes from the expert review of the coverage rule: - An empty flight (stopped at or before it started) overlaps no period, so a buy canceled before its flight owes nothing. - Each regime carries its per-offering handoff. E for a continuing or new offering is the latest of the outgoing calendar's boundary and the outgoing regime's own handoff, so two cutovers inside one period never owe a period twice. A timezone change passed at T or at its E now gives the same result. - The extensible reporting-dimension vocabulary parses on selectors, offerings and the capability block; an unknown selector value matches nothing. - Zone names must be IANA zones (posixrules and right/... are refused), and offerings with a year or month delivery_sla are refused at construction. - The beta.1 projection keeps distinct timezone changes distinct and drops prior_versions[].configuration. The feature id is refused without the principal section. Stale docstrings are corrected. adcp#8588 (pending for 3.3.0-beta.2): - Sub-daily periods under a local alignment restart at each local midnight on wall-clock multiples, so a DST day has 23 or 25 hourly periods. The duration must divide 24 hours and cannot mix date and time components. - prior_versions[].configuration is consumed, and require_prior_configurations makes a missing one an error. A version accepted while inactive has an empty window. The parity runner now checks row order, expected_obligations and the error class. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): accept undated continuation re-grants and re-vendor the parity table Re-vendor the TypeScript coverage table at a99597a4: 79 cases, adding - empty flights; - two cutovers inside one period; - sub-daily local periods; - DST gaps and folds at midnight; - a skipped civil date; - PT24H versus P1D; - month-end clamps; - three-grant chains; - simultaneous changes; - unknown dimensions. All pass under the strict runner. Match TS on one rule the table pins: a grant whose authorized_at is at or before the previous loss continues the chain at an unknown instant. This happens because adcp#8585 keeps authorized_at at the chain start after a re-grant inside retention. The buyer removes no gap of its own; the seller's gaps[] decide. Grants are read in input order. The schema check accepts dimension values outside the beta.1 enum, because the vocabulary is x-extensible. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): owe the period containing confirmed_at in full, per adcp#8593 Owner decision for 3.3.0-beta.2: - The start bound is the first boundary at or after the later of the version's accepted_at and the account's authorized_at. - A buy is then owed every period that ends after its confirmed_at, so the confirmation period is owed in full. - An empty flight still owes nothing. The rule lives in the one function that holds it, _first_owed_start. Re-vendor the TypeScript parity table at f8b48c0c. It has 81 cases, conformed to the same decision, and adds an ASAP buy confirmed mid-day and a monthly billing buy confirmed mid-month. All pass under the strict runner. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… track what they owe (#1698) * feat(reporting): accept reporting subscriptions on sync_principal and track what they owe Seller runtime for AdCP 3.3 principal-level reporting subscriptions, on top of the shared coverage rule. Principal runtime (opt-in): PrincipalService(reporting_subscriptions=...) or set_reporting_subscriptions() accepts the sync_principal reporting_subscriptions section. Without the option the section is ignored as before. With it, the service: - validates what the generated model cannot: exactly one scope form, exclude_account_ids only with all-accounts scope, period_timezone only for one named account, billing and consumer_receipt rules, destinations that exist after the same request, path_template placeholders and {account_id}, distinct templates on a shared destination, the delivery_config_id namespace, and same-level conflicts; - versions subscriptions: reused versions must keep their semantics; active and replace_account_configs change in place; new versions record accepted_at and prior_versions; omitted subscriptions are deactivated and retained; - reads back core/reporting-subscription-state.json. A destination needing action moves the subscription to action_required, and update_subscription_status() records seller-derived status without changing configuration_version. Both emit principal.changed with reason reporting_subscription_changed. ReportingSubscriptionEngine: host hooks list_authorized_accounts and account_buys, plus optional account_delivery_configs for precedence; authorization_changed() and reconcile() track grants and losses. It records these exceptions as cutover intervals: overridden (the caller's own sync_accounts config, or its account_ids subscription), capacity under a deterministic max_accounts order, excluded_by_buyer, and per-buy and per-package offering_unsupported. It also maintains accounts_with_exceptions and accounts_unauthorized (with withheld counts) and serves subscription_coverage with gaps and timezone_cutovers. owed_periods() runs the same expected_subscription_periods a buyer runs, and receipt_roster() derives receipt duty from active consumer_receipt subscriptions. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): announce a subscription's recovery from action_required transition_destination fired principal.changed with reason reporting_subscription_changed only when a subscription moved to action_required. Fire it on any subscription state change, so a caller that caches the readback learns when the destination is ready again. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): order max_accounts by the current grant chain The capacity order used an account's first grant ever. A re-grant at or after unauthorized_at + status_retention_days is a fresh coverage start, so it now starts a new chain for ordering too, matching the coverage rule. Also pin that destination transitions work when no principal.changed emitter is wired. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): keep max_accounts slots with the accounts already served Align the capacity order with the TypeScript SDK (adcp-client#3508). Accounts already served keep their slots, and the remaining slots go by account_id in byte order. A newly granted account never displaces one being served, which also settles the re-grant ordering case without depending on grant history. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(reporting): count withheld periods from host delivery facts when available A host may implement is_period_delivered(principal, account_id, period, by=). When it does, withheld_period_count counts the owed periods that ended at or before unauthorized_at and were not delivered by then. Without the hook, the engine still counts the periods that were not yet due when authority ended, as the TypeScript SDK does. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(reporting): emit the reporting-subscription fields settled in adcp#8585 Seller side of adcontextprotocol/adcp#8585. - The readback lists inactive_intervals. Deactivating, including by omission, opens an interval, and turning the subscription back on records reactivated_at. The engine reads reactivations from the readback instead of tracking them itself, so a buyer recomputes the same periods after local-state loss. - timezone_cutovers[] carry previous_timezone. Both timezone_cutovers[] and gaps[] carry one entry per offering_id when the covered offerings' boundaries differ, and a single entry for every offering when they agree. Coverage now resolves against the account's buys rather than every offering. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(reporting): gate the adcp#8585 readback and coverage fields by served release sync_principal gains an optional context, like get_principal. Both drop inactive_intervals from the subscription readback for callers served below 3.3.0-beta.2, or with no known release, because the server validates responses against the pinned 3.3.0-beta.1 schema. ReportingSubscriptionEngine.subscription_coverage takes adcp_version. For older callers it drops previous_timezone and offering_id, and reports per-offering gaps and cutovers account-wide. Tests pin that the projected coverage validates against the beta.1 schema. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): address the seller-runtime review of reporting subscriptions Fixes from the expert review of the principal runtime and engine: - With the option off, sync_principal rejects reporting_subscriptions with UNSUPPORTED_FEATURE instead of silently applying nothing. replace_account_configs: true is rejected the same way until it is implemented. - No UTC defaults. AuthorizedAccount requires an IANA timezone and the authorized_at that list_accounts serves. An account with no known zone fails its account-timezone schedules, isolated per subscription in evaluation_errors. Setting a first zone is not a cutover, and the wire's previous_timezone comes from each change. - Losing authority no longer closes open exceptions, so a backfilled re-grant never owes a second feed next to an override. - Each version's destination is snapshotted at acceptance, offerings that cannot be scheduled are refused at construction, prior versions without a known configuration are an error, and one failing subscription never stops the others. - Account-level exceptions use the earliest E across calendars, resolved per package. AccountDeliveryConfig.active_from opens an override at its own cutover. accounts_unauthorized lists only accounts the subscription served. Delivery facts are asked once per loss. - Readback: prior_versions carry their configuration (adcp#8588, beta.2+ only). A version bump while inactive stays computable. inactive_intervals are never dropped: the 65th deactivation is refused. - Validation: resends that only reorder uniqueItems arrays or spell out the default match are the same version, and braces outside placeholders are refused. render_path_template() and PrincipalService.reporting_subscription_ids() are added. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(reporting): keep backslashes from escaping a path-template segment render_path_template percent-encoded only values containing "/" or equal to "." or "..". account_id has no character-set constraint, so a value such as "..\..\x" could gain or escape a segment on a backslash-separated file_transfer target (SMB, UNC). Values containing a backslash are now encoded too. Other values are left as the spec writes them, so paths stay identical across SDKs. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The TypeScript (adcontextprotocol/adcp-client#3505, #3508, #3509) and Python (adcontextprotocol/adcp-client-python#1696, #1698, #1699) implementations of principal-level reporting subscriptions each hit the same spec gaps independently. Both SDKs converged on one reading, checked by a shared 46-case parity table. This PR makes that reading normative before beta.2.
matchfrom the wirestandard_dimensions(standard vocabulary) andunmapped_dimensions; sellers advertising subscriptions MUST publishstandard_dimensionson every offeringexactwithoutdimensionsoffering_idwinsperiod_timezone)account_timezoneuses the override;utcmatches only a UTC alias;source_timezoneand fixedbilling_cyclematch only their own published zone; a buyer-configuredbilling_cyclenever matches a subscriptionPT24H≠P1Dacross DST); zero-length periods are never owedauthorized_at"current grant" versus withheld periods staying owedauthorized_atstarts the current authorization chain; a re-grant withinstatus_retention_dayscontinues it; a later one starts freshinactive_intervalsscope_resolved_atfor subscription obligationsgaps[]andtimezone_cutovers[]are per account, but boundaries are per offeringoffering_idper entry; timezone cutovers carryprevious_timezone, because the account already shows the new zoneThe worst-case capabilities size moves from 91,262,597 to 91,804,421 bytes, because of the two new 16-entry arrays on every offering.
tests/capabilities-structural-bounds.test.cjsandsecurity.mdxare repinned.SDK follow-ups once this merges:
standard_dimensions/unmapped_dimensionsinstead of host-supplied dimensions.inactive_intervals,previous_timezone, and per-offering gap and cutover entries.scopehas bothall_authorized_accountsandaccount_ids, which the schema forbids.Tests:
tests/reporting-subscriptions.test.cjsgains three tests.test:schemas,test:json-schema,test:compliance-snippets, the MCP projection and capabilities bounds tests, the x-added-in sweep,test:docs-nav, andtest:owned-linkspass locally.🤖 Generated with Claude Code
Open workspace in Conductor