Skip to content

fix(auth): operator messages through a checked registry; refuse unrenderable auth config at startup; README /mcp rule (see Consiliency/pmcp#326) - #340

Merged
ViperJuice merged 1 commit into
mainfrom
feat/326-auth-messages
Oct 3, 2026
Merged

ViperJuice merged 1 commit into
mainfrom
feat/326-auth-messages

Conversation

@ViperJuice

@ViperJuice ViperJuice commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Implements the merged plan .consiliency/plans/detailed-326-auth-messages-20261003-0226.md (#333) for #326.

What changes

  • One registry for auth operator messages. pmcp.auth.AuthMessage (32 members) holds every fixed message that auth.py and transport/http.py raise or send as an auth rejection. render_auth_message is the one runtime guard. It checks membership by identity, requires exactly the member's placeholder fields with valid shapes (absolute public http(s) URL; RFC 6749 scope list), and refuses anything else with TypeError. The auth error constructors, _auth_response/_reject (whose headers are now keyword-only and must be a mapping) and all 13 builtin ValueError/HTTPError sites go through it.

  • Four texts reworded, because pmcp's own sanitiser rewrote them (Token [REDACTED] not be verified…, Missing bearer [REDACTED], Unsupported token [REDACTED], shared-secret [REDACTED] mode…).

  • Startup refusal. check_auth_config checks values as they will be stored, using the renderer's own validators:

    • the CLI/env JWKS URL and required scopes, only with the HTTP transport in resource-server mode;
    • the stored URL in AsyncJWKS;
    • in create_http_app, the metadata URL as configured, and required scopes only in resource-server mode.

    A URL that sanitize_public_auth_url refuses keeps its specific registry message.

  • No empty resource. The metadata route refuses to start rather than publish an empty resource.

  • README. It documents the /mcp rule behind a prefix-stripping proxy, separately for CLI and embedding deployments.

  • Tests.

    • The existing tests that built auth errors from strings now use members. That covers test_auth.py, test_http_transport.py and test_scoped_advisor_audit.py.
    • The one test in test_transport_http.py that pinned the old silent metadata drop now expects the refusal.
    • New module tests/test_auth_operator_messages.py, with 244 tests.

Behaviour changes

  • In resource-server mode over HTTP, PMCP refuses to start on a JWKS URL, metadata URL or required scope that an auth message could not carry. Values are cleaned the way they are stored first, so a trailing newline or a leading space starts as before. A stray value in another mode is ignored, as on main.
  • On the programmatic path, create_http_app refuses a protected-resource metadata URL that normalisation used to drop silently, which omitted the route. That means a relative, non-http(s), plain-http non-loopback or non-public-IP URL. An https URL with a ?token= query still starts and serves its route.
  • Known follow-up: the metadata 404 behind a prefix-stripping proxy is auth: protected-resource metadata route 404s behind a prefix-stripping proxy or an ASGI root_path #334.

Guarantee vs guard rail

The runtime registry is the guarantee. The static check in the test module is a guard rail for seven listed shape families: unknown raises, non-member messages, arity, fields, named raises, pyjwt_text placement and payload mutation. It is not exhaustive against deliberate obfuscation.

Deviations from the plan

  1. The promise is qualified (round-9 claude nit). The claim "every operator-facing message in auth.py/transport/http.py" now reads "every message they raise or send as an auth rejection". Wherever it appears, the code docstrings (AuthMessage, the test module and _site_violations) and the CHANGELOG name what is out of scope, matching the plan's own non-goals: the metadata diagnostics lists, UNVERIFIED_URL_CAVEAT, fetch_json_metadata's strings, the plain 413/429/504 bodies and the log templates. The README makes no such claim.
  2. README additions beyond the plan's patch.
  3. CHANGELOG follows the plan's Documentation impact bullet, plus the qualified scope, both behaviour changes and the auth: protected-resource metadata route 404s behind a prefix-stripping proxy or an ASGI root_path #334 follow-up.

Everything else is the plan's three embedded patches and test module, applied exactly. Each was cmp-identical to the plan's extraction before items 1–3 were added.

Gates

Measured on this branch, on 516d689:

  • ruff check src tests: All checks passed!
  • ruff format --check src tests: 173 files already formatted
  • mypy src/: Success: no issues found in 52 source files
  • tests/test_auth_operator_messages.py: 244 passed
  • The plan's step-2 suites: 998 passed, 55 deselected
  • Extra touched suites (cli_p4, http_dos, manifest, tools): 316 passed, 1 skipped
  • scripts/check_security_claims.py: OK, 129 cited node id(s)
  • Full suite, run once, alone and detached with the npm vars unset: 5474 passed, 3 skipped, 80 deselected in 587.47s, exit 0. The first attempt hit the shared /tmp disk quota (57 OSError: [Errno 122] Disk quota exceeded, no assertion failures), so it was rerun with --basetemp and the log on the workspace volume
  • Mutation table on the real files (each mutant reverted from a saved copy, never a git checkout): 64 of 64 red. Every file was byte-identical afterwards.

Not merged; needs a cross-vendor panel CR before merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…derable auth config at startup; README /mcp rule (see #326)

Implements the merged plan .consiliency/plans/detailed-326-auth-messages-20261003-0226.md:
- AuthMessage registry (32 members) and render_auth_message, the one runtime
  guard (identity membership, exact placeholder fields with checked shapes),
  used by the auth error constructors, _auth_response/_reject (headers
  keyword-only, a mapping) and all 13 builtin ValueError/HTTPError sites.
- Four texts reworded that pmcp's own sanitiser rewrote.
- check_auth_config: refuse at startup, on values as stored, a JWKS URL,
  metadata URL or required scope a message could not carry (CLI/env only
  for HTTP + resource-server; create_http_app checks the metadata URL as
  configured and scopes only in resource-server mode).
- Metadata route refuses to start rather than publish an empty resource.
- README: the /mcp rule behind a prefix-stripping proxy; follow-up
  #334 for the metadata 404.
- Tests: new tests/test_auth_operator_messages.py; existing tests moved onto
  registry members; the pinned silent metadata drop now expects refusal.

The "every operator-facing message" promise is qualified against the plan's
non-goals (metadata diagnostics, UNVERIFIED_URL_CAVEAT, fetch_json_metadata
strings, plain 413/429/504 bodies, log templates).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
@ViperJuice
ViperJuice merged commit 160a9e8 into main Oct 3, 2026
14 checks passed
@ViperJuice
ViperJuice deleted the feat/326-auth-messages branch October 3, 2026 17:00
ViperJuice added a commit that referenced this pull request Oct 4, 2026
…velope checked before dispatch on every transport; repr rule takes every spelling; re-embedded on main c9206a9 (see #297)

Round-12 board on rev 12 (48b7a89): B1 (claude seat and codex) and B2 (codex).
B1: a rejected gateway.cancel id is null in the response and names no server
in the audit; every output or audit field that copies a caller input is
derived from the models and the code and triaged. B2: the JSON-RPC 2.0 / MCP
envelope gates dispatch on stdio, and a strict adapter gates the SDK client
transports; a malformed frame settles nothing. The claude notes: the repr
rule in every spelling, keyed by site; the record wording and scope.

Re-embedded on main c9206a9 (#337, #339,
#340). The merge with #337 exposed a dropped
task hint kept in raw and in invoke's size; both closed.

Code: wip/297-code @ ebcf4fc (local only). Repro 11/11 -> 0/11; full suite
6004 passed; mutation 85/86 and 81/86 (NO_STATIC; M25 equivalent; M19, M20,
M22, M35 static-only); embedding proof on c9206a9: 46 patches cmp-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxgMZ9qVCrCaV4aBRBAGNM
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.

1 participant