Repository navigation
feat(server): let PrincipalResolverError carry a stable error code - #1633
Conversation
PrincipalResolverError(403, code="identity_org_mismatch") answers
403 {"error": "identity_org_mismatch"} on MCP and A2A. Codes must match
^[a-z][a-z0-9_]{0,63}$; anything else falls back to the generic body.
Exception text is never sent, and the default bodies are unchanged.
Fixes #1623
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
8da049e to
5deec1c
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — no blocking findings.
Checked:
- Adds an optional keyword-only
codeto the publicPrincipalResolverError, echoed as{"error": code}only when it passesfullmatchagainst^[a-z][a-z0-9_]{0,63}$at the render boundary in_resolver_rejection. Invalid/non-string codes fail closed to the generic body and log onlyreason=invalid_code, never the value — no exception text leaks. - Backward-compatible addition correctly signaled with
feat(server):(not a breaking change; no removal/rename/required-flip on the public surface). - Both transport rejection paths (auth.py:662, :1750) route through the single chokepoint; internal fallback raises with no code.
- Tests cover both legs, 401/403, and a thorough unsafe-code matrix.
Decision-table trace: no critical/high/medium findings (rows 1, 4–6, 8 don't fire). gated_paths is false (row 2 n/a). high_risk is true but the only reason is src/adcp/server/auth.py (modified) with no medium finding on it (rows 3, 5 don't fire). No no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve. Note: high_risk + REVIEW_REQUIRED is a heuristic look-harder signal only, not an escalation trigger on a clean modification.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean additive change to the public server auth surface.
What I checked:
- Additive keyword-only
codefield onPrincipalResolverError: validated fail-closed at render time viafullmatch(correct anchoring despite the unanchored pattern), falls back to the generic body on any non-matching or non-string value, logs onlyreason=invalid_codewith no value leak, and preserves the 401 Bearer challenge. - Public export is additive with correct
feat(server):semver signal — no removal/rename/type-signature change, no required→optional flip, so no breaking-change footer required. - Both MCP and A2A paths covered by tests (
tests/test_auth_principal_resolver.py,tests/type_checks/principal_resolver.py). - No credential-in-
ctx_metadataconcern; no type-layering or generated-code edits.
Decision path: No critical/high/medium findings (rows 1, 4, 5, 6, 8 don't fire). gated_paths is false (row 2 n/a). high_risk is true but the only high-risk match is src/adcp/server/auth.py (modified) with no medium-or-higher finding — presumed safe per the modified-file rule; no deletions (rows 3, 5 don't fire). No no-auto-approve team match (row 7 n/a). Prior decision was approve, not escalate (row 6 n/a). Falls through to row 9 → approve.
Fixes #1623
What changed
PrincipalResolverErrortakes an optional keyword-onlycode:codeis checked withfullmatchagainst^[a-z][a-z0-9_]{0,63}$when the response is rendered, so a trailing newline doesn't pass. A non-matching value, including a non-string, falls back to the genericforbidden/unauthenticatedbody for that status. The auth log records onlyreason="invalid_code", never the value.code, behaviour is unchanged: the generic bodies, theWWW-AuthenticateBearer challenge on 401, and no challenge on 403.codeworks with both 401 and 403. Both transports (MCP and A2A) render through the shared_resolver_rejection, so both get the change.Tests
tests/test_auth_principal_resolver.py:token=..., non-string) falling back to the generic body without logging the value;serve()app (MCP and A2A on one ASGI app) answering403 {"error": "identity_org_mismatch"}on both transports.tests/type_checks/principal_resolver.py: an adopter fixture usingcode=(mypy --strict and pyright).docs/principal-resolution.md: documentscode, the pattern and the fallback.make lint,make typecheck-allandmake testpass locally except one pre-existing failure,tests/integration/test_a2a_context_id.py::test_two_calls_share_server_assigned_context_id. It fails only inside the full serial suite, and it fails the same way on cleanorigin/main.🤖 Generated with Claude Code