fix(mcp): close the nine defects the v0.1.125 retest found - #34
Merged
Conversation
A deep retest of v0.1.125 confirmed all four of its Windows fixes on real
hardware, then found nine more defects underneath them.
The severe one is the same leak from a different door. v0.1.125 taught
search_project's filesystem walk to consult PathDenylist::isSecretPath, and
that was the ONLY place it ran — so the leak survived in the plainest possible
form: stop searching and just ask. open_file opened ~/.ssh/id_rsa with
isError:false and read_tab returned the whole key body. find_in_tab returned
the matching key lines. And search_project's own open-tab leg read the same
file straight out of the buffer while its disk leg refused it. All four now
check the same list, and open_file refuses BEFORE the tab exists.
NP-05 credential deny-list guarded 1 read path of 4
NP-06 mock transport was indistinguishable from a real editor
NP-07 workspace_searched/scope sent by C++, dropped by the Rust struct
NP-08 zero matches reported as "No workspace folder is open"
NP-09 format_json repaired invalid JSON and reported success
NP-10 read_tab unbounded, truncated omitted when false
NP-11 find_in_tab rejected a title read_tab accepted
NP-12 invalid pagination cursor ignored
NP-13 tabs had no stable identity; writes could land elsewhere
format_json is the other one worth naming: the MCP path called the JSON
panel's auto-FIXER, so `[1,2` came back as `[1,2]` with isError:false. A
truncated config file becomes syntactically valid and semantically invented,
and an assistant that formats then writes it back has fabricated data the user
never had. rust-core gains a strict non-repairing json_parse_error; the panel
keeps its fixer, where a human sees the repair and can undo it.
Tabs now carry a stable id. tab_index is positional, so closing a tab shifts
every later index onto a different document — and out-of-range was the LUCKY
case, because a shifted-but-valid index writes silently with only the approval
card in the way. Every verb that names a tab accepts tab_id; tab_index still
works everywhere it did.
Verification: 76/76 C++ suites, 160 rust-core tests, sidecar green across BOTH
Cargo feature sets (the gap that hung four CI runners last release). Every new
test red-state verified by restoring the pre-fix code rather than negating a
condition.
Two implementation notes worth keeping:
- The mock marker is a separate content block, not a text prefix. Prefixing
was the obvious approach and it corrupted every JSON-returning tool on the
surface; the suite caught it immediately.
- Known gap: the strict-JSON validator is unit-tested, but its wiring into
MainWindow's formatText host lambda is verified by inspection — that
lambda is not reachable from the bridge's fake host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6BGY2dFUSujZeS3vTDikq
tests/pipe_bridge.rs is `#![cfg(windows)]` — the named-pipe twin of socket_bridge.rs, and the only place the shipped Windows transport's read worker is exercised at runtime. It never compiles on Linux, so `cargo test` here never reads it. The read_tab signature gained a max_bytes parameter in the previous commit. I updated the unix twin and left a one-argument call in the windows twin, which compiles nowhere I ran it and failed the Windows CI job at 8 minutes with E0061. Local green was not evidence about a file that was never opened. `cargo check --tests --target x86_64-pc-windows-gnu` compiles it in about a second and reproduces the exact rustc error, so release-check.sh now runs it. Verified the gate FIRES by restoring the one-argument call: it fails with the same E0061 CI reported. Soft-skips with an install hint when the target is absent — a release-machine gate, not a barrier for a contributor without it. Same shape as the feature-matrix gap that hung four runners last release: a suite that is green because a configuration was never built proves nothing about that configuration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6BGY2dFUSujZeS3vTDikq
…m rot
docs/mcp.html — the dedicated MCP page — still said "the current release is
v0.1.123" and "Everything on this page describes v0.1.123". It had sat three
releases stale through TWO releases that were entirely about MCP.
Every gate passed the whole time. The "new in vX" freshness check only matches
that literal phrase, and the count gates assert the tool NUMBER, which had not
changed. Nothing looked at a present-tense claim about which version the page
describes.
So this adds that check. The distinction it encodes: "since v0.1.118" is
history and stays true forever; "the current release is v0.1.118" is a claim
about NOW and rots the moment you tag. Only the second kind is gated. Verified
it FIRES by restoring the stale string — it names the file and line.
Content brought current:
- mcp.html: a "What changed in v0.1.126" section (the deny-list covering one
door of four, format_json inventing data, stable tab ids), read_tab and
find_in_tab tool rows updated for the new selectors and max_bytes, two new
limitations (credential files are refused and you cannot override it; the
Deny button is less exercised than the timeout), and a FAQ entry on what
the mock server now says about itself
- llms.txt: tab addressing, credential safety, the mock marker
- index.html: MCP badge was v0.1.123, FAQ download sizes said v0.1.124
- notepatra-mcp/README.md: said 48 tools; the server answers tools/list with
49, which is what every other surface already claimed. Derived the number
from the running binary rather than trusting either figure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6BGY2dFUSujZeS3vTDikq
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.
A deep retest of v0.1.125 confirmed all four of its Windows fixes on real hardware, then found nine more defects underneath them. All nine are fixed here. No new editor features — every change is a fix to the MCP surface.
The severe one: the leak came back through a different door
v0.1.125's fix was real, and it was the only place
PathDenylist::isSecretPathran —search_project's filesystem walk. So the leak survived in the plainest possible form. Stop searching, and just ask:Three more paths were open alongside it.
find_in_tabreturns the matching lines of a tab, which is a content channel with extra steps. Andsearch_project's open-tab leg read the file straight out of the buffer — the deny-list was guarding the disk while the identical bytes sat in a tab, unguarded.All four now check the same list.
open_filerefuses before the tab exists, because opening a key and then declining to read it would still put it on screen.The nine
isSecretPathran in 1 of 4 read paths--help, which a model never readsworkspace_searched/scopenever arrivedSearchResultsstruct declared 2 fields, serde dropped the resthits.isEmpty(), so match luck decided which fact you were toldformat_jsoninvented structure, reported successread_tabunboundedtruncatedomitted when falsefind_in_tabrejectedtitleformat_jsonis the other one worth naming. Over MCP there is no human in the repair loop, so[1,2came back as[1,2]withisError:false— a truncated config file made syntactically valid and semantically invented. An assistant that formats and writes it back has fabricated data the user never had.rust-coregains a strict non-repairingjson_parse_error; the panel keeps its fixer, where a human sees the repair and can undo it.Wire contract changes
list_open_tabspublishes a stableid; every verb that names a tab acceptstab_id.tab_indexkeeps working everywhere it did.read_tabtakesmax_bytesand always reportstruncatedandtotal_chars.find_in_tabacceptstitle.search_projectreturnsworkspace_searched,scope, andnotice. Zero matches is a success with an empty list.[MOCK DATA]content block and_meta.mock;initializereportsserverInfo.mock.Verification
76/76 C++ suites, 160 rust-core tests, sidecar green across both Cargo feature sets — the gap that hung four CI runners last release.
release-check.shclean apart from the working-tree check. Every new test red-state verified by restoring the pre-fix code rather than negating a condition; all nine went red on the intended assertion.Two notes worth keeping:
MainWindow'sformatTexthost lambda is verified by inspection — that lambda is not reachable from the bridge's fake host.Also still uncovered, carried forward from the retest: the approval card's explicit Deny button (deny-by-timeout is proven; deny-by-click is inferred), and the remote gateway, which stays behind a non-default Cargo feature.
🤖 Generated with Claude Code
https://claude.ai/code/session_01W6BGY2dFUSujZeS3vTDikq