Skip to content

fix(mcp): close the nine defects the v0.1.125 retest found - #34

Merged
singhpratech merged 3 commits into
mainfrom
fix/mcp-retest-nine-defects
Aug 7, 2026
Merged

fix(mcp): close the nine defects the v0.1.125 retest found#34
singhpratech merged 3 commits into
mainfrom
fix/mcp-retest-nine-defects

Conversation

@singhpratech

Copy link
Copy Markdown
Owner

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::isSecretPath ran — search_project's filesystem walk. So the leak survived in the plainest possible form. Stop searching, and just ask:

→ open_file  {"path": ".../.ssh/id_rsa"}
← "Opened .../.ssh/id_rsa as tab 8"          isError: false
→ read_tab   {"title": "id_rsa"}
← "-----BEGIN OPENSSH PRIVATE KEY-----\n<full key body>\n-----END..."

Three more paths were open alongside it. find_in_tab returns the matching lines of a tab, which is a content channel with extra steps. And search_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_file refuses before the tab exists, because opening a key and then declining to read it would still put it on screen.

The nine

Defect Root cause
NP-05 Private keys returned in full isSecretPath ran in 1 of 4 read paths
NP-06 Mock data indistinguishable from real Nothing on the wire said so; only --help, which a model never reads
NP-07 workspace_searched/scope never arrived C++ sent them; the Rust SearchResults struct declared 2 fields, serde dropped the rest
NP-08 "No workspace open" for a query that missed The error was gated on hits.isEmpty(), so match luck decided which fact you were told
NP-09 format_json invented structure, reported success It called the JSON panel's auto-fixer
NP-10 read_tab unbounded 5 MB cap was the only bound; truncated omitted when false
NP-11 find_in_tab rejected title Two hand-written resolvers that had drifted
NP-12 Bogus pagination cursor ignored No validation at all
NP-13 No stable tab identity Every verb addressed tabs positionally

format_json is the other one worth naming. Over MCP there is no human in the repair loop, so [1,2 came back as [1,2] with isError: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-core gains a strict non-repairing json_parse_error; the panel keeps its fixer, where a human sees the repair and can undo it.

Wire contract changes

  • list_open_tabs publishes a stable id; every verb that names a tab accepts tab_id. tab_index keeps working everywhere it did.
  • read_tab takes max_bytes and always reports truncated and total_chars.
  • find_in_tab accepts title.
  • search_project returns workspace_searched, scope, and notice. Zero matches is a success with an empty list.
  • Mock results carry a [MOCK DATA] content block and _meta.mock; initialize reports serverInfo.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.sh clean 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:

  • The mock marker is a separate content block, not a text prefix. Prefixing was the obvious implementation 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.

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

Prateek Singh and others added 3 commits August 7, 2026 14:12
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
@singhpratech
singhpratech merged commit 1600fa8 into main Aug 7, 2026
7 checks passed
@singhpratech
singhpratech deleted the fix/mcp-retest-nine-defects branch August 7, 2026 20:27
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