Skip to content

Tier 2 commercial-identity gate: latency / headers / side-effects parity contract #392

@bokelley

Description

@bokelley

Summary

Follow-up from #375 (PR for the wire-code rename portion). This issue tracks the parity-contract refactor that was deferred from that PR per the user's stop-rule ("if the parity contract turns out to be more complex than fits in a single PR, file a separate issue for the parity-contract follow-up").

Why

The cross-tenant onboarding-oracle clamp in the AdCP spec requires the unrecognized-agent path and the recognized-but-denied path to be observably indistinguishable to an external attacker — across:

  • HTTP status code (already aligned via shared PERMISSION_DENIED envelope).
  • Response headers (Content-Type, Content-Length within negligible tolerance).
  • Side effects (audit emission, metric increment with the same operation label).
  • Latency (cannot shortcut the unrecognized path; needs a deliberate budget).
  • Observability (logs, APM spans, third-party error telemetry).

#375's rename portion closed the code mismatch but left the eager-raise structural difference — see the docstring on _resolve_buyer_agent (the "Note on parity" paragraph) and the test-file docstring on tests/test_tier2_spec_conformance.py.

Required design

Per the issue body of #375:

Approach: introduce a PermissionDeniedReason internal enum/struct with (scope, status) fields. The dispatch layer raises a single PermissionDeniedError carrying a reason; the wire-level translator emits the spec-correct PERMISSION_DENIED envelope with details populated only when scope is set. The unrecognized path still does the same audit-emission + metric increment as the denied path. Latency parity may require an explicit small delay budget — document the tradeoff.

Concretely:

  1. Single emit point. Move all four denial paths in _resolve_buyer_agent (and the validate_billing_for_agent path) through one common projection — an internal PermissionDeniedError wrapping a PermissionDeniedReason struct, projected to the wire envelope by a single translator. Today four call sites build the envelope inline.
  2. Latency budget. The unrecognized path needs to do equivalent resolution work to the recognized-but-denied path before raising. The simplest design is an explicit small delay (50–100 ms?) on every denial path so the variance between branches is dominated by the budget rather than the code path. Document the budget tradeoff (extra latency on every denial vs. closing the side channel).
  3. Audit / metric parity. Today the recognized branches log details.agent_url and details.status; the unrecognized branch logs nothing. Need to converge on a uniform log shape — same operation label, same fields (with the discriminator either missing or hashed-then-truncated to prevent log scraping from becoming the side channel).
  4. Header parity. A2A and MCP transports both go through the framework's wire serializer. Verify Content-Length is within tolerance (the message strings are already identical post-Tier 2 commercial-identity error codes don't conform to AdCP spec — cross-tenant oracle risk #375; the variance is details size). Consider padding the unrecognized envelope to match a fixed byte count.

Tests

  • Latency parity test: assert p99 difference between unrecognized-agent and recognized-but-suspended paths is < 5 ms over N requests (with the explicit budget set high enough that branch-comparison variance is negligible).
  • Header parity test: same Content-Type, Content-Length within ±N bytes (where N covers the details shape variance).
  • Side-effects parity test: same audit-sink writes, same metric increments with the same operation label.
  • Status code parity test: same HTTP status across all four denial paths.

Refs

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions