fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout#98381
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 2, 2026, 8:57 PM ET / 00:57 UTC. Summary PR surface: Source +14, Tests +35. Total +49 across 2 files. Reproducibility: yes. source-reproducible: current main parses successful mcporter stdout with a bare Review metrics: 1 noteworthy metric.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Keep this as a narrow memory-core error-normalization and redaction fix, while leaving the broader qmd first-search stdout-contamination issue tracked separately. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main parses successful mcporter stdout with a bare Is this the best way to solve the issue? Yes. The narrow parse-boundary guard plus generic cause is the maintainable fix here; filtering specific daemon text would miss variants, and preserving the raw SyntaxError cause would leak through AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9238d9aeae8d. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +14, Tests +35. Total +49 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
eabd28a to
b1c2a91
Compare
49de54e to
4d208be
Compare
4d208be to
88855a2
Compare
|
@clawsweeper re-review Updated Real behavior proof + added a cause-chain fix commit (ecfe4d3): formatErrorMessage walks the .cause chain, so the guard now gives .cause a generic message too. Non-mocked node proof uses the real formatErrorMessage to show no raw stdout leaks through the cause chain on the current head. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
runQmdSearchViaMcporter parsed mcporter subprocess stdout with JSON.parse outside the runMcporter try/catch (qmd-manager.ts:2722). A non-JSON stdout (daemon warning bleeding onto stdout, output truncated by maxOutputChars, CLI killed early, or flag mismatch) threw a raw SyntaxError that propagated uncaught out of runQmdSearchViaMcporter, surfacing in agent logs as a context-free SyntaxError with no hint of the actual mcporter failure. Wrap JSON.parse in try/catch and throw a typed Error carrying a stdout snippet (matching the guard pattern already used in parseListedCollections in this file, and the recent matrix openclaw#97973 / sms openclaw#97999 / signal openclaw#98073 / telegram-ingress openclaw#98372 JSON.parse guard series). Co-Authored-By: Claude <[email protected]>
Add { cause: err } to the re-thrown Error to satisfy the preserve-caught-error
lint rule; the original SyntaxError is now chained, improving diagnosability.
Co-Authored-By: Claude <[email protected]>
…-boundary) ClawSweeper flagged that the previous error message exposed raw mcporter stdout (first 200 chars) before session visibility filtering, which could leak sensitive content. Drop the stdout preview from the thrown message; keep the original SyntaxError as `cause` for diagnostics so the parse-failure reason is still reachable without surfacing unfiltered subprocess output. Co-Authored-By: Claude <[email protected]>
…out leak) The SyntaxError thrown by JSON.parse embeds a snippet of the raw input in its message (e.g. Unexpected token '<', then the raw bytes). Including that SyntaxError message in the thrown Error would surface unfiltered mcporter stdout before session visibility filtering. Drop the parse-error message from the thrown Error; keep the original SyntaxError as cause for developer diagnostics only. Co-Authored-By: Claude <[email protected]>
… via formatErrorMessage) formatErrorMessage walks the .cause chain into the user-visible path. Keeping the JSON.parse SyntaxError on .cause leaked its embedded raw stdout snippet through formatErrorMessage even with a generic message. Give the cause a generic message too; the raw snippet no longer reaches the user. Co-Authored-By: Claude <[email protected]>
ecfe4d3 to
6eb1289
Compare
|
No description provided. |
7d939b6
into
openclaw:main
* origin/main: (1287 commits) fix(android): block loopback canvas navigation (openclaw#99874) fix(hooks): suppress unhandled stdout/stderr stream errors in gmail watcher (openclaw#100519) fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout (openclaw#98381) fix(build): fall back to tsx for build TypeScript scripts (openclaw#91262) feat(skills): suggest saving detected reusable workflows by default (openclaw#95477) (openclaw#100692) docs(changelog): remove generated release-note entries feat(telegram): offer BotFather web app flow in setup help and docs (openclaw#100540) fix(ios): unify Talk and Settings row typography on one branded detail row (openclaw#100515) feat(gateway): add persisted crash-loop breaker and fatal-config exit contract refactor(macos): lock and unify PortGuardian tunnel record persistence so concurrent app instances cannot lose orphan records (openclaw#100601) fix: stop reconnecting on protocol mismatch (openclaw#98414) fix(maint): reuse recent hosted gates after rebase (openclaw#100663) fix(ui): reopen web terminals without stale content (openclaw#100665) fix(browser): diagnose empty WSL2 Chrome replies (openclaw#100590) fix(ios): chat snaps back to bottom when scrolling to top via status-bar tap (openclaw#100502) Treat already-compacted CLI compaction as no-op (openclaw#99136) docs(changelog): remove direct main fix entry fix(feishu): strip internal tool-trace banners from outbound text (openclaw#98705) fix(message): thread --limit through to CLI formatter and surface provider pagination hints (openclaw#99089) fix(voyage): close response body stream when batch output JSONL parsing throws (openclaw#98840) ... # Conflicts: # extensions/memory-wiki/package.json
…ut (openclaw#98381) * fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout runQmdSearchViaMcporter parsed mcporter subprocess stdout with JSON.parse outside the runMcporter try/catch (qmd-manager.ts:2722). A non-JSON stdout (daemon warning bleeding onto stdout, output truncated by maxOutputChars, CLI killed early, or flag mismatch) threw a raw SyntaxError that propagated uncaught out of runQmdSearchViaMcporter, surfacing in agent logs as a context-free SyntaxError with no hint of the actual mcporter failure. Wrap JSON.parse in try/catch and throw a typed Error carrying a stdout snippet (matching the guard pattern already used in parseListedCollections in this file, and the recent matrix openclaw#97973 / sms openclaw#97999 / signal openclaw#98073 / telegram-ingress openclaw#98372 JSON.parse guard series). Co-Authored-By: Claude <[email protected]> * fix(memory-core): preserve cause in qmd mcporter JSON.parse guard Add { cause: err } to the re-thrown Error to satisfy the preserve-caught-error lint rule; the original SyntaxError is now chained, improving diagnosability. Co-Authored-By: Claude <[email protected]> * fix(memory-core): redact raw stdout from qmd mcporter error (security-boundary) ClawSweeper flagged that the previous error message exposed raw mcporter stdout (first 200 chars) before session visibility filtering, which could leak sensitive content. Drop the stdout preview from the thrown message; keep the original SyntaxError as `cause` for diagnostics so the parse-failure reason is still reachable without surfacing unfiltered subprocess output. Co-Authored-By: Claude <[email protected]> * fix(memory-core): keep qmd mcporter error message generic (no raw stdout leak) The SyntaxError thrown by JSON.parse embeds a snippet of the raw input in its message (e.g. Unexpected token '<', then the raw bytes). Including that SyntaxError message in the thrown Error would surface unfiltered mcporter stdout before session visibility filtering. Drop the parse-error message from the thrown Error; keep the original SyntaxError as cause for developer diagnostics only. Co-Authored-By: Claude <[email protected]> * fix(memory-core): keep qmd mcporter cause generic (no raw stdout leak via formatErrorMessage) formatErrorMessage walks the .cause chain into the user-visible path. Keeping the JSON.parse SyntaxError on .cause leaked its embedded raw stdout snippet through formatErrorMessage even with a generic message. Give the cause a generic message too; the raw snippet no longer reaches the user. Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
…ut (openclaw#98381) * fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout runQmdSearchViaMcporter parsed mcporter subprocess stdout with JSON.parse outside the runMcporter try/catch (qmd-manager.ts:2722). A non-JSON stdout (daemon warning bleeding onto stdout, output truncated by maxOutputChars, CLI killed early, or flag mismatch) threw a raw SyntaxError that propagated uncaught out of runQmdSearchViaMcporter, surfacing in agent logs as a context-free SyntaxError with no hint of the actual mcporter failure. Wrap JSON.parse in try/catch and throw a typed Error carrying a stdout snippet (matching the guard pattern already used in parseListedCollections in this file, and the recent matrix openclaw#97973 / sms openclaw#97999 / signal openclaw#98073 / telegram-ingress openclaw#98372 JSON.parse guard series). Co-Authored-By: Claude <[email protected]> * fix(memory-core): preserve cause in qmd mcporter JSON.parse guard Add { cause: err } to the re-thrown Error to satisfy the preserve-caught-error lint rule; the original SyntaxError is now chained, improving diagnosability. Co-Authored-By: Claude <[email protected]> * fix(memory-core): redact raw stdout from qmd mcporter error (security-boundary) ClawSweeper flagged that the previous error message exposed raw mcporter stdout (first 200 chars) before session visibility filtering, which could leak sensitive content. Drop the stdout preview from the thrown message; keep the original SyntaxError as `cause` for diagnostics so the parse-failure reason is still reachable without surfacing unfiltered subprocess output. Co-Authored-By: Claude <[email protected]> * fix(memory-core): keep qmd mcporter error message generic (no raw stdout leak) The SyntaxError thrown by JSON.parse embeds a snippet of the raw input in its message (e.g. Unexpected token '<', then the raw bytes). Including that SyntaxError message in the thrown Error would surface unfiltered mcporter stdout before session visibility filtering. Drop the parse-error message from the thrown Error; keep the original SyntaxError as cause for developer diagnostics only. Co-Authored-By: Claude <[email protected]> * fix(memory-core): keep qmd mcporter cause generic (no raw stdout leak via formatErrorMessage) formatErrorMessage walks the .cause chain into the user-visible path. Keeping the JSON.parse SyntaxError on .cause leaked its embedded raw stdout snippet through formatErrorMessage even with a generic message. Give the cause a generic message too; the raw snippet no longer reaches the user. Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
What Problem This Solves
runQmdSearchViaMcporterinextensions/memory-core/src/memory/qmd-manager.tsparsed the mcporter subprocess stdout withJSON.parseoutside therunMcportertry/catch. When mcporter exits 0 but prints non-JSON to stdout (daemon warning, output truncated bymaxOutputChars, CLI killed early, flag mismatch), theSyntaxErrorpropagated uncaught. Wrapping it is not enough on its own:errors.ts formatErrorMessagewalks the.causechain into the user-visible path, so keeping the rawSyntaxErroronError.cause(whose message embeds a raw stdout snippet) still leaked that snippet throughformatErrorMessageeven when the thrown message was generic.Why This Change Was Made
Same JSON.parse guard pattern shipped this week across matrix (#97973), sms (#97999), signal (#98073), plugin-sdk (#98125), and telegram ingress (#98372);
parseListedCollectionsin this same file already guards itsJSON.parse.The thrown Error message and its
.causeare both deliberately generic.JSON.parse'sSyntaxErrormessage embeds a snippet of the raw input (e.g.Unexpected token '<', "<html>..."), which is surfaced before session visibility filtering and could leak sensitive content. Placing that SyntaxError on.causeis unsafe becauseformatErrorMessagetraverses.causeto build the user-visible message — so the cause now carries onlymcporter stdout was not valid JSON, and the raw parse detail is not placed on the cause chain at all.User Impact
A memory recall that hits a malformed mcporter response fails with a typed, diagnosable Error whose user-visible formatted message is generic — no raw subprocess stdout reaches the user through
formatErrorMessage. Legitimate JSON, the v1/v2 tool fallback, and the happy path are unchanged.Real behavior proof
Behavior addressed: A non-JSON mcporter stdout leaked raw input: first as an uncaught
SyntaxError, and (after the initial wrap) throughformatErrorMessagewalking.causeinto the SyntaxError's raw-input snippet.Real environment tested: Linux x86_64, Node v24.14.0, commit
ecfe4d38d1on branchfix/qmd-mcporter-json-parse-guard(current head, after the cause-chain fix).Exact steps or command run after this patch:
node --import tsx verify-qmd-cause.mjs— imports the realformatErrorMessagefromopenclaw/plugin-sdk/error-runtime, runs a realJSON.parseon a sensitive non-JSON stdout, then comparesformatErrorMessageover the current-head Error shape (generic cause) vs the pre-fix shape (SyntaxError on cause). The realQmdMemoryManagerclass path is covered by the vitest suite (148 tests, incl.wraps non-JSON mcporter stdout as a typed errordrivingcreateManager() -> manager.search() -> runQmdSearchViaMcporter -> guardwith a mocked non-JSON mcporter stdout).Evidence after fix: Terminal capture of
noderuntime verification (live stdout, copied verbatim):Observed result after fix:
formatErrorMessage(current-head error)returnsqmd mcporter returned non-JSON stdout | mcporter stdout was not valid JSON— no raw stdout, no<html>snippet, no sensitive bytes from the simulated stdout. The pre-fix shape under the sameformatErrorMessageincludes the SyntaxError's raw snippet (the leak this PR closes). The cause chain is still present for diagnostics, carrying only a generic message.What was not tested: Live end-to-end against a real mcporter subprocess emitting a daemon warning over an actual QMD collection (the mcporter CLI is not available in CI/local; the patched class path is exercised by the 148-test vitest suite through the real
QmdMemoryManager, and the formatErrorMessage cause-chain invariant is proven by the node runtime check above).Tests and validation
Regression case
wraps non-JSON mcporter stdout as a typed error instead of a raw SyntaxErrordrives the realQmdMemoryManagerviacreateManager() -> manager.search()with a mocked non-JSON mcporter stdout (0-exit) and asserts the rejection matches/non-JSON stdout/. The cause-chain redaction invariant (no raw stdout throughformatErrorMessage) is proven by the node runtime check above.Risk checklist
.causemessage embeds raw mcporter stdout or the SyntaxError raw-input snippet;formatErrorMessage(which walks.cause) can no longer surface it. Verified by the node runtime check across the cause chain.runQmdSearchViaMcporterkeeps its return type..causechain - resolved by giving both the message and the cause generic text (verified by formatErrorMessage over the current-head error shape)./non-JSON stdout/) + node runtime verification provingformatErrorMessagedoes not leak the raw snippet over the cause chain + tsgo green.Closes: N/A (no existing issue)