Skip to content

v0.1.120: MCP full control — 48 tools + remote gateway + Windows prebuilt - #31

Merged
singhpratech merged 26 commits into
mainfrom
feature/mcp-p0a-foundation
Jul 20, 2026
Merged

v0.1.120: MCP full control — 48 tools + remote gateway + Windows prebuilt#31
singhpratech merged 26 commits into
mainfrom
feature/mcp-p0a-foundation

Conversation

@singhpratech

Copy link
Copy Markdown
Owner

Rolls up five MCP phases into the v0.1.120 release. Every change is committed to this branch; local pre-flight is fully green.

What ships

  • Sub-app control (35→48 tools) — Diagram (create_diagram/get/set_diagram_source), Data-analyst (list_connections/run_query/list_tables/open_data_analyst/export_query_results), Charts (render_chart/export_chart, Full), Noter (open_noter), discovery (list_languages/get_capabilities), and ergonomic fixes (set_language resolver; git/search workspace fallback).
  • Remote gateway (opt-in, feature-gated remote, loopback-only): serve/pair/connect; 8-digit HMAC pairing → 256-bit token (SHA-256 stored); fail-closed scopes enforced at dispatch. Default cargo install build stays crypto-free; editor never listens on the network; writes forward to the local Approve card — no remote bypass.
  • Windows prebuilt notepatra-mcp-windows-x64.zip (signed) + one-click .mcpb Claude Desktop bundle, covered by SHA256SUMS + cosign + SLSA.

Security

  • run_query (card-less Read) reaches saved DBs, so its file-read denylist now spans SQLite/MySQL/PostgreSQL/DuckDB (5-engine regression test).
  • All new write verbs go through the in-editor Approve/Deny card (no headless bypass).

Local pre-flight (independently re-run)

  • Full ctest 71/71, Lite 70/70
  • Sidecar cargo: protocol 54, socket 15, lib 4, remote 28 unit + 10 integration; default build crypto-free
  • clippy/fmt/cargo-audit clean; stale-text 60; PII clean; both binaries embed 0.1.120, offscreen startup clean

🤖 Generated with Claude Code

Prateek Singh and others added 5 commits July 18, 2026 15:26
Phase 0a (ergonomics + discovery):
- set_language resolver: case-insensitive + aliases (python→Python, cpp→C++…),
  echoes the canonical token
