@oomkapwn/enquire-mcp 3.5.5 → 3.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [3.5.6] — 2026-05-10
6
+
7
+ **Patch — root-cause fixes for two issue classes surfaced by external reviews.** Closes the systemic gaps, not just the individual symptoms. No behavior changes.
8
+
9
+ ### Root-cause #1 — cold-import test flakes (class fix)
10
+
11
+ v3.5.5 fixed ONE symptom (`tests/doctor.test.ts` timeout) with a per-test 30s timeout. Audit of the wider codebase found the same pattern in **3 more test files**:
12
+
13
+ - `pdf.test.ts` (~20 tests) — `extractPdfText()` triggers `pdfjs-dist` cold load
14
+ - `ocr.test.ts` (`isOcrAvailable` + `extractPdfWithOcr`) — loads tesseract.js + canvas + pdfjs (combined ~30 MB WASM/native)
15
+ - `hnsw.test.ts` (24 tests) — `buildHnsw()` loads hnswlib-node native binding
16
+
17
+ Per-test timeout bumps don't address the class. The systemic fix: **`tests/setup.ts`** that warms every native / heavy optional dep ONCE per Vitest process via `setupFiles` in `vitest.config.ts`. The first test process startup pays the cumulative cold-import cost (~10 s in our environment); every subsequent test in every file sees a fully cached module.
18
+
19
+ Cost: +10 s to the first process; saved ad-hoc timeout bumps for every future test that touches optional deps. Net win.
20
+
21
+ ### Root-cause #2 — security-doc drift (selective fix)
22
+
23
+ v3.5.5 fixed ONE symptom (SECURITY.md stale on stateful HTTP from v2.14.0). Audit of doc-vs-feature drift found that v3.2.0+ tools (`obsidian_list_bases` / `read_base` / `query_base` / `get_communities` / `hyde_search`) introduced **new attack surfaces** that SECURITY.md didn't cover:
24
+
25
+ - `.base` file parsing — malformed YAML, DSL predicate ReDoS risk, path traversal via base file path, filter-against-private-paths concern
26
+ - GraphRAG-light community detection — vault-wide read amplification, memory bounds on dense vaults, Louvain compute cap (50 passes), no LLM call surface
27
+
28
+ Added two new SECURITY.md sections covering these. Out-of-scope items called out explicitly (formula evaluation deferred; adversarial graph construction = user-owns-vault non-threat).
29
+
30
+ ### Known remaining gap (tracking for v3.6+)
31
+
32
+ `docs/api.md` is missing entries for 12 tools (the 5 v3.x ones plus 7 that pre-date v3 — `chat_thread_read/append`, `context_pack`, `frontmatter_get/search/set`, `list_pdfs`, `read_pdf`). This drift pre-dates v3.5 and would need a substantial backfill + a new docs-consistency invariant to prevent recurrence. Tracked for the v3.6 docs-completion sprint, not blocking this release. The existing `tests/docs-consistency.test.ts` invariant catches drift in **README** only (where every tool IS mentioned).
33
+
34
+ ### Tests
35
+
36
+ 664 unit tests pass (unchanged). Setup time +10 s once per process, individual tests faster (no cold-load cost).
37
+
38
+ ### Migration
39
+
40
+ **No-op for default users.** Pure test stability + docs additions.
41
+
5
42
  ## [3.5.5] — 2026-05-10
6
43
 
7
44
  **Patch — fixes from external review #2.** Two issues: test flakiness on cold I/O + a documentation drift between SECURITY.md and the v2.14.0 stateful-HTTP code path. No behavior changes.
package/SECURITY.md CHANGED
@@ -214,3 +214,35 @@ Out of scope (stateful mode specifically):
214
214
  - Transport errors written to stderr with no token / no credential leakage.
215
215
  - `/health` endpoint (`GET /health → 200 ok`) is **unauthenticated** and exists specifically for tunnel/uptime monitors. It returns the literal string `ok` — no version info, no vault path, no operational metadata. Health probes can't be used to fingerprint the deployment.
216
216
  - `OPTIONS` preflight requests are unauthenticated (per CORS spec) but only emit CORS headers when the request's `Origin` is in the allowlist.
