fix(server): Tier 2 permission-denied parity contract (#392)#735
fix(server): Tier 2 permission-denied parity contract (#392)#735bokelley wants to merge 1 commit into
Conversation
Folds the four PERMISSION_DENIED branches in `_resolve_buyer_agent` through a single emit point with: - A single internal PermissionDeniedError + translator (was 4 inline envelope sites). - Deadline-relative latency budget on every denial path, env-tunable via ADCP_PERMISSION_DENIED_BUDGET_MS (default 50 ms). - Audit-row parity: uniform operation label, uniform key set, agent_url hashed-truncated to defend against log-scraping. - Header-parity tradeoff documented (Content-Length variance within the spec's omit-on-unestablished-identity allowance). Closes the structural / latency / observability gaps deferred from #375 (wire-code rename). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Holding for human security review before merge — this is a 1,100-line auth refactor with explicit timing-side-channel + audit/header parity concerns. The implementing agent self-flagged that an independent security-reviewer pass is the right next step. Not blocking the 5.5.0 release. Will ship in a subsequent release after review. |
|
Closing as superseded by #748. Quick recap of why this PR's wire-level parity premise no longer holds, and where the residual value moved: Why this PR is supersededPR #748 (merged 2026-05-20) migrated the recognized-but-denied paths to dedicated wire codes ( This PR was built against the pre-#748 world where all four denial branches funneled through Per the protocol-expert review of #748: "recognized-but-denied path no longer carries Independent issues this PR hadReviewed by
Residual value moved to #772The narrow follow-up at #772 captures what remains useful:
The parent issue #392 will also close, since its 4-numbered design list is largely obsoleted by #748 and the residue is covered by #772. Thanks to the original implementing agent for the careful scoping note — the "holding for human security review" comment was the right call, and the review surfaced the supersession. |
|
Acknowledged — thanks for the detailed write-up. The supersession by #748 (dedicated wire codes + Triaged by Claude Code. Session: https://claude.ai/code/session_01SV2WtK4kVkhhErSe8Eeind Generated by Claude Code |
…#772) (#774) Adds PermissionDeniedBudget to floor both PERMISSION_DENIED branches in _resolve_buyer_agent (registry-miss / no-credential, and unknown-status default-reject) at the same deadline. Registry I/O variance between the two branches (cache-hit-returning-None vs. real-row read) is now absorbed into a fixed budget rather than leaking as a latency oracle. Scope is intentionally narrow per #772 (extracted from the larger #735 that was superseded by #748's dedicated-code migration): - Budget applies only to PERMISSION_DENIED. AGENT_SUSPENDED / AGENT_BLOCKED skip the budget — the code itself is the discriminator, so latency parity carries no additional bit. - Env-tunable via ADCP_PERMISSION_DENIED_BUDGET_MS (default 50 ms). - Fails closed on misconfiguration: 0, negative, nan, inf, and non-numeric values fall back to the default and log WARNING. The defense is never silently disabled. - Deadline-relative sleep using perf_counter() so audit/registry latency variance is absorbed into the budget rather than added on top. The audit-hygiene / DenialAuditRecord work (#772 PR B) is descoped — no concrete SecOps consumer asking for it; can be re-filed if one surfaces. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes #392. Closes the structural / latency / header / observability gaps in the cross-tenant onboarding-oracle clamp left over from #375.
PermissionDeniedError+ translator (was 4 inline envelope sites in_resolve_buyer_agent)ADCP_PERMISSION_DENIED_BUDGET_MS, default 50 ms)buyer_agent_registry.permission_denied, uniformdetailskey set,agent_urlhashed-truncated viasha256(...)[:12]so log-scraping cannot rebuild the side channelContent-Lengthvariance is bounded bydetailspayload size; the spec's omit-on-unestablished-identity rule prevents padding the unrecognized envelope, and the intra-recognized variance (agent_urlis buyer-controlled) already exceeds the recognized-vs-unrecognized deltaSecurity review
Self-checks performed against the design's review points:
Field-presence side channel in audit emission? The audit row's
detailskey set is identical on every denial path —outcome,reason_scope,reason_status,agent_url_hashare all present; values areNoneon the unrecognized branch. Verified bytest_audit_emit_uniform_details_key_setandtest_audit_emit_unrecognized_carries_none_hash_not_missing_key. On the wire,detailsis omitted entirely on unrecognized (the empty dict onAdcpErroris dropped byto_wire()), preserving the existing spec-conformance behavior.Ordering leak between audit emit and budget sleep? Identical ordering on every branch, structurally enforced by routing all branches through
raise_permission_denied: capture deadline → emit audit → sleep until deadline → raise. The deadline-relative sleep (rather than fixed-duration) absorbs audit-sink variance so total wall-clock is dominated by the budget regardless of branch.Padding content leak? No padding applied — the spec's omit-on-unestablished-identity rule prevents padding the unrecognized envelope. The tradeoff is documented in the module docstring with the justification that intra-recognized
agent_urllength variance already exceeds the recognized-vs-unrecognized delta.What I'd want a security reviewer to verify next:
PlatformHandlernow acceptspermission_denied_audit_sink— verify that the v3 reference seller / SalesAgent wires this to the same sink as the registry-cache layer so SecOps sees registry events and denial events in a single stream.raise_permission_deniedfromPlatformHandlersetup overhead so the assertion targets the parity property rather than handler jitter. Verify the test stays sub-5ms on the CI runner profile; if CI moves to noisier hardware, the budget in that test can be raised (the production setting is independent).Test plan
tests/test_tier2_parity_contract.py::test_latency_parity_p99_difference_under_budget)Content-Length(test_header_content_length_within_tolerance); two unrecognized envelopes byte-identical (test_unrecognized_envelopes_are_byte_equivalent)test_audit_emit_same_operation_label_across_paths,test_audit_emit_uniform_details_key_set,test_audit_emit_agent_url_is_hashed_not_plaintext,test_audit_emit_unrecognized_carries_none_hash_not_missing_key)test_status_code_parity_across_four_paths)test_raising_sink_does_not_propagate)test_budget_env_var_*)tests/test_tier2_spec_conformance.pystill passes (12 tests green)ruff check src/+mypy src/adcp/clean🤖 Generated with Claude Code