fix(dispatch): require params at the MCP boundary (closes #210)#212
Conversation
#210 reported that `vault.update` with no `content` writes the literal 9-byte string `"undefined"` to the target file — `String(undefined)` coerced silently, the validator accepted the 9-byte string, and the formatter rendered the misleading heading `✓ Updated: undefined`. The same `String(params.x)` footgun existed at every vault-mutating dispatch site: `edit.append` (would append "undefined"), `edit.window` (would search for or replace with "undefined"), plus path-only cases that produced cryptic "File not found: undefined" errors instead of a clean dispatch-level rejection. Fix the class, not just the instance: - Add `requireParamStr(params, key, action, hint?)` in operations/shared. Throws at the boundary when a required string param is missing or wrong-typed, so a malformed MCP call cannot reach a vault sink. - Convert the corruption-class sites (`vault.update`, `vault.create`, `vault.delete`, `edit.append`, `edit.window`) and the noisy path-only sites (`edit.patch`, `edit.at_line`, `edit.from_buffer`, `view.file`, `view.window`, `view.open_in_obsidian`) to use the helper. - `vault.update`'s `content` guard carries a hint pointing at `vault.patch` / `edit.window` — that's the wrong-tool path that produced the #210 corruption in the wild. - Lift `edit.*`'s path guard above `FileLockManager.withLock` so a missing path can't take a lock on the literal string "undefined". - `updateFile` / `deleteFile` now return `{ success, path }` so `formatFileWrite` renders `Updated: <path>` — belt-and-suspenders for the second issue called out in #210. Tests: `tests/dispatch-param-guards.test.ts` records every mutation a mock API receives; for each malformed call we assert the route rejects AND no mutation was recorded, proving the guard runs before any sink.
|
✅ Build succeeded! Artifacts are available in the Actions tab. |
Review SummaryFocused review against the PR's six asks. One BLOCKING finding in the recovery hint; everything else is sound or NIT-tier. BLOCKING
"For partial replacement, use vault.patch with operation='replace', oldText, newText — or edit.window for fuzzy in-place edits.",There is no The signature is otherwise correct for Suggested fix — change the action name only: "For partial replacement, use edit.patch with operation='replace', oldText, newText — or edit.window for fuzzy in-place edits.",NON-BLOCKING1.
The assertion is Not blocking because the guard itself is correct (router.ts:145-146) and the corruption-class case for 2.
The path-hoist applies to all five edit actions ( 3. Comment indentation glitch
NIT
Sweep verification
Return-shape widening (
|
- vault.update content-guard hint pointed at vault.patch, which does not exist — patch lives at edit.patch. Bad recovery hint is worse than no hint. - edit.window guard test claimed "before searching the file" but only asserted on mutations; the mock's 'original' content doesn't contain 'undefined' so even a leaked guard wouldn't show a write. Add a reads recorder and assert getFile is never called, making the test honest about what it proves. - Strip a stray space from the lock-key guard comment.
|
✅ Build succeeded! Artifacts are available in the Actions tab. |
Summary
vault.updatewith missingcontentwas writing the literal 9-byte string"undefined"to the target file, with a nominal-success response (✓ Updated: undefined).String(params.x)footgun existed at every vault-mutating dispatch site.edit.appendwould have appended"undefined";edit.windowwould have searched for or replaced with"undefined". Path-onlyString(params.path)sites (view, edit.patch, edit.at_line, edit.from_buffer) produced cryptic "File not found: undefined" instead of a clean dispatch rejection.requireParamStr(params, key, action, hint?)inoperations/shared.ts— throws at the MCP boundary when a required string param is missing or wrong-typed, so a malformed call cannot reach a vault sink. Sweeps all affected dispatch sites invault.tsandrouter.ts.vault.update'scontentguard carries a hint pointing atvault.patch/edit.window— that's the wrong-tool path that produced the corruption in the wild.edit.*'s path guard aboveFileLockManager.withLockso a missing path can't take a lock on the literal string"undefined".updateFile/deleteFilenow return{ success, path }soformatFileWriterendersUpdated: <path>instead ofUpdated: undefined— belt-and-suspenders for the second issue called out in vault.update silently corrupts files to literal "undefined" when content parameter is missing #210.Files changed
src/semantic/operations/shared.ts—requireParamStrhelpersrc/semantic/operations/vault.ts— guards oncreate/update/deletesrc/semantic/router.ts— guards onedit.window,edit.append,edit.patch,edit.at_line,edit.from_buffer,view.file,view.window,view.open_in_obsidian; lock-key guard hoistedsrc/utils/obsidian-api.ts—updateFile/deleteFilereturnpathtests/dispatch-param-guards.test.ts— new regression suite (9 cases)Test plan
npm test— 305/305 across 24 suites, including the new 9npm run build— type-checks cleannpm run lint— no new warnings (5 pre-existing, unrelated to this change inmain.ts/image-handler.ts)#210reproduction shape ({action:"update", path, mode, search, replacement}with nocontent) now throws before anyvault.modifycall — verified by recording-API test that asserts zero mutations on the malformed call AND thatexisting.mdcontent is untouched.Closes #210.