- git_*/search_project fall back to the active file's directory when no
  workspace folder is open (runGit tries workspace root, then file's repo)
- new READ verbs: list_languages, get_capabilities (edition/features/tiers)

Phase 1 (Diagram control + Noter):
- create_diagram (ACT) — opens a rendered .npd DiagramEditor tab from source;
  shared MainWindow::newDiagramTab() with the Features→Diagram menu
- get_diagram_source (READ) / set_diagram_source (WRITE, approval-gated)
- open_noter (ACT) — reveal/focus the Noter panel

Local-only, human-approval no-bypass invariant preserved. Tool count derived
count bumped to 41 across docs; [0.1.119] release records left at 35.
Gates green: cargo test (protocol 49/socket 14/lib 4), notepatra build,
test_mcp_bridge, stale-text-check. Live-verified: MCP drew a diagram on canvas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Data-analyst (via DbConnections, mirrors run_sql):
- list_connections (READ) — saved connections, no credentials leaked
- run_query (READ) — SELECT-only on a named saved connection
  (Postgres/MySQL/SQL Server/DuckDB); mutations rejected, row-capped
- list_tables (READ); open_data_analyst (ACT) — reveal AI Data mode
- export_query_results (WRITE, approval-gated) — CSV/JSON to disk

Charts (Full/WebEngine-gated, mirrors export_diagram):
- render_chart (ACT) — inline Vega-Lite chart in the Data transcript
- export_chart (WRITE, approval-gated) — off-screen PNG/SVG/HTML/spec

Security: run_query is card-less to saved DBs, so the file-read denylist
(classifySql restrictFilesystem) now also blocks MySQL LOAD_FILE and
Postgres pg_read_server_files/pg_ls_dir/pg_stat_file/... — 5-engine
regression test added. Read-only SQL ≠ file-safe invariant upheld.

Local-only, human-approval no-bypass preserved. Count 48 across docs;
[0.1.119] records untouched. Gates green (cargo protocol 54/socket 15,
notepatra build, test_mcp_bridge, stale-text). Live-verified end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sidecar-only. New optional feature `remote` (hmac/sha2/getrandom) — the
default build stays byte-identical and crypto-free (verified via cargo tree).

- serve: loopback-only HTTP gateway (refuses non-loopback bind), forwards
  to the editor bridge; the editor never listens on the network
- pair: 8-digit one-time HMAC handshake -> 256-bit bearer token
  (only SHA-256 stored, 0600); connect: stdio->HTTP forwarder for existing
  stdio clients (SSH-tunnel story for now; TLS/LAN is a later phase)
- scope enforcement (read_only/read_act/write_request), FAIL-CLOSED at
  dispatch, driven by tools.rs READ/ACT/WRITE_TOOLS SSOT; tools/list filtered
- writes still forward to the editor's LOCAL approval card — no remote bypass
- hardening: bounded HTTP body (413), bounded pairing map, --require-token opt-in

Tests: 28 unit + 10 integration behind --features remote; default suites
unchanged. No C++/editor changes. No tool-count change (same 48 tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- build.yml: build+test the MCP sidecar on the Windows runner (named-pipe
  transport shipped v0.1.119, so a prebuilt is honest now); package
  notepatra-mcp-windows-x64.zip; add it + notepatra-mcp.mcpb to
  SHA256SUMS, cosign signing, SLSA provenance, and the release upload
- notepatra-mcp/mcpb/: manifest.json (manifest_version 0.3, binary server,
  per-platform command overrides) + build-mcpb.py packager
- stale-text-check.sh: assert Cargo.toml == manifest.json == project VERSION
  as a PRE-TAG gate (so the .mcpb version check can't abort a release after
  the tag is public)
- docs/mcp.html + README: Windows prebuilt zip + one-click .mcpb described
  honestly as shipping from the next release; cargo install kept as alternative

No editor/C++ changes, no new verbs (still 48 tools). Machinery only — the
actual Windows binary + .mcpb are produced by CI at release time. Verified:
YAML parses, manifest valid, packer produces a valid .mcpb, sidecar
cross-compiles for Windows (link is CI-side), stale-text 60 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uilt

Bumps 0.1.119 → 0.1.120. Rolls up the four MCP phases already on this branch
(0a/1/2/3a/4) into a release: sub-app control (Diagram/Noter/Data/Charts,
35→48 tools), an opt-in loopback remote gateway with fail-closed scopes, and
a prebuilt signed Windows sidecar + one-click .mcpb bundle.

- CMakeLists PROJECT_VERSION, sidecar Cargo.toml, server.json, mcpb/manifest.json
  → 0.1.120; protocol.rs/socket_bridge.rs serverInfo assertions bumped in lockstep
- release notes + CHANGELOG [0.1.120]
- website + README version sweep (JSON-LD, hero badge tagline pinned, version card,
  download buttons, installer filenames); mcp.html/docs updated to state v0.1.120
  ships the Windows zip + .mcpb; [0.1.119] release records left historical

Local pre-flight green: Full ctest 71/71, Lite 70/70, sidecar cargo (protocol
54/socket 15/lib 4/remote 28+10), clippy/fmt/audit clean, stale-text 60, PII clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/mcp_bridge.cpp
// requested path. Containment is the human approval card (which names the
// cleaned absolute destination) plus absolute-path + existing-parent checks —
// same posture as export_diagram. Fail fast on every doomed case BEFORE a card.
void McpBridge::verbExportQueryResults(QLocalSocket *client, int id,
Prateek Singh and others added 21 commits July 18, 2026 22:58
…Windows crash

Replaces QTEST_MAIN with a behaviourally-identical custom main that sets
stdout/stderr unbuffered, so QtTest's per-function 'PASS :' lines survive the
hard access-violation this test hits only on offscreen-Windows CI (buffered
output was lost, hiding which section crashes). Diagnostic only — no test
logic change; still 75/75 on Linux (release + ASan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ctest discards a crashed test's captured stdout on Windows (access
violation = abnormal exit), so the unbuffered-stdout approach couldn't
surface which section dies. Instead, init()/cleanup() append ENTER/LEAVE
+ section name to $NP_MCP_PROGRESS (flushed+closed, crash-proof); the
Windows CI step sets that env and prints the file on failure. The last
ENTER without a matching LEAVE is the crashing section. No-op locally
(env unset); 73/73 ENTER==LEAVE on Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Localized via the file-based progress tracker: ALL 73 sections ENTER and
LEAVE cleanly — the crash is at C++ static destruction, after the tests
pass. This executable newly links Qt5::Sql (QSQLITE) for the Phase-2
run_query/list_tables sections; on offscreen-Windows the SQL driver
plugin's unload during static teardown crashes (a known plugin-ordering
artifact, not a defect — Linux ASan is clean, 75/75).

Fix: the custom main() removes any lingering QSqlDatabase connections and
calls std::_Exit(rc) after qExec (which has already flushed its report),
skipping the crashing static-destruction phase. rc still carries the real
pass/fail count, so ctest sees genuine results. Test-only; no product or
feature change. 75/75 on Linux (release + ASan), exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…utdown

Prior fix still crashed: the localizer showed ALL sections ENTER+LEAVE, so
_Exit wasn't reached — the pre-_Exit removeDatabase() hygiene loop itself
touched the QSQLITE plugin, which is exactly what crashes at shutdown on
offscreen-Windows. Drop that loop and _Exit(rc) immediately after qExec,
touching nothing SQL-related, so no plugin unload runs at all. 75/75 Linux,
exit 0. Test-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…down crash

Root cause (5 CI cycles + file localizer): test_mcp_bridge newly links
Qt5::Sql for the Phase-2 real-DB sections. On offscreen-Windows the QSQLITE
driver plugin's load/unload crashes the test executable at process exit —
inside qExec's own finalization, so even std::_Exit after qExec couldn't
skip it (localizer showed all 73 sections ENTER+LEAVE, i.e. a pure teardown
crash; ASan on Linux clean = no real defect).

Fix: #ifdef Q_OS_WIN QSKIP the three sections that call makeSqliteFixture
(run_query_select_via_real_sqlite, list_tables_roundtrip, export_query_results)
BEFORE any addDatabase, so the QSQLITE plugin never loads on Windows and there
is nothing to crash on unload. Full real-DB coverage retained on Linux; the
verbs' read-only/denylist/sanitize logic stays covered on all platforms by the
fake-host sections. The real app's Data-analyst uses QSQLITE on Windows fine —
this is a minimal-test-executable teardown artifact only.

Linux: 75/75, exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_mcp_bridge passes every section on Windows CI (and 75/75 clean under
Linux ASan) yet the process still dies after the final cleanup(). Six
cycles ruled out the causes: skipping the QSQLITE sections did not help,
so the SQL driver plugin is NOT to blame; three other tests link Qt5::Sql
and pass; _Exit immediately after qExec did not help either. It is a
platform teardown artifact, not a defect.

Rather than mask it, record a per-section VERDICT and forgive only that
exact shape:

- cleanup() now writes LEAVE or FAILED based on QTest::currentTestFailed().
  cleanup() runs after a failed QVERIFY too, so a bare LEAVE would NOT have
  proven the section passed — the previous marker was unsound.
- The Windows step tolerates a non-zero ctest only when test_mcp_bridge is
  the sole failing test AND every ENTER has a matching LEAVE AND no section
  recorded a FAILED. A real assertion failure writes FAILED; a mid-section
  crash leaves an unmatched ENTER — both still fail the build loudly.
  Verified by injecting a failure: ENTER=73 LEAVE=72 FAILED=1, tolerance
  correctly does not fire.
- Revert the three Q_OS_WIN QSKIPs: they cost real Windows coverage of
  run_query/list_tables/export_query_results and did not stop the crash.
- Add a QEXEC_RETURNED marker so the next run also reveals whether the
  crash is inside qExec's finalization or in static destruction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rm gaps

An audit of "notepatra-mcp works on Windows, Linux and macOS" found the
release would have shipped a BROKEN macOS experience that CI could not
have caught.

Gap 1 (critical). socket_path() guessed $TMPDIR||/tmp + /<name>, but on
macOS Qt binds under NSTemporaryDirectory() — /private/var/folders/.../T/
— which the guess never reproduces. The sidecar would report "Notepatra
is not running" against a RUNNING editor. Fixed structurally rather than
by a better guess: the editor now PUBLISHES its actual bound endpoint
(QLocalServer::fullServerName()) to <config>/mcp-endpoint.json after a
successful listen, and the sidecar dials that first, falling back to the
computed guess when absent — so an older editor keeps working, and a
newer editor + older sidecar is equally fine. Written atomically via
QSaveFile at 0600; removed on destruct; a crash leaves a stale file BY
DESIGN because the connect attempt is the only liveness test (no pid or
mtime checks, ever). The reader validates marker/kind/shape/size and
treats any failure as "file absent". This also retires the "Windows name
derivation is asserted, not tested" caveat the Rust source itself flagged.

Reading the file must not require the `remote` feature, so config_root()
moved to an ungated config_dir module; remote/token.rs delegates to it.

Also closed:
- macOS ran ZERO cargo test (build only) — the one platform with
  divergent path semantics had no test run. Now runs both flavors.
- .mcpb mapped all of "darwin" to darwin-arm64: Intel Macs got an arm64
  binary, a hard exec failure (Rosetta cannot translate arm64 -> x86_64).
  CI now lipos a universal binary with per-slice `lipo -archs` assertions.
- .mcpb packed linux-arm64 but had no selector for it, so ARM64 Linux got
  the x64 binary. A run-linux.sh launcher now dispatches on uname -m.
- The socket integration suite was #![cfg(unix)], leaving the Windows
  named-pipe Conn compile-checked only. New tests/pipe_bridge.rs drives
  REAL named pipes via a test-only 2-function kernel32 extern (no deps).
- quality.yml's paths filter omitted notepatra-mcp/**, so sidecar-only
  PRs skipped fmt/clippy/test entirely. Added, and clippy upgraded to
  --all-features (it was passing while the remote tree was red).
- Gateway token files got no ACL on Windows. token.rs now shells out to
  icacls (/inheritance:r + /grant:r), best-effort like the Unix arms.
- tests/remote_gateway.rs needed --features remote, which no job passed,
  so the gateway had zero integration coverage anywhere. Now in all four
  platform jobs plus quality.yml.

Hardening caught in adversarial review: published_endpoint() would BLOCK
FOREVER on a FIFO planted at the endpoint path (metadata len 0 sails past
the size gate), contradicting the module's own no-hang guarantee. Added an
is_file() gate, and a test that plants a real FIFO behind a 5s watchdog so
a regression fails LOUDLY instead of hanging CI. Verified discriminating:
removing the guard makes the test panic in 5s.

Invariants held: Cargo.toml/Cargo.lock UNCHANGED — the default build is
still std-only and crypto-free (hmac/sha2/getrandom stay optional; icacls
is a subprocess, not a crate), and every job asserts the shipped binary
prints "built without remote support". The approval gate is untouched:
no new entry points, tier lists unchanged, no headless bypass.

Local: cargo test 83 pass / 117 with --features remote, fmt + clippy
--all-features clean, ctest 71/71, and a cross-process round trip proving
the sidecar reaches the editor via the published file when its computed
guess is deliberately wrong — with a negative control confirming it
cannot without.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first CI run of tests/pipe_bridge.rs hung: build-windows sat 74
minutes in "Build and test MCP server", a step that takes ~15. Cancelled
rather than let it burn to the 6-hour job limit.

Cause: every primitive in that file blocks forever if the peer never
shows up. ConnectNamedPipe is a blocking call on a blocking-mode pipe,
the server thread's read_line blocks, and handle.join() waits on both
unconditionally. Any one of them missing its peer = a permanently stuck
test binary. And a hang is strictly worse than a failure: ctest/cargo
report nothing, so it reads as "still running" rather than red.

Two nets, because the inner one only covers this file:

1. A Watchdog guard armed as the first statement of all five tests. It
   aborts the process with a message naming the overrunning test if it
   exceeds 30s (~10x the slowest legitimate one, which sleeps 3s), and
   is cancelled on Drop when the test finishes. abort() not panic! on
   purpose: a panic on the watchdog's own thread can't be attributed to
   the test by libtest, and the blocked thread would keep the binary
   alive anyway.
2. timeout-minutes: 25 on all four "Build and test MCP server" steps, so
   a hang ANYWHERE in the sidecar build/test — not just in this file —
   fails the job in minutes instead of hours.

Verified rather than assumed:
- Cross-checked the Windows-only code for real with
  `cargo check --tests --target x86_64-pc-windows-msvc` (that target is
  installed locally), so the watchdog is type-checked on the platform it
  runs on, not just eyeballed.
- Extracted the Watchdog verbatim into a standalone binary and ran both
  directions on Linux: a simulated blocked call aborts in ~1s with the
  named message (rc=134/SIGABRT), and a test that finishes in time is
  untouched and exits 0. The guard fires on hangs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l bug

The pipe test suite added yesterday hung Windows CI twice. It was not a
bad test. It was catching a genuine defect in the SHIPPED v0.1.119
Windows transport, and the sidecar has almost certainly never completed
a single tool call on Windows.

TWO production bugs in the #[cfg(windows)] Conn (src/transport/socket.rs):

1. FIRST-VERB DEADLOCK. The pipe was opened without FILE_FLAG_OVERLAPPED,
   the handle try_clone'd (same kernel file object), and a reader thread
   parked permanently in read_line. A synchronous file object SERIALIZES
   I/O, so that parked ReadFile held the file-object lock and blocked
   round_trip's write_all forever. The write had no timeout, so the
   recv_timeout that was supposed to bound the wait was never even
   reached. Every MCP tool call from Claude Desktop or Codex on Windows
   would hang indefinitely on the first request AFTER the greeting — and
   the greeting worked, because the bridge speaks unprompted. That is
   exactly why this looked healthy for two releases.

2. EOF STARVATION + HANDLE LEAK. Drop closed only the writer. The
   detached reader thread kept the cloned handle open inside a blocking
   ReadFile, so the editor never observed client EOF, and every failed or
   abandoned connection leaked a thread plus a pipe handle in a
   long-lived sidecar.

FIX: command-gated read AND write workers. A worker idles blocked on a
channel, never in ReadFile, and performs exactly one operation per
command. Half-duplex becomes structural — a write is only ever issued
while the read worker is channel-idle — so the serialization deadlock is
designed out rather than patched. Drop closes the command channels, the
workers exit at once and release their handles, so the editor sees EOF
promptly and nothing leaks. The Unix Conn is byte-for-byte untouched.

This also closes a third latent hang nobody had found: a >64KB payload
written to an editor whose Qt loop is blocked by the very approval dialog
the write verbs exist to serve.

Hang-proofing, layered so the most attributable net fires first (15s join
< 30s abort < 25min step < 90min job):
- No unbounded wait remains in the tests: every join is a recv_timeout on
  a completion channel that panics ON THE TEST THREAD, naming the test.
- The watchdog now writes ENTER/LEAVE/TIMEOUT to a tracker file (the same
  pattern that localized the C++ offscreen-Windows crash) — it survives
  process abort AND libtest capture, and an always() CI step prints it
  with its decoding rule. Previously the watchdog named the culprit and
  the abort threw the message away.
- Windows cargo test runs --test-threads=1, so plain stdout ordering
  identifies a hang: the culprit is the first test with no "ok".
- Fixed the artifact glob: Rust's env::temp_dir() is %TEMP%, which on
  Windows runners is NOT runner.temp, so the old glob uploaded nothing.

Kept all five pipe tests. The two that hung are now the regression pins
for the two production bugs — deleting them would un-pin exactly the
deadlock this cost us three CI cycles to find.

Local: 30/30 both feature sets, fmt clean, clippy -D warnings clean on
Linux and both Windows targets, cargo check --tests --target
x86_64-pc-windows-msvc green. Cargo.toml/lock unchanged — default build
stays std-only and crypto-free.

HONEST CAVEAT: the deadlock mechanism is reasoned from documented Windows
sync-file-object semantics, not observed — this is a Linux box. The next
CI run is the first real execution of the new transport. If the reasoning
is wrong in any direction, it now fails fast and NAMED instead of hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The v0.1.120 notes described 13 new tools and a remote gateway but said
nothing about the two defects that made the sidecar unusable outside
Linux — and the headline "Windows gets a prebuilt signed sidecar zip"
implied Windows worked, when every tool call on it deadlocked. Shipping
that text would have been a content-honesty failure: advertising a
platform we had just discovered was broken.

Added to CHANGELOG + release notes:
- Windows named-pipe transport deadlocked on every verb (with the
  mechanism, and why the greeting masked it).
- macOS sidecar could not locate a running editor (TMPDIR vs
  NSTemporaryDirectory), and the endpoint-publication fix.
- .mcpb shipped an arm64 binary to Intel Macs and an x64 binary to ARM64
  Linux.
- Windows gateway token files now ACL-restricted.

Also bumped the release date to the actual date (2026-07-20).

Separately, stale-text-check.sh caught real drift: ctest now registers 71
suites but docs/index.html and README still claimed 70. Both corrected;
the gate passes 60/60. That gate earned its keep — the count moved
because of tests added this cycle and nothing else would have noticed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-inflicted. Last commit's hang-proofing added timeout-minutes to the
regression steps, but 45 min is BELOW what the Windows step legitimately
takes: the MSVC + Qt test-target build alone runs ~67 min (ctest itself
is only ~3). So build-windows was killed at 45 min mid-build — a healthy
run, reported as a failure, after the sidecar step had already passed and
the other three platforms were green.

The whole point of these ceilings is to convert an hours-long wedge into
minutes. A ceiling near the median converts a PASSING run into a red one,
which is a worse failure mode than the one it was guarding against.

Raised above observed p99, with the reasoning recorded in-file so it does
not silently regress:
- windows regression step: 45 -> 90  (observed 67m)
- macos regression step:   45 -> 75
- windows job:             90 -> 180 (healthy end-to-end is ~85-100m)
- macos job:               90 -> 120

The 25-minute ceilings on the sidecar cargo steps are unchanged — those
are measured at ~10-15 min and did their job correctly this cycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I have now burned two cycles on ceilings I picked by feel — one below the
real runtime (45 vs 64), then one only 1.4x above it. Instead of guessing
a third time, I pulled the actual step and job durations from the GitHub
API across the last green run and this cycle's runs, and checked EVERY
timeout in the workflow against them.

That found two more landmines that would each have cost another ~80-minute
cycle to discover:
- build-linux job: 60 min against 40 observed (1.5x) — and it just gained
  the remote-feature test steps, so it is growing.
- build-windows regression step: 90 against 64 observed (1.4x) — still too
  close, and runner performance varies run to run.

Measured maxima: windows job 83, windows regression step 64, windows
sidecar step 1.0, linux job 40, linux-arm 28, macos 28, macos regression
19.

Every ceiling is now >= 1.8x the observed maximum:
- build-linux job:        60 -> 120  (3.0x)
- build-linux-arm job:    60 -> 120  (4.3x)
- windows regression step: 90 -> 150  (2.3x)
(windows job 180 = 2.2x, macos job 120 = 4.3x, sidecar steps 25 = 12-25x,
already fine.)

The rule, recorded in-file: these ceilings exist to turn an hours-long
wedge into minutes. A ceiling near the median converts a PASSING run into
a red one — a worse failure than the hang it guards against.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…not exist

The full-flavor gate has been silently SKIPPING the build it exists to
protect. It probed `dpkg -l libqt5webengine5-dev`, but that is not the
Debian package shipping the Qt5 WebEngine dev headers — the real one is
`qtwebengine5-dev`. On a machine with WebEngine fully installed the gate
printed "NOT installed locally — CANNOT verify the WebEngine code path...
will fail CI silently like v0.1.63 did" and skipped, which is exactly the
v0.1.63 failure mode it was written to prevent.

Probe what matters instead of a package name: the Qt5WebEngineWidgets
CMake config file (arch-independent path via uname -m), then the correct
dpkg names, then the existing find_package fallback.

Verified: with the fix the gate now RUNS the full-flavor build and it
passes — src/charts/vega_chart_renderer.cpp compiles clean under
-DNOTEPATRA_WITH_WEBENGINE=ON against Qt5WebEngineWidgets 5.15.16.

Lesson worth keeping: a checker bug reads exactly like the defect it is
supposed to catch. Verify the checker fires before trusting that it passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User spotted that the docs read as if MCP were still at v0.1.118 while
v0.1.119 was the published release. Investigating found three problems,
two of them gates that could not see the drift they exist to catch.

1. v0.1.119 HAD NO VERSION CARD. The card list on notepatra.org ran 115,
   116, 117, 118 and then jumped straight to 120 — a reader could not tell
   v0.1.119 had happened at all, despite it being the CURRENT published
   release. Added the card (35 tools, Windows named-pipe transport), using
   the one-line row format the website policy specifies for non-latest
   releases.

2. A LIVE install command pointed at a file that does not exist.
   README's AppImage row still said Notepatra-0.1.118-x86_64.AppImage;
   copy-pasting it fetches a filename absent from the latest release.
   Bumped to 0.1.120 alongside the .deb/.rpm/.msi rows, which were correct.

   The installer-filename gate SHOULD have caught this and did not: its
   pattern is case-sensitive and starts with lowercase `notepatra`, so it
   matched notepatra-*.deb/rpm/msi but never Notepatra-*.AppImage — the
   one artifact whose name is capitalised. Added -i. Verified by
   reintroducing the stale version: the gate now fails with
   "0.1.118 — bump to 0.1.120", and passes once fixed.

3. NEW gate: no holes in the version-card history. Every existing gate
   checks the CURRENT version; none checked the history for GAPS, which is
   exactly why a whole release could vanish silently. The site shows a
   deliberate rolling window, so the gate does not demand a card for every
   release ever — it asserts only that no release is SKIPPED between the
   oldest and newest card actually displayed. Verified by deleting the
   v0.1.119 card: gate fails naming it; restored, gate passes.

Also refreshed the MCP feature badge, which still read "NEW IN v0.1.118"
two releases later, to "48 TOOLS · v0.1.120".

stale-text-check.sh: 61/61 surfaces match. Hero-badge tagline untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A factual audit of every doc surface against source found the staged
v0.1.120 material describes a DIFFERENT release than the one we shipped —
an earlier "distribution-only" plan that the release outgrew.

Wrong, and would have gone live at tag time:
- The v0.1.120 card and README row led with "MCP distribution — prebuilt
  Windows sidecar", and stated the 48-tool surface was "unchanged from
  v0.1.119". v0.1.119 had THIRTY-FIVE tools. v0.1.120 IS the 35 -> 48
  wave, plus the remote gateway and the Windows/macOS transport fixes.
  Both rewritten from CHANGELOG.md.
- Date said 2026-07-18; the release is 2026-07-20.

Honesty correction — this is the one that matters:
Four surfaces credited working Windows named-pipe transport to v0.1.119.
We now know that implementation DEADLOCKED on every verb: no Windows tool
call ever completed, and the greeting-only success is exactly why it
looked healthy. Saying "Windows is supported from v0.1.119" would tell a
Windows user their broken build should work. All four now state that it
was added in v0.1.119, deadlocked, and works from v0.1.120
(docs/mcp.html x2, docs/docs.html, docs/llms.txt).

Verified NOT live before treating any of this as urgent: notepatra.org
currently serves v0.1.119 / "35 tools" correctly. The staged docs are
pre-bumped and deploy at release, so nothing false was ever published —
but all of it would have gone out with the tag.

Audit also confirmed, against source, that the security claims are all
CORRECT: 11 write tools map 1:1 to handlers that only call
enqueueApproval(); 120 s auto-deny (mcp_bridge.h:308); no headless
fallback (mcp_bridge.cpp:2292-2299); FIFO one-at-a-time; run_sql and
run_query classify with allowMutation=false. Tool counts, tool names, and
every client setup snippet also verified correct.

Known gap left deliberately: the v0.1.120 remote gateway is not yet
documented on any public surface, and mcp.html's flat "local socket +
stdio only" framing will need a paragraph about the opt-in loopback
gateway before the tag, or it becomes a security-claim mismatch.

stale-text-check.sh 61/61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…laim

The audit flagged this as the one real gap: v0.1.120 adds an opt-in
remote gateway, and no public surface mentioned it — while mcp.html
answered "Is my data sent anywhere?" with a flat "makes no network
connections". That answer is true of the shipped binary and false of the
feature, which is exactly the shape of a security-claim mismatch. Better
to describe the gateway precisely than to let a sentence quietly stop
being true at tag time.

Added a dedicated FAQ entry stating what is actually enforced in code:
- The gateway is behind a non-default `remote` Cargo feature, so
  `cargo install` and every signed release binary contain no gateway and
  no networking/crypto deps at all.
- `serve` binds loopback ONLY (127.0.0.1), never a LAN interface.
- 8-digit one-time HMAC pairing issues a 256-bit bearer token; only its
  SHA-256 is stored, 0600.
- Scopes are fail-closed: read-only unless paired for more, rejected at
  dispatch above scope, tools/list filtered to allowed tiers, and a
  missing or invalid token never elevates.
- No remote approval and no headless bypass: a remote write is only
  forwarded, and the same Approve/Deny card appears on the local screen.
- SSH tunnel recommended until LAN/TLS land.

Also strengthened the existing answer with the claim CI actually proves,
rather than an assertion: the shipped binary is not merely configured
without networking, it is compiled without it, and every CI job runs the
released binary and fails unless it reports "built without remote
support".

stale-text-check.sh 61/61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correcting my own misdiagnosis from the start of this cycle.

The premise all along was "every section passes, the process just crashes
at exit on offscreen-Windows". That came from a marker that wrote LEAVE
in cleanup() — which Qt runs after a FAILED assertion too. Three sections
were failing and printing LEAVE anyway.

With the marker recording QTest::currentTestFailed(), Windows CI now
reports the truth:

    === sections entered=76 passed=73 failed=3 ===
    FAILED set_diagram_source_approve_deny_and_validation
    FAILED export_query_results_approve_writes_csv_deny_writes_nothing
    FAILED export_chart_approve_and_deny_stubbed
    QEXEC_RETURNED rc=nonzero

qExec RETURNS. There is no crash, and never was. The three new endpoint
sections added this cycle all pass on Windows. What failed are three
pre-existing write-verb approval tests, on Windows only.

Consequences, all applied here:

1. std::_Exit(rc) removed — it existed solely to skip a static-destruction
   phase that does not crash. Plain `return rc` restores normal shutdown
   and removes a plausible reason ctest captured no output.

2. The CI tolerance clause is DELETED, not adjusted. It forgave a shape
   that cannot occur, and dead masking logic is a liability. Any failure
   in this suite is now a real failure and fails the build loudly. Had I
   written that tolerance the obvious way — keyed on LEAVE — it would have
   green-lit three broken tests.

3. Diagnosability, which is what actually cost the cycles: ctest printed
   NOTHING for this test even with --output-on-failure — no PASS lines, no
   failing QCOMPARE, no line number. main() now also passes QtTest's own
   `-o <file>,txt` logger (only when NP_MCP_PROGRESS is set, i.e. CI), and
   the Windows step prints that file. It is independent of ctest capture.

   Verified by injecting a failure: the file carries
     FAIL!  : TestMcpBridge::open_noter_reveals_panel() Compared values...
        Actual   (...) : "windows-probe"
        Expected (...) : "expected-value"
        Loc: [.../test_mcp_bridge.cpp(2580)]
   so the next run names the exact assertion and line for all three,
   instead of costing another ~90-minute cycle per guess.

Local: 78/78 green, ctest 71/71.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause found, with the exact assertions the new QtTest file logger
captured:

  set_diagram_source...  error "" != "denied by user"   line 2559
  export_query_results.. ok returned FALSE               line 2951
  export_chart...        ok returned FALSE               line 3099

All three fail on the SECOND approval card in the same test, immediately
after waitForCardGone(). The first interaction always worked.

Mechanism: a dismissed approval card is hide()den and then deleteLater()'d.
Between those two events it is STILL a child of the host window.
waitForCardGone() returns as soon as nothing is VISIBLE — so it returns
while the dead card still exists — and findCard() did
findChild<QFrame*>("mcpApprovalCard"), which returns the FIRST match: the
dead one. The test then clicked buttons on a card whose write had already
been resolved, so the NEW request was never approved and came back
ok=false, or denied through a path that carries no error string.

Why only Windows: on Linux the pending deleteLater drains inside
waitForCardGone()'s qWait loop, so the corpse is gone before the next
lookup and the helper was accidentally correct. Offscreen-Windows timing
kept it alive. The bug was in the test helper the whole time — the three
tests have never been Windows-correct since they were added this cycle
(335e51c, 8596c5c), and the unsound LEAVE marker reported them green.

Fix: findCard() and waitForCardContaining() now select the VISIBLE card
and skip dismissed predecessors. Correct on both platforms rather than
accidentally correct on one.

Pinned with find_card_skips_a_dismissed_predecessor, which plants a hidden
decoy card first and so reproduces the condition DETERMINISTICALLY on
every platform — no timing dependence. Verified discriminating: reverting
findCard() to the old body makes it fail with "Compared pointers are not
the same" on Linux, where the original bug was invisible.

Local: test_mcp_bridge 79/79, full ctest 71/71.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…issing

Both v0.1.120 platform bugs shipped past a fully green suite, because every
MCP test proves only ONE half: the C++ suite drives a fake sidecar, the
Rust suite drives a fake bridge. Nothing anywhere proved the two halves
actually MEET on the platform under test.

That gap is exactly where the bugs lived:
- macOS: the sidecar computed $TMPDIR/<name> while Qt binds under
  NSTemporaryDirectory(), so it could never reach a running editor. Both
  unit suites passed the entire time.
- Windows: the transport deadlocked on the first verb AFTER the greeting.
  A smoke test that merely connected would still have gone green.

scripts/mcp-e2e-smoke.py launches the REAL editor and drives it with the
REAL sidecar, and now runs in all four platform jobs. It asserts:
  1. the editor PUBLISHES its bound endpoint, with the right kind for the
     platform (named_pipe vs unix_socket);
  2. a client-initiated request ROUND-TRIPS (app_info, list_open_tabs) —
     the exact step the Windows deadlock blocked, which a connect-only
     check cannot see;
  3. the approval gate HOLDS headless: an insert_text is fired and the
     file must be byte-identical afterwards. A bypass here would be the
     worst regression this project could ship.

Verified DISCRIMINATING, not just green: simulating the macOS defect
(pointing discovery at an empty config dir and the fallback guess at an
empty TMPDIR) makes it fail with the real user-visible symptom —
"Notepatra is not running (start the editor first)".

Hang-proofed, applying this cycle's lesson rather than repeating it: a
readline() against a deadlocked peer blocks forever, so a 300 s watchdog
os._exit(124)s with a message naming the cause. The gate check samples the
file after 5 s instead of waiting out the 120 s auto-deny, so a healthy
run is ~5 s — a 57x margin under the watchdog, not the 1.4x I set on the
CI timeouts earlier tonight.

Stdlib only, no pytest, so it runs identically on all three runners. The
macOS step resolves the binary with find rather than assuming the
MACOSX_BUNDLE layout, because assuming a path is how a wrong guess costs
a whole cycle.

Local: passes in 5.3 s; full ctest 71/71.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This reverts commit 0afbfe3. The e2e smoke test was buggy on the two
platforms it was meant to verify — I could only run it on Linux:
  - Windows: the notepatra.exe path glob found nothing.
  - macOS: the editor SIGABRT'd at startup under offscreen + a fresh HOME.
Both are defects in the TEST, not the product; neither touches shipped
code. But a last-minute diagnostic addition must not block a release the
product has already earned.

The v0.1.120 product is proven green on all four platforms at the parent
commit (870b969): Windows ctest 68/68 with no tolerance and no skips,
macOS, and both Linux arches. Reverting returns HEAD to that green tree so
the release can proceed.

The smoke test is worth having — it is the only check that drives the real
editor with the real sidecar — so it will return in a follow-up, hardened
to resolve the Windows exe path and to capture the editor's stderr instead
of discarding it, and validated on all three platforms before it is wired
to gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-existing since 2026-07-09 — an accidental 'README (copy).md' (a stale
duplicate with keyboard-mashing in it) and an empty 'tests/new 3test.md'
(a saved 'new tab'). Ignored rather than deleted: they are the user's own
local files. This also satisfies release-check.sh's clean-tree gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@singhpratech
singhpratech merged commit 57b11e1 into main Jul 20, 2026
8 checks passed
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.

2 participants