Skip to content

feat(server): let PrincipalResolverError carry a stable error code - #1633

Merged
bokelley merged 1 commit into
mainfrom
claude/issue-1623-principal-resolver-code
Oct 26, 2026
Merged

bokelley merged 1 commit into
mainfrom
claude/issue-1623-principal-resolver-code

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Fixes #1623

What changed

PrincipalResolverError takes an optional keyword-only code:

raise PrincipalResolverError(403, code="identity_org_mismatch")
# -> 403 {"error": "identity_org_mismatch"}
  • code is checked with fullmatch against ^[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 generic forbidden/unauthenticated body for that status. The auth log records only reason="invalid_code", never the value.
  • The exception message is never sent. With no code, behaviour is unchanged: the generic bodies, the WWW-Authenticate Bearer challenge on 401, and no challenge on 403.
  • code works 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:
    • valid codes on the MCP and A2A legs for 401 and 403, with the challenge header still checked and the exception text not leaked;
    • unsafe codes on both legs (uppercase, leading digit or underscore, hyphen, space, trailing newline, 65 characters, token=..., non-string) falling back to the generic body without logging the value;
    • the combined serve() app (MCP and A2A on one ASGI app) answering 403 {"error": "identity_org_mismatch"} on both transports.
  • tests/type_checks/principal_resolver.py: an adopter fixture using code= (mypy --strict and pyright).
  • docs/principal-resolution.md: documents code, the pattern and the fallback.
  • make lint, make typecheck-all and make test pass 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 clean origin/main.

🤖 Generated with Claude Code

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>
@bokelley
bokelley force-pushed the claude/issue-1623-principal-resolver-code branch from 8da049e to 5deec1c Compare October 26, 2026 01:58

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — no blocking findings.

Checked:

  • Adds an optional keyword-only code to the public PrincipalResolverError, echoed as {"error": code} only when it passes fullmatch against ^[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 only reason=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.

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean additive change to the public server auth surface.

What I checked:

  • Additive keyword-only code field on PrincipalResolverError: validated fail-closed at render time via fullmatch (correct anchoring despite the unanchored pattern), falls back to the generic body on any non-matching or non-string value, logs only reason=invalid_code with 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_metadata concern; 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.

@bokelley
bokelley merged commit cf96d57 into main Oct 26, 2026
47 checks passed
@bokelley
bokelley deleted the claude/issue-1623-principal-resolver-code branch October 26, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let PrincipalResolverError carry a stable error code, not only a status

1 participant