217
+
218
+ ## Obsidian Bases (`.base`) execution (v3.2.0+): parser + DSL posture
219
+
220
+ `obsidian_list_bases` / `obsidian_read_base` / `obsidian_query_base` parse user-authored YAML files and evaluate a filter-DSL subset against the vault's markdown notes. New attack surfaces vs the markdown-only v1.x baseline:
221
+
222
+ **Threat model:**
223
+ - **Malformed YAML / YAML bombs.** Parsed via `js-yaml`'s `SAFE_SCHEMA` (the same engine and schema `gray-matter` uses for frontmatter). No anchor-expansion, no `!!js/function` tag, no code execution path. A YAML bomb (deeply nested anchors) is rejected at parse time before our zod schema validation runs.
224
+ - **ReDoS in DSL predicate regexes.** Each predicate is matched against a small set of fixed, non-backtracking regexes (`^tag\s*(==|!=)\s*..." literal "$"` style). No user-controlled regex compilation. Predicate strings that don't match any pattern fall into `unevaluated_predicates` and are treated as `true` (permissive) — they don't cause regex evaluation against user content.
225
+ - **Path traversal via `.base` file path.** `obsidian_read_base({ path })` and `obsidian_query_base({ path })` resolve through `vault.readBinaryFile` → `vault.resolveSafePath` — the same realpath + `--exclude-glob` + `--read-paths` chain as `readNote`. Symlinks-out-of-vault rejected; excluded paths refuse to load.
226
+ - **Filter against private paths.** `queryBase`'s vault walk goes through `vault.listFilesByExtension(".md", folder)`, which respects `--exclude-glob` / `--read-paths`. A `.base` filter cannot surface content that the privacy filter would block from `readNote`.
227
+ - **Outbound wikilink-set materialization.** v3.5.0 added `linksTo()` predicate evaluation; the per-note outbound set is computed from `extractWikilinks(body)` — same parser as the read-only `obsidian_get_outbound_links` tool. No new file reads or path resolution beyond what's already exposed.
228
+
229
+ **Out of scope (deferred):**
230
+ - **Formula evaluation** (`formulas:` section). Our DSL is filters-only; formulas are surfaced as metadata via `obsidian_read_base` but never evaluated. Until a formula evaluator ships (separate sprint), there is no code execution path through `.base` formulas — they're inert strings.
231
+ - **Summaries / aggregations.** Same — surfaced as metadata, not evaluated. No SQL-injection-class concern since there's no executable backend.
232
+ - **Date arithmetic** (`inDate` etc). Falls into `unevaluated_predicates`, permissive. No date-parser surface yet.
233
+
234
+ When formula evaluation lands, this section gets an "Expression engine sandbox" subsection covering the threat model for that.
235
+
236
+ ## GraphRAG-light: wikilink community detection (v3.4.0+): graph-build posture
237
+
238
+ `obsidian_get_communities` builds an in-memory undirected graph from every resolved `[[wikilink]]` in the vault, then runs single-phase Louvain modularity optimization. New attack surfaces:
239
+
240
+ **Threat model:**
241
+ - **Vault-wide read amplification.** Where a single `obsidian_read_note` call reads one file, `obsidian_get_communities` reads ALL markdown files in the vault (or under `--folder` if specified) to extract their wikilinks. Privacy filter is honored: excluded/disallowed paths are never in `listFilesByExtension(".md")`'s output, so they don't contribute nodes or edges to the graph. The graph is also bounded by the vault: nodes are vault-relative paths only, no off-vault leakage.
242
+ - **Memory bounds on huge vaults.** The adjacency map is `Map<path, Map<path, weight>>` — O(|V| + |E|). For a vault with 50K notes and average degree 10, that's ~250 KB of node strings plus ~3 MB of edge weights — comfortably bounded. Pathologically dense vaults (every note links every other note) hit O(|V|²) memory; this is acceptable since the dense case is implausible and the user controls the vault.
243
+ - **Modularity-optimization compute bounds.** Louvain is capped at 50 passes per `detectCommunities` call; each pass is O(|E|). On a 50K-node vault with 500K edges this is ~25M ops × 50 = ~1.3B ops, ~5-10 s wall time. The tool is read-only and per-call (we don't cache), so cost is paid only when the agent explicitly invokes the tool. No persistent background work.
244
+ - **No LLM call surface.** The server stays LLM-free for this tool — the agent is expected to summarize communities itself with the member list we return. There is no code path where `obsidian_get_communities` makes outbound HTTP or invokes an embedding model.
245
+
246
+ **Out of scope:**
247
+ - **Multi-phase Louvain refinement.** Single-phase is "good enough up to ~50K notes" by design; the trade-off is documented in `src/communities.ts`. Vault > 50K notes may see lower modularity quality, but never an unbounded compute spike (the 50-pass cap holds).
248
+ - **Adversarial graph construction.** A vault author could construct a graph designed to be slow to partition (e.g. specific dense bipartite structures). Acceptable — the user owns the vault; there is no "attacker writes a vault" threat model.
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { chunkContent, defaultIndexFile, FtsIndex } from "./fts5.js";
12
12
  import { appendToNote, archiveNote, chatThreadAppend, chatThreadRead, contextPack, createNote, dataviewQuery, embeddingsSearch, findPath, findSimilar, frontmatterGet, frontmatterSearch, frontmatterSet, getBacklinks, getNoteNeighbors, getOpenQuestions, getOutboundLinks, getRecentEdits, getUnresolvedWikilinks, getVaultStats, lintWiki, listCanvases, listNotes, listPdfs, listTags, ocrPdf, openInUi, paperAudit, readCanvas, readNote, readPdf, renameNote, replaceInNotes, resolveWikilink, searchHybrid, searchText, semanticSearch, validateNoteProposal } from "./tools.js";
13
13
  import { Vault } from "./vault.js";
14
14
  import { VaultWatcher } from "./watcher.js";
15
- const VERSION = "3.5.5";
15
+ const VERSION = "3.5.6";
16
16
  /** Default location for the persistent embedding index, alongside .fts5.db. */
17
17
  function embedDbPath(vaultRoot) {
18
18
  // Match the FTS5 location convention by stripping the .fts5.db extension
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@oomkapwn/enquire-mcp",
4
- "version": "3.5.5",
4
+ "version": "3.5.6",
5
5
  "description": "The most advanced MCP server for Obsidian vaults. Hybrid retrieval (BM25 + TF-IDF + multilingual ML embeddings, RRF-fused) with BGE cross-encoder reranking, HNSW vector index, int8 quantization, late-chunking, HyDE-augmented retrieval, sub-question decomposition, PDFs (with OCR), Bases (.base query execution, standalone — no Obsidian needed), GraphRAG-light (Louvain wikilink community detection), wikilinks, backlinks, Dataview, frontmatter, canvas. 44 tools, 19 MCP prompts, 5 cross-encoder reranker models, 664 tests, SLSA-3, semver-bound. Works with Claude Code, Claude Desktop, Cursor, ChatGPT custom GPT, Codex, and any MCP client.",
6
6
  "type": "module",
7
7
  "bin": {