fix: make the client-traffic trace answer the question it exists for (#120) - #122
Merged
Merged
Conversation
Three defects, all found by reading a customer's capture of a failing ChatGPT connection rather than by reading the code. The panel recorded its own polling. targets_mcp() was a bare strpos() prefix test and the admin API shares the saddle/v1 namespace, so /saddle/v1/mcp-diagnostics matched — and the panel polls it every five seconds while recording. At 25 entries the ring buffer therefore turned over every ~125 seconds. The capture that arrived spanned 114 seconds and held two real rows among twenty-three of the panel watching itself. The instrument was evicting the evidence it exists to collect. Matched exactly now, and the buffer raised to 100 for margin. The credential scheme was recorded and rendered nowhere. Every row has carried Saddle_Connection::credential_scheme() since the recorder shipped; neither report() nor the table printed it. So the one field that distinguishes "the key was rejected" from "no key arrived" — a 401 either way, and opposite fixes — was written to the option and shown to no one, and a customer had to ask us what his own trace already knew. Now first on the row, ahead of session and protocol, which are trivia by comparison. Paired with an explicit auth present/absent/unknown, because '' and 'unknown' are different answers and neither is "present". And the HTTP method is recorded, so a row carrying no MCP method is no longer ambiguous between a GET, a POST with the wrong content type, and an empty body. The credential-leak test was passing for the wrong reason: it set an Authorization header on a synthetic WP_REST_Request, which populates no $_SERVER, so the recorder never saw a credential to leak. The helper now sets both, which is what made the two new scheme assertions meaningful — and what proves the existing assertion was load-bearing rather than vacuous. Closes #120
7 tasks
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.
Closes #120
Found by reading a customer's capture of a failing ChatGPT connection, not by reading the code. Three defects, all of which shaped the last two weeks of that thread.
1. The panel recorded its own polling
targets_mcp()was a barestrpos()prefix test, andSaddle_REST_Admin::REST_NAMESPACEis alsosaddle/v1— so/saddle/v1/mcp-diagnosticsmatched, andMcpDiagnostics.jsxpolls it every 5 seconds while recording.At
MAX_ENTRIES = 25the ring buffer therefore turned over every ~125 seconds. The capture that arrived spanned 114 seconds and held two real rows among twenty-three of the panel watching itself. The instrument was evicting the evidence it exists to collect.Matched exactly now (
=== $mcp || strpos( $route, $mcp . '/' ) === 0), buffer raised to 100 for margin.2. The credential scheme was recorded and rendered nowhere
Every row has carried
Saddle_Connection::credential_scheme()since the recorder shipped. Neitherreport()'s format string nor the table printed it.So the one field that separates "the key was rejected" from "no key arrived" — the same 401, opposite fixes, one is reconnect the app and the other is your host is stripping the Authorization header — was written to the option and shown to nobody. The customer asked us for it by name, after the answer was already sitting in
saddle_mcp_traceon his own site.It is now first on the row, ahead of
sessionandprotocol, which are trivia next to it. Paired with an explicitauth: present|absent|unknown, because''and'unknown'are different answers and neither is "present".3. No HTTP method
A row reading
Asked for —was ambiguous between a GET, a POST with the wrong content type, and an empty body. Different answers, identical row.methodis recorded now.A test that was passing for the wrong reason
test_a_recorded_request_contains_no_credential()set anAuthorizationheader on a syntheticWP_REST_Request, which populates no$_SERVER— andSaddle_Connectionreads$_SERVERdeliberately, because the failure this surface exists to diagnose is a host stripping the header before PHP. So the recorder never saw a credential and the "no leak" assertion was vacuous.The helper now sets both. That is what makes the two new scheme assertions meaningful, and it retroactively proves the original assertion was load-bearing.
Testing
composer test— 599 tests (was 596), 2003 assertions, 1 pre-existing skip, greentargets_mcpregression passed against the bug, becauserest_do_request()never firesrest_post_dispatchand so nothing was written either way. Rewritten to drive the real chain, and it now fails correctly without the fix. Left a comment saying so.composer lint— 0 errors (pre-existing warnings only);npm run lint:jscleannpm run build— bundle rebuilt and committedlanguages/saddle.potregenerated