Repository navigation
docs(plan): auth operator messages that survive pmcp's own sanitiser, the README /mcp rule behind a prefix-stripping proxy, no empty resource (see Consiliency/pmcp#326) - #333
Merged
Conversation
… the README /mcp rule behind a prefix-stripping proxy, no empty resource (see #326) Detailed plan for #326, widened to the class: every fixed message in auth.py and transport/http.py, derived by an AST walk (33 texts plus 9 pyjwt texts), checked against the base redactor, the additive rules and the composed sanitiser. Measured on main 89559db: four texts are rewritten, not one (three predate #325); none reaches the HTTP wire. The metadata 404 behind a prefix-stripping proxy is measured (--oauth-audience fixes resource, not the route) and left to a separate issue with drafted text; README names the case. Empty resource fails closed at startup. Spiked: 80 tests, 9 red on main; 7/7 mutants red. This commit adds only the plan file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…ins (see #326) Round-1 panel (claude and codex): the derived message list failed open. It read one positional argument of five callees, skipped unknown shapes and hid a shrinking list behind a >= 28 floor. The walk now reads positional and keyword arguments, renders names, f-strings, % and .format, classifies every raise, requires every unrenderable message to be a reviewed exemption (multiset), and pins the derived texts (39) and log templates (16) exactly. Sinks settled by reading them: no log record passes through the sanitiser (no filter, plain or verbatim-JSON formatter, pmcp logs prints raw), so log templates are pinned but not reworded, with a tripwire test. Took claude's N1-N4 and the loopback nit: the startup text names the API parameters; the README separates CLI deployments (only --oauth-audience; the CLI serves no metadata route) from embedders; the tripwire also asserts the literal path serves 200. Re-measured: 98 tests, 9 red on main; touched suites 644 passed; full suite 5027 passed; 24/25 mutants red, K7 survives by construction with the K7+C9 double mutant measured. This commit changes only the plan file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…asses and refuses ambiguous names; behavioural log check (see #326) Round-2 panel (codex blocking, claude non-blocking, coordinator agreed with codex): the walk still failed open on `+=`, an exception built then raised by name, an alias raised by name, and a subclass carrying its message in super().__init__; the log tripwire missed a redacting text formatter. The walk now treats every call that resolves at test time to an exception class (aliases, attribute chains, module-defined subclasses) as a message call, follows a subclass's message into super().__init__, resolves `raise <name>` to its one constructing assignment or fails, and renders a name only when its sole binding in the innermost scope is one plain assignment (refusing +=, parameters, for/with targets, walrus, unpacking, global, reassignment). The log check runs the real setup_logging in text and JSON mode through every handler. Re-measured: 117 tests, 9 red on main; touched suites 663 passed; full suite 5046 passed; 34/36 mutants red, with K7 and K14 surviving by construction and their double mutants measured. This commit changes only the plan file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…pass; the static collector is removed (see #326) Round-3 panel (claude DISAGREE, codex 3 blocking): four more shapes beat the static message collector (subclass constant or self attribute, += on a parameter, comprehension walrus, alias resolved in the wrong scope). Fourth round, so round 4 changes the design instead of adding rules. Every fixed operator-facing text in auth.py and transport/http.py is now a member of pmcp.auth.AuthMessage (29; JWKS URL and scope names are filled from configuration by the constructor). ResourceServerAuthError, ResourceServerJWKSUnavailable and the new _auth_response refuse anything else with TypeError (always on), with one narrow pyjwt_text pass-through. A syntax-only check (no resolver) requires every raise and _reject site to name AuthMessage.<NAME> and forbids new exception classes. The resolver, scope walker, exemption multiset and pins are gone. Logs are checked end to end through the real logger, stderr and the log file (claude N3). The first full-suite run caught three existing test files that built the errors from strings (28 failures); they now pass members. Measured: 123-test module green (collection-red on main), touched suites 877 passed, 20/20 mutants red, full suite 5052 passed. This commit changes only the plan file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…placeholder fields, lexical re-raises (see #326) Round-4 panel: gemini clean; claude one blocker (B1) and N1/N2; codex two blockers overlapping them. The registry design holds; its enforcement had five holes, each closed with a regression test and mutants: - membership is identity (an id set of the registry), not isinstance; a PyJwtText can be minted only by pyjwt_text (private token, checked); - fields must be exactly the member's placeholders, so a missing field can no longer publish {url} and a misnamed one no longer raises KeyError; the static check requires field values to be runtime names; - AsyncJWKS.get and _fetch re-raise TypeError/KeyError ahead of their broad except, so a programming error is not a 503 and opens no backoff; - raise <name> is accepted only inside the handler that binds it, and every raise is counted (34); - pyjwt_text(<name>) is accepted only inside except _FIXED_TEXT_CLAIM_ERRORS as <name>; challenge tests build lazily, so that mutant fails tests instead of erroring the module. Scope names in MISSING_SCOPES (claude N3) are stated as a non-goal. Measured: 141-test module, touched suites 895 passed, 31/31 mutants red, full suite 5070 passed. This commit changes only the plan file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…ecked field values (see #326) Round-5 panel: gemini clean, claude nothing blocking (N1-N4), codex two blockers, both closed: - _auth_response checked type, not membership; it now renders through the one identity-checked guard, and a structural test forbids any other isinstance(..., AuthText) in src/pmcp. - A field value bound to prose passed. The renderer now checks each value's shape: {url} must be an absolute http(s) URL with no whitespace (reusing the absolute-URL rule sanitize_public_auth_url applies to configured auth URLs), {scopes} an RFC 6749 scope list; every placeholder has a validator, and an unknown one fails at import. The residual (prose made only of scope-token characters) is stated. Claude's notes: the static check compares each site's field names with its member's placeholders and a test drives the JWKS redirect path (N1); reload staleness documented (N3); named-only fields pinned and the renderer always formats (N4). Measured: 157-test module, touched suites 911 passed, 38/38 mutants red, full suite 5086 passed. This commit changes only the plan file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
… unrenderable auth config at startup (see #326) - Static check: each site kind has an exact message-position count and an allowed-keyword set (ValueError, HTTPError, both auth error classes, _reject/_auth_response); star-args refused. Runtime: _auth_response and _reject take headers keyword-only, _auth_response refuses non-mapping headers. - One shared validator, check_auth_config, built on the renderer's own _url_field/_scopes_field, refuses a JWKS URL, metadata URL or required scope at startup in AsyncJWKS.__init__, create_http_app and the CLI/env path (_check_auth_args, exit 1). - Re-measured: module 203 passed; touched suites 957 passed; full suite 5132 passed, 0 failed; 49/49 mutants red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
Brings the plan branch up to current main so the round-8 spike is measured on 199ee7c (see #326). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…k a listed-shape guard rail; startup checks on stored values where read (see #326) - Promise narrowed: acceptance criteria split into the runtime guarantee (identity membership, field shapes, constructor/_auth_response arity, startup refusal) and a static guard rail for seven listed shape families, stated as not exhaustive against deliberate obfuscation. - claude r7 B1: check_auth_config validates URLs as stored (sanitize_public_auth_url first); the CLI check runs only for HTTP + resource-server; create_http_app checks scopes only in that mode. Five must-start cases added. - codex r7 1: static check refuses payload mutation of an except-bound exception (8 shapes). - codex r7 2: metadata URL checked as configured, not as normalised; the behaviour change for create_http_app is stated, one pinned test migrated. - Re-measured on 199ee7c: module 222 passed; step-2 suites 976 passed; full suite 5452 passed, 0 failed; 58/58 mutants red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
…h the registry guard; sanitize's specific message kept (see #326) - claude B1 + codex (round 8): all 13 builtin ValueError/HTTPError sites now raise ValueError(render_auth_message(AuthMessage.<NAME>, **fields)); the static check requires that form with field names equal to the member's placeholders; a renderer spy drives every builtin site; codex's swapped member is a TypeError, never a published {url}. - claude N1: check_auth_config lets sanitize_public_auth_url's specific registry message through (IP-literal JWKS URL tested via the real CLI). - claude N2: the list of now-refused metadata URLs is corrected (a ?token= query alone is not one; an https case is pinned as starting), and the migrated test says why its URL is refused. - Re-measured on 199ee7c: module 244 passed; step-2 suites 998 passed; full suite 5474 passed, 0 failed; 64/64 mutants red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detailed implementation plan for #326. Plan only: this PR adds one file,
.consiliency/plans/detailed-326-auth-messages-20261003-0226.md. It ships no code.What the plan finds (measured on main
89559db)Four fixed texts are rewritten by the sanitiser, not one. The base redactor rules do it; the additive layer rewrites none of them. Three of the four predate PR auth: JWKS refresh cooldown, fetch timeout and shared backoff, operator-configured metadata resource, no-usable-keys 503 (see Consiliency/pmcp#231, Consiliency/pmcp#320) #325.
Token could not be verified with the published key.Token [REDACTED] not be verified…Missing bearer token.Missing bearer [REDACTED]Unsupported token algorithm.Unsupported token [REDACTED]shared-secret auth mode requires auth_token.shared-secret [REDACTED] mode requires auth_token.No description reaches the HTTP wire. 401, 403 and 503 responses send fixed bodies and a
WWW-Authenticateheader with onlyresourceorresource_metadata,errorandscope. The mangled text is the exception's stored text. So the rewording changes nothing a client sees.README
/mcp. Behind a reverse proxy that strips a path prefix, a prefixed metadata URL returns 404.--oauth-audiencefixes the publishedresourcebut not the 404. An ASGIroot_pathmount answers the metadata only at the doubled prefix. pmcp has no--root-pathflag.Empty
resource. It is unreachable throughnormalize_auth_metadata. When forced, main serves200 {"resource": ""}, which is invalid under RFC 9728.Decisions
WWW-Authenticateis checked one parameter at a time; the whole header is pinned as redacted by design.--oauth-audience).resourcefails closed at startup. The route is not mounted. The plan rejects omittingresource(RFC 9728 requires it), a per-request assert (a 500 on a public endpoint) and leaving it as is.Measured on the spike
check_security_claims.pyare clean.check_plan_consistency.pyreports 0 blocking issues.The patches and the test module are embedded verbatim. They were checked to reproduce the spike byte for byte from
89559db.🤖 Generated with Claude Code
https://claude.ai/code/session_01DsWXuxhZkEQrfn7TN9GCN2
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.