knowledge-rag 4.8.3__tar.gz → 4.8.4__tar.gz

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.
Files changed (25) hide show
  1. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/PKG-INFO +91 -24
  2. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/README.md +89 -22
  3. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/__init__.py +1 -1
  4. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/ingestion.py +24 -3
  5. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/server.py +11 -1
  6. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/pyproject.toml +1 -1
  7. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/.gitignore +0 -0
  8. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/LICENSE +0 -0
  9. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/config.example.yaml +0 -0
  10. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/config.py +0 -0
  11. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/fts5_index.py +0 -0
  12. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/guarded.py +0 -0
  13. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/instance_lock.py +0 -0
  14. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/metrics.py +0 -0
  15. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/preflight.py +0 -0
  16. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/query_router.py +0 -0
  17. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/ratelimit.py +0 -0
  18. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/mcp_server/security.py +0 -0
  19. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/npm/README.md +0 -0
  20. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/presets/cybersecurity.yaml +0 -0
  21. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/presets/developer.yaml +0 -0
  22. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/presets/general.yaml +0 -0
  23. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/presets/multilingual.yaml +0 -0
  24. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/presets/research.yaml +0 -0
  25. {knowledge_rag-4.8.3 → knowledge_rag-4.8.4}/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: knowledge-rag
3
- Version: 4.8.3
3
+ Version: 4.8.4
4
4
  Summary: Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 13 MCP Tools + 20 Format Parsers. Zero external servers.
5
5
  Project-URL: Homepage, https://github.com/lyonzin/knowledge-rag
6
6
  Project-URL: Repository, https://github.com/lyonzin/knowledge-rag
@@ -278,19 +278,32 @@ See [Changelog](#changelog) for full history.
278
278
  flowchart TB
279
279
  subgraph MCP["MCP SERVER (FastMCP)"]
280
280
  direction TB
281
- TOOLS["13 MCP Tools<br/>search | get | add | update | remove<br/>reindex | reindex_status | list | stats | url | similar | evaluate"]
281
+ TOOLS["13 MCP Tools (frozen, LEI 1)<br/>search_knowledge | get_document | search_similar<br/>add_document | add_from_url | update_document | remove_document<br/>reindex_documents | get_reindex_status<br/>list_categories | list_documents | get_index_stats | evaluate_retrieval"]
282
282
  end
283
283
 
284
- subgraph SEARCH["HYBRID SEARCH ENGINE"]
284
+ subgraph SEARCH["SEARCH DISPATCH (v4.8.2+)"]
285
+ direction TB
286
+ SM["search_method dispatch<br/>auto | hybrid | fts5"]
287
+ QROUTER["QueryRouter (regex)<br/>lexical vs semantic<br/>(ADR-002)"]
288
+ FTS5PATH["FTS5 Fast-Path<br/>(exact identifiers <10ms)"]
289
+ HYBRIDPATH["Hybrid Pipeline<br/>(conceptual queries)"]
290
+
291
+ SM --> QROUTER
292
+ QROUTER -->|lexical| FTS5PATH
293
+ QROUTER -->|semantic| HYBRIDPATH
294
+ FTS5PATH -.->|not_ready / low_hits<br/>fallback| HYBRIDPATH
295
+ end
296
+
297
+ subgraph HYBRID["HYBRID PIPELINE"]
285
298
  direction LR
286
- ROUTER["Keyword Router<br/>(word boundaries)"]
299
+ KWROUTER["Keyword Router<br/>(word boundaries → category filter)"]
287
300
  SEMANTIC["Semantic Search<br/>(ChromaDB)"]
288
301
  BM25["BM25 Keyword<br/>(inverted-index + expansion)"]
289
302
  RRF["Reciprocal Rank<br/>Fusion (RRF)"]
290
303
  RERANK["Cross-Encoder<br/>Reranker"]
291
304
 
292
- ROUTER --> SEMANTIC
293
- ROUTER --> BM25
305
+ KWROUTER --> SEMANTIC
306
+ KWROUTER --> BM25
294
307
  SEMANTIC --> RRF
295
308
  BM25 --> RRF
296
309
  RRF --> RERANK
@@ -298,9 +311,11 @@ flowchart TB
298
311
 
299
312
  subgraph STORAGE["STORAGE LAYER"]
300
313
  direction LR
301
- CHROMA[("ChromaDB<br/>Vector Database")]
302
- COLLECTIONS["Collections<br/>security | ctf<br/>logscale | development"]
314
+ CHROMA[("ChromaDB<br/>data/chroma_db/<br/>(vector store)")]
315
+ FTS5DB[("SQLite FTS5<br/>data/fts5_index.db<br/>(lexical index, ADR-001)")]
316
+ COLLECTIONS["8 Categories<br/>redteam | blueteam | ctf<br/>security | logscale<br/>development | aar | general"]
303
317
  CHROMA --- COLLECTIONS
318
+ FTS5DB --- COLLECTIONS
304
319
  end
305
320
 
306
321
  subgraph EMBED["EMBEDDINGS (In-Process)"]
@@ -312,36 +327,55 @@ flowchart TB
312
327
  subgraph INGEST["DOCUMENT INGESTION"]
313
328
  PARSERS["20 Parsers<br/>MD | PDF | TXT | PY | C | H | CPP | JS | JSX | TS | TSX | JSON | XML | CSV<br/>DOCX | XLSX | PPTX | IPYNB | MQH | MQ4"]
314
329
  CHUNKER["Chunking<br/>MD: section-aware<br/>Other: 1000 chars + 200 overlap"]
315
- PARSERS --> CHUNKER
330
+ FTS5SYNC["FTS5 CRUD sync<br/>(v4.8.2 task 05)"]
331
+ PARSERS --> CHUNKER --> FTS5SYNC
316
332
  end
317
333
 
318
334
  CLAUDE["Claude Code"] --> MCP
319
335
  MCP --> SEARCH
320
- SEARCH --> STORAGE
336
+ HYBRIDPATH --> HYBRID
337
+ FTS5PATH --> FTS5DB
338
+ HYBRID --> STORAGE
321
339
  STORAGE --> EMBED
322
340
  INGEST --> EMBED
323
341
  EMBED --> STORAGE
342
+ FTS5SYNC --> FTS5DB
324
343
  ```
325
344
 
326
345
  ### Query Processing Flow
327
346
 
328
347
  ```mermaid
329
348
  flowchart TB
330
- QUERY["User Query<br/>'mimikatz credential dump'"] --> EXPAND
349
+ QUERY["User Query<br/>'mimikatz credential dump' | 'CVE-2021-4034'"] --> METHOD
350
+
351
+ subgraph DISPATCH["Dispatch (v4.8.2+, ADR-006)"]
352
+ METHOD{"search_method<br/>auto | hybrid | fts5"}
353
+ AUTOROUTE["QueryRouter.classify()<br/>regex-based lexical detection"]
354
+ FASTPATH["FTS5 Fast-Path<br/>SQLite MATCH<br/>(&lt;10ms cold, &lt;2ms hot)"]
355
+ NOTREADY{"FTS5 ready<br/>AND hits >= min_hits?"}
356
+
357
+ METHOD -->|auto| AUTOROUTE
358
+ METHOD -->|fts5| FASTPATH
359
+ METHOD -->|hybrid| EXPAND
360
+ AUTOROUTE -->|lexical| FASTPATH
361
+ AUTOROUTE -->|semantic| EXPAND
362
+ FASTPATH --> NOTREADY
363
+ NOTREADY -->|no, fallback| EXPAND
364
+ end
331
365
 
332
- subgraph EXPANSION["Query Expansion"]
366
+ subgraph EXPANSION["Query Expansion (hybrid path)"]
333
367
  EXPAND["Synonym Expansion<br/>mimikatz -> mimikatz, sekurlsa, logonpasswords"]
334
368
  end
335
369
 
336
- EXPAND --> ROUTER
370
+ EXPAND --> KWROUTER
337
371
 
338
- subgraph ROUTING["Keyword Routing"]
339
- ROUTER["Keyword Router"]
372
+ subgraph ROUTING["Keyword Routing (category filter)"]
373
+ KWROUTER["Keyword Router<br/>(word boundaries)"]
340
374
  MATCH{"Word Boundary<br/>Match?"}
341
375
  CATEGORY["Filter: redteam"]
342
376
  NOFILTER["No Filter"]
343
377
 
344
- ROUTER --> MATCH
378
+ KWROUTER --> MATCH
345
379
  MATCH -->|Yes| CATEGORY
346
380
  MATCH -->|No| NOFILTER
347
381
  end
@@ -374,7 +408,8 @@ flowchart TB
374
408
  BM25 --> RRF
375
409
 
376
410
  ADJ --> MINSCORE
377
- SNIPPET --> RESULTS["Results<br/>search_method: hybrid|semantic|keyword<br/>score + filtered_by_score + content_length"]
411
+ NOTREADY -->|yes, hit| MINSCORE
412
+ SNIPPET --> RESULTS["Results<br/>search_method: fts5 | hybrid | semantic | keyword<br/>routed_by: fts5_router | none<br/>score + filtered_by_score + content_length"]
378
413
  ```
379
414
 
380
415
  ### Document Ingestion Flow
@@ -382,7 +417,7 @@ flowchart TB
382
417
  ```mermaid
383
418
  flowchart LR
384
419
  subgraph INPUT["Input"]
385
- FILES["documents/<br/>├── security/<br/>├── development/<br/>├── ctf/<br/>└── general/"]
420
+ FILES["documents/<br/>├── security/<br/>│ ├── redteam/<br/>│ ├── blueteam/<br/>│ └── ctf/<br/>├── aar/<br/>├── logscale/<br/>├── development/<br/>└── general/"]
386
421
  end
387
422
 
388
423
  subgraph PARSE["Parse (20 formats)"]
@@ -402,9 +437,18 @@ flowchart LR
402
437
  FASTEMBED["FastEmbed ONNX<br/>bge-small-en-v1.5<br/>(384D, CPU or GPU)"]
403
438
  end
404
439
 
440
+ subgraph DISPATCH["Write Dispatch (v4.8.3, #161)"]
441
+ WCOL{"_write_collection<br/>routes writes"}
442
+ STAGING["Staging Collection<br/>(nuclear_rebuild only,<br/>keeps prod serving queries)"]
443
+ PROD["Production Collection<br/>(default)"]
444
+ WCOL -->|_staging_target set| STAGING
445
+ WCOL -->|default| PROD
446
+ end
447
+
405
448
  subgraph STORE["Store"]
406
- CHROMADB[("ChromaDB")]
407
- BM25IDX["BM25 Index"]
449
+ CHROMADB[("ChromaDB<br/>data/chroma_db/")]
450
+ BM25IDX["BM25 Index<br/>(in-memory,<br/>rebuilt on write)"]
451
+ FTS5DB[("SQLite FTS5<br/>data/fts5_index.db<br/>(CRUD-synced, ADR-008)")]
408
452
  end
409
453
 
410
454
  FILES --> MD & PDF & OFFICE & CODE
@@ -413,14 +457,20 @@ flowchart LR
413
457
  MDSPLIT --> DEDUP
414
458
  TXTSPLIT --> DEDUP
415
459
  DEDUP --> EMBED
416
- EMBED --> STORE
460
+ EMBED --> DISPATCH
461
+ STAGING --> CHROMADB
462
+ PROD --> CHROMADB
463
+ PROD --> BM25IDX
464
+ PROD --> FTS5DB
417
465
  ```
418
466
 
419
467
  ### hybrid_alpha Parameter Effect
420
468
 
469
+ `hybrid_alpha` weights RRF fusion between semantic and BM25 rankings on the **hybrid pipeline only**. When `search_method="auto"` and the QueryRouter classifies the query as lexical (e.g. `CVE-2021-4034`, `MDR-AD002`, `T1078.001`, file hashes), the FTS5 fast-path fires and `hybrid_alpha` is not consulted — FTS5 uses SQLite's native `bm25()` scoring exclusively. Pass `search_method="hybrid"` to force RRF fusion + rerank on every query if you want deterministic hybrid semantics regardless of the query shape.
470
+
421
471
  ```mermaid
422
472
  flowchart LR
423
- subgraph ALPHA["hybrid_alpha values"]
473
+ subgraph ALPHA["hybrid_alpha values (hybrid path only)"]
424
474
  A0["0.0<br/>Pure BM25<br/>Instant"]
425
475
  A3["0.3 (default)<br/>Keyword-heavy<br/>Fast"]
426
476
  A5["0.5<br/>Balanced"]
@@ -429,7 +479,7 @@ flowchart LR
429
479
  end
430
480
 
431
481
  subgraph USE["Best For"]
432
- U0["CVEs, tool names<br/>exact matches"]
482
+ U0["CVEs, tool names<br/>exact matches<br/>(consider FTS5 fast-path)"]
433
483
  U3["Technical queries<br/>specific terms"]
434
484
  U5["General queries"]
435
485
  U7["Conceptual queries<br/>related topics"]
@@ -1483,6 +1533,23 @@ Common issues:
1483
1533
 
1484
1534
  ### Unreleased
1485
1535
 
1536
+ ### v4.8.4 (2026-08-13) — Patch: security + durability + defensive fixes
1537
+
1538
+ **Recommended upgrade for anyone running HTTP/SSE transport with bearer auth on a non-loopback host, or relying on `reindex_documents(force=True)`.** Four independent fixes accumulated since v4.8.3: one previously blocked remote-with-auth deployments (#174), one silently corrupted durable index metadata after a failed rebuild (#173), and two defensive fixes hardened `.ipynb` ingestion (#175) and the nightly chaos suite (#164).
1539
+
1540
+ **Fixed:**
1541
+
1542
+ - **fix(security)** — create the HTTP MCP app with the configured `server_host`. On the bearer-auth path the app factory was called without a host, so it defaulted to `127.0.0.1` and MCP 2.0.0's localhost-only DNS-rebinding allowlist rejected a configured non-loopback host with `421 Invalid Host header` before auth ran — even though uvicorn was bound to that host. Remote/LAN deployments with auth are now reachable (#174).
1543
+ - **fix(reindex)** — restore durable metadata on a failed zero-downtime rebuild. The staging populate path persisted the staging map to `index_metadata.json` before validation ran; when validation failed, rollback restored only the in-memory `_indexed_docs`, leaving the file describing staging while memory held production. After a restart `_load_metadata` trusted the stale file, corrupting skip/update and embedding-dimension decisions. Rollback now re-persists the restored production metadata (#173).
1544
+ - **fix(ingestion)** — `_parse_ipynb` handled invalid JSON gracefully but then assumed the parsed notebook was a well-formed dict. A valid-JSON notebook with a null `metadata` or `cells` field, a non-dict cell, a bare list, or a dict cell whose `source` is not `str | list[str]` (a number, a dict, or a list with non-string items) crashed with `AttributeError`/`TypeError`. Guarded the notebook, `metadata`, `cells`, cell, and cell `source` lookups so a parseable-but-malformed notebook is handled the same as any other odd input (#175, thanks @eeshsaxena).
1545
+ - **fix(ci)** — the nightly chaos job (`pytest -m chaos`) aborted collection with `ModuleNotFoundError: No module named 'hypothesis'` because `tests/test_query_router.py` imported hypothesis at module top-level while the chaos job installs only `pytest psutil numpy`. Guarded the import with `pytest.importorskip("hypothesis")` so the module is skipped cleanly instead of failing the whole nightly run (#164).
1546
+
1547
+ **Docs:**
1548
+
1549
+ - **docs(architecture)** — sync the 4 Mermaid diagrams in `## Architecture` (System Overview, Query Processing Flow, Document Ingestion Flow, hybrid_alpha Parameter Effect) with the v4.8.2 FTS5 fast-path (ADR-002/003/006), the v4.8.3 `_write_collection` staging dispatcher (#161), and the current 8-category layout. The diagrams previously described only the pre-v4.8 hybrid pipeline and 4-category storage; they now also show FTS5 dispatch, fallback semantics, FTS5 CRUD sync (ADR-008), and the zero-downtime staging write path. `hybrid_alpha` now clarifies that the fast-path bypasses RRF entirely so the weighting only matters on the hybrid path.
1550
+
1551
+ **No breaking changes.** Backwards compat verified by `check_api_surface.py` (zero diff vs baseline). All 13 MCP tool parameter names frozen (verified by `tests/test_backwards_compat.py`).
1552
+
1486
1553
  ### v4.8.3 (2026-08-10) — Critical hotfix: nuclear-rebuild + smart-reindex hardening
1487
1554
 
1488
1555
  **Recommended upgrade for any corpus with >1000 chunks or any user relying on `reindex_documents(force=True)` after a config change.** The v4.8.0 nuclear rebuild + v4.8.2 FTS5 fast-path have several latent state-management bugs that only trigger under a real-world combination (large corpus + rebuild + concurrent queries + FTS5 opt-in). Enterprise users with hacktricks-scale corpora (~50k+ chunks) are affected.
@@ -230,19 +230,32 @@ See [Changelog](#changelog) for full history.
230
230
  flowchart TB
231
231
  subgraph MCP["MCP SERVER (FastMCP)"]
232
232
  direction TB
233
- TOOLS["13 MCP Tools<br/>search | get | add | update | remove<br/>reindex | reindex_status | list | stats | url | similar | evaluate"]
233
+ TOOLS["13 MCP Tools (frozen, LEI 1)<br/>search_knowledge | get_document | search_similar<br/>add_document | add_from_url | update_document | remove_document<br/>reindex_documents | get_reindex_status<br/>list_categories | list_documents | get_index_stats | evaluate_retrieval"]
234
234
  end
235
235
 
236
- subgraph SEARCH["HYBRID SEARCH ENGINE"]
236
+ subgraph SEARCH["SEARCH DISPATCH (v4.8.2+)"]
237
+ direction TB
238
+ SM["search_method dispatch<br/>auto | hybrid | fts5"]
239
+ QROUTER["QueryRouter (regex)<br/>lexical vs semantic<br/>(ADR-002)"]
240
+ FTS5PATH["FTS5 Fast-Path<br/>(exact identifiers <10ms)"]
241
+ HYBRIDPATH["Hybrid Pipeline<br/>(conceptual queries)"]
242
+
243
+ SM --> QROUTER
244
+ QROUTER -->|lexical| FTS5PATH
245
+ QROUTER -->|semantic| HYBRIDPATH
246
+ FTS5PATH -.->|not_ready / low_hits<br/>fallback| HYBRIDPATH
247
+ end
248
+
249
+ subgraph HYBRID["HYBRID PIPELINE"]
237
250
  direction LR
238
- ROUTER["Keyword Router<br/>(word boundaries)"]
251
+ KWROUTER["Keyword Router<br/>(word boundaries → category filter)"]
239
252
  SEMANTIC["Semantic Search<br/>(ChromaDB)"]
240
253
  BM25["BM25 Keyword<br/>(inverted-index + expansion)"]
241
254
  RRF["Reciprocal Rank<br/>Fusion (RRF)"]
242
255
  RERANK["Cross-Encoder<br/>Reranker"]
243
256
 
244
- ROUTER --> SEMANTIC
245
- ROUTER --> BM25
257
+ KWROUTER --> SEMANTIC
258
+ KWROUTER --> BM25
246
259
  SEMANTIC --> RRF
247
260
  BM25 --> RRF
248
261
  RRF --> RERANK
@@ -250,9 +263,11 @@ flowchart TB
250
263
 
251
264
  subgraph STORAGE["STORAGE LAYER"]
252
265
  direction LR
253
- CHROMA[("ChromaDB<br/>Vector Database")]
254
- COLLECTIONS["Collections<br/>security | ctf<br/>logscale | development"]
266
+ CHROMA[("ChromaDB<br/>data/chroma_db/<br/>(vector store)")]
267
+ FTS5DB[("SQLite FTS5<br/>data/fts5_index.db<br/>(lexical index, ADR-001)")]
268
+ COLLECTIONS["8 Categories<br/>redteam | blueteam | ctf<br/>security | logscale<br/>development | aar | general"]
255
269
  CHROMA --- COLLECTIONS
270
+ FTS5DB --- COLLECTIONS
256
271
  end
257
272
 
258
273
  subgraph EMBED["EMBEDDINGS (In-Process)"]
@@ -264,36 +279,55 @@ flowchart TB
264
279
  subgraph INGEST["DOCUMENT INGESTION"]
265
280
  PARSERS["20 Parsers<br/>MD | PDF | TXT | PY | C | H | CPP | JS | JSX | TS | TSX | JSON | XML | CSV<br/>DOCX | XLSX | PPTX | IPYNB | MQH | MQ4"]
266
281
  CHUNKER["Chunking<br/>MD: section-aware<br/>Other: 1000 chars + 200 overlap"]
267
- PARSERS --> CHUNKER
282
+ FTS5SYNC["FTS5 CRUD sync<br/>(v4.8.2 task 05)"]
283
+ PARSERS --> CHUNKER --> FTS5SYNC
268
284
  end
269
285
 
270
286
  CLAUDE["Claude Code"] --> MCP
271
287
  MCP --> SEARCH
272
- SEARCH --> STORAGE
288
+ HYBRIDPATH --> HYBRID
289
+ FTS5PATH --> FTS5DB
290
+ HYBRID --> STORAGE
273
291
  STORAGE --> EMBED
274
292
  INGEST --> EMBED
275
293
  EMBED --> STORAGE
294
+ FTS5SYNC --> FTS5DB
276
295
  ```
277
296
 
278
297
  ### Query Processing Flow
279
298
 
280
299
  ```mermaid
281
300
  flowchart TB
282
- QUERY["User Query<br/>'mimikatz credential dump'"] --> EXPAND
301
+ QUERY["User Query<br/>'mimikatz credential dump' | 'CVE-2021-4034'"] --> METHOD
302
+
303
+ subgraph DISPATCH["Dispatch (v4.8.2+, ADR-006)"]
304
+ METHOD{"search_method<br/>auto | hybrid | fts5"}
305
+ AUTOROUTE["QueryRouter.classify()<br/>regex-based lexical detection"]
306
+ FASTPATH["FTS5 Fast-Path<br/>SQLite MATCH<br/>(&lt;10ms cold, &lt;2ms hot)"]
307
+ NOTREADY{"FTS5 ready<br/>AND hits >= min_hits?"}
308
+
309
+ METHOD -->|auto| AUTOROUTE
310
+ METHOD -->|fts5| FASTPATH
311
+ METHOD -->|hybrid| EXPAND
312
+ AUTOROUTE -->|lexical| FASTPATH
313
+ AUTOROUTE -->|semantic| EXPAND
314
+ FASTPATH --> NOTREADY
315
+ NOTREADY -->|no, fallback| EXPAND
316
+ end
283
317
 
284
- subgraph EXPANSION["Query Expansion"]
318
+ subgraph EXPANSION["Query Expansion (hybrid path)"]
285
319
  EXPAND["Synonym Expansion<br/>mimikatz -> mimikatz, sekurlsa, logonpasswords"]
286
320
  end
287
321
 
288
- EXPAND --> ROUTER
322
+ EXPAND --> KWROUTER
289
323
 
290
- subgraph ROUTING["Keyword Routing"]
291
- ROUTER["Keyword Router"]
324
+ subgraph ROUTING["Keyword Routing (category filter)"]
325
+ KWROUTER["Keyword Router<br/>(word boundaries)"]
292
326
  MATCH{"Word Boundary<br/>Match?"}
293
327
  CATEGORY["Filter: redteam"]
294
328
  NOFILTER["No Filter"]
295
329
 
296
- ROUTER --> MATCH
330
+ KWROUTER --> MATCH
297
331
  MATCH -->|Yes| CATEGORY
298
332
  MATCH -->|No| NOFILTER
299
333
  end
@@ -326,7 +360,8 @@ flowchart TB
326
360
  BM25 --> RRF
327
361
 
328
362
  ADJ --> MINSCORE
329
- SNIPPET --> RESULTS["Results<br/>search_method: hybrid|semantic|keyword<br/>score + filtered_by_score + content_length"]
363
+ NOTREADY -->|yes, hit| MINSCORE
364
+ SNIPPET --> RESULTS["Results<br/>search_method: fts5 | hybrid | semantic | keyword<br/>routed_by: fts5_router | none<br/>score + filtered_by_score + content_length"]
330
365
  ```
331
366
 
332
367
  ### Document Ingestion Flow
@@ -334,7 +369,7 @@ flowchart TB
334
369
  ```mermaid
335
370
  flowchart LR
336
371
  subgraph INPUT["Input"]
337
- FILES["documents/<br/>├── security/<br/>├── development/<br/>├── ctf/<br/>└── general/"]
372
+ FILES["documents/<br/>├── security/<br/>│ ├── redteam/<br/>│ ├── blueteam/<br/>│ └── ctf/<br/>├── aar/<br/>├── logscale/<br/>├── development/<br/>└── general/"]
338
373
  end
339
374
 
340
375
  subgraph PARSE["Parse (20 formats)"]
@@ -354,9 +389,18 @@ flowchart LR
354
389
  FASTEMBED["FastEmbed ONNX<br/>bge-small-en-v1.5<br/>(384D, CPU or GPU)"]
355
390
  end
356
391
 
392
+ subgraph DISPATCH["Write Dispatch (v4.8.3, #161)"]
393
+ WCOL{"_write_collection<br/>routes writes"}
394
+ STAGING["Staging Collection<br/>(nuclear_rebuild only,<br/>keeps prod serving queries)"]
395
+ PROD["Production Collection<br/>(default)"]
396
+ WCOL -->|_staging_target set| STAGING
397
+ WCOL -->|default| PROD
398
+ end
399
+
357
400
  subgraph STORE["Store"]
358
- CHROMADB[("ChromaDB")]
359
- BM25IDX["BM25 Index"]
401
+ CHROMADB[("ChromaDB<br/>data/chroma_db/")]
402
+ BM25IDX["BM25 Index<br/>(in-memory,<br/>rebuilt on write)"]
403
+ FTS5DB[("SQLite FTS5<br/>data/fts5_index.db<br/>(CRUD-synced, ADR-008)")]
360
404
  end
361
405
 
362
406
  FILES --> MD & PDF & OFFICE & CODE
@@ -365,14 +409,20 @@ flowchart LR
365
409
  MDSPLIT --> DEDUP
366
410
  TXTSPLIT --> DEDUP
367
411
  DEDUP --> EMBED
368
- EMBED --> STORE
412
+ EMBED --> DISPATCH
413
+ STAGING --> CHROMADB
414
+ PROD --> CHROMADB
415
+ PROD --> BM25IDX
416
+ PROD --> FTS5DB
369
417
  ```
370
418
 
371
419
  ### hybrid_alpha Parameter Effect
372
420
 
421
+ `hybrid_alpha` weights RRF fusion between semantic and BM25 rankings on the **hybrid pipeline only**. When `search_method="auto"` and the QueryRouter classifies the query as lexical (e.g. `CVE-2021-4034`, `MDR-AD002`, `T1078.001`, file hashes), the FTS5 fast-path fires and `hybrid_alpha` is not consulted — FTS5 uses SQLite's native `bm25()` scoring exclusively. Pass `search_method="hybrid"` to force RRF fusion + rerank on every query if you want deterministic hybrid semantics regardless of the query shape.
422
+
373
423
  ```mermaid
374
424
  flowchart LR
375
- subgraph ALPHA["hybrid_alpha values"]
425
+ subgraph ALPHA["hybrid_alpha values (hybrid path only)"]
376
426
  A0["0.0<br/>Pure BM25<br/>Instant"]
377
427
  A3["0.3 (default)<br/>Keyword-heavy<br/>Fast"]
378
428
  A5["0.5<br/>Balanced"]
@@ -381,7 +431,7 @@ flowchart LR
381
431
  end
382
432
 
383
433
  subgraph USE["Best For"]
384
- U0["CVEs, tool names<br/>exact matches"]
434
+ U0["CVEs, tool names<br/>exact matches<br/>(consider FTS5 fast-path)"]
385
435
  U3["Technical queries<br/>specific terms"]
386
436
  U5["General queries"]
387
437
  U7["Conceptual queries<br/>related topics"]
@@ -1435,6 +1485,23 @@ Common issues:
1435
1485
 
1436
1486
  ### Unreleased
1437
1487
 
1488
+ ### v4.8.4 (2026-08-13) — Patch: security + durability + defensive fixes
1489
+
1490
+ **Recommended upgrade for anyone running HTTP/SSE transport with bearer auth on a non-loopback host, or relying on `reindex_documents(force=True)`.** Four independent fixes accumulated since v4.8.3: one previously blocked remote-with-auth deployments (#174), one silently corrupted durable index metadata after a failed rebuild (#173), and two defensive fixes hardened `.ipynb` ingestion (#175) and the nightly chaos suite (#164).
1491
+
1492
+ **Fixed:**
1493
+
1494
+ - **fix(security)** — create the HTTP MCP app with the configured `server_host`. On the bearer-auth path the app factory was called without a host, so it defaulted to `127.0.0.1` and MCP 2.0.0's localhost-only DNS-rebinding allowlist rejected a configured non-loopback host with `421 Invalid Host header` before auth ran — even though uvicorn was bound to that host. Remote/LAN deployments with auth are now reachable (#174).
1495
+ - **fix(reindex)** — restore durable metadata on a failed zero-downtime rebuild. The staging populate path persisted the staging map to `index_metadata.json` before validation ran; when validation failed, rollback restored only the in-memory `_indexed_docs`, leaving the file describing staging while memory held production. After a restart `_load_metadata` trusted the stale file, corrupting skip/update and embedding-dimension decisions. Rollback now re-persists the restored production metadata (#173).
1496
+ - **fix(ingestion)** — `_parse_ipynb` handled invalid JSON gracefully but then assumed the parsed notebook was a well-formed dict. A valid-JSON notebook with a null `metadata` or `cells` field, a non-dict cell, a bare list, or a dict cell whose `source` is not `str | list[str]` (a number, a dict, or a list with non-string items) crashed with `AttributeError`/`TypeError`. Guarded the notebook, `metadata`, `cells`, cell, and cell `source` lookups so a parseable-but-malformed notebook is handled the same as any other odd input (#175, thanks @eeshsaxena).
1497
+ - **fix(ci)** — the nightly chaos job (`pytest -m chaos`) aborted collection with `ModuleNotFoundError: No module named 'hypothesis'` because `tests/test_query_router.py` imported hypothesis at module top-level while the chaos job installs only `pytest psutil numpy`. Guarded the import with `pytest.importorskip("hypothesis")` so the module is skipped cleanly instead of failing the whole nightly run (#164).
1498
+
1499
+ **Docs:**
1500
+
1501
+ - **docs(architecture)** — sync the 4 Mermaid diagrams in `## Architecture` (System Overview, Query Processing Flow, Document Ingestion Flow, hybrid_alpha Parameter Effect) with the v4.8.2 FTS5 fast-path (ADR-002/003/006), the v4.8.3 `_write_collection` staging dispatcher (#161), and the current 8-category layout. The diagrams previously described only the pre-v4.8 hybrid pipeline and 4-category storage; they now also show FTS5 dispatch, fallback semantics, FTS5 CRUD sync (ADR-008), and the zero-downtime staging write path. `hybrid_alpha` now clarifies that the fast-path bypasses RRF entirely so the weighting only matters on the hybrid path.
1502
+
1503
+ **No breaking changes.** Backwards compat verified by `check_api_surface.py` (zero diff vs baseline). All 13 MCP tool parameter names frozen (verified by `tests/test_backwards_compat.py`).
1504
+
1438
1505
  ### v4.8.3 (2026-08-10) — Critical hotfix: nuclear-rebuild + smart-reindex hardening
1439
1506
 
1440
1507
  **Recommended upgrade for any corpus with >1000 chunks or any user relying on `reindex_documents(force=True)` after a config change.** The v4.8.0 nuclear rebuild + v4.8.2 FTS5 fast-path have several latent state-management bugs that only trigger under a real-world combination (large corpus + rebuild + concurrent queries + FTS5 opt-in). Enterprise users with hacktricks-scale corpora (~50k+ chunks) are affected.
@@ -8,7 +8,7 @@ import sys # noqa: I001
8
8
  _original_stdout = sys.stdout
9
9
  sys.stdout = sys.stderr
10
10
 
11
- __version__ = "4.8.3"
11
+ __version__ = "4.8.4"
12
12
  __author__ = "Ailton Rocha (Lyon.)"
13
13
 
14
14
  from .config import Config # noqa: E402
@@ -613,23 +613,44 @@ class DocumentParser:
613
613
  metadata["is_valid_json"] = False
614
614
  return raw, metadata
615
615
 
616
+ # Valid JSON, but not necessarily a well-formed notebook: it may be a
617
+ # bare list/scalar, or have null / wrong-typed "metadata"/"cells" fields.
618
+ # Guard the lookups so a malformed-but-parseable .ipynb is handled
619
+ # gracefully instead of crashing with AttributeError/TypeError.
620
+ if not isinstance(nb, dict):
621
+ metadata["is_valid_json"] = False
622
+ return raw, metadata
623
+
616
624
  metadata["is_valid_json"] = True
617
625
  metadata["nbformat"] = nb.get("nbformat", 0)
618
- kernel = nb.get("metadata", {}).get("kernelspec", {})
626
+ nb_metadata = nb.get("metadata")
627
+ kernel = nb_metadata.get("kernelspec") if isinstance(nb_metadata, dict) else None
628
+ if not isinstance(kernel, dict):
629
+ kernel = {}
619
630
  metadata["kernel"] = kernel.get("display_name", kernel.get("name", "unknown"))
620
631
 
621
- cells = nb.get("cells", [])
632
+ cells = nb.get("cells")
633
+ if not isinstance(cells, list):
634
+ cells = []
622
635
  metadata["cells"] = len(cells)
623
636
  code_cells = 0
624
637
  markdown_cells = 0
625
638
 
626
639
  parts = []
627
640
  for cell in cells:
641
+ if not isinstance(cell, dict):
642
+ continue
628
643
  cell_type = cell.get("cell_type", "")
629
644
  source = cell.get("source", "")
630
645
 
646
+ # Per the nbformat spec `source` is str | list[str]. Anything else
647
+ # (a number, a dict, a list with non-string items) is the same
648
+ # malformed-but-parseable case handled above, so tolerate it rather
649
+ # than letting it reach "".join()/.strip() and raise.
631
650
  if isinstance(source, list):
632
- source = "".join(source)
651
+ source = "".join(s for s in source if isinstance(s, str))
652
+ elif not isinstance(source, str):
653
+ continue
633
654
 
634
655
  if not source or not source.strip():
635
656
  continue
@@ -2420,6 +2420,12 @@ class KnowledgeOrchestrator:
2420
2420
  # collection we're about to delete.
2421
2421
  self._staging_target = None
2422
2422
  self._rollback_staging_state(saved)
2423
+ # GH #173: populate persisted the staging map to index_metadata.json
2424
+ # (index_all -> _finalize_reindex -> _save_metadata) before validation.
2425
+ # Restoring only in-memory fields would leave the file describing
2426
+ # staging while memory holds production; re-persist so durable metadata
2427
+ # matches the restored state and a restart won't load stale staging.
2428
+ self._save_metadata()
2423
2429
  try:
2424
2430
  self.chroma_client.delete_collection(f"{prod_name}__staging_{ts}")
2425
2431
  except Exception:
@@ -4550,7 +4556,11 @@ def _run_transport(transport: str) -> None:
4550
4556
  import uvicorn
4551
4557
 
4552
4558
  app_factory = _http_app_factory(transport)
4553
- app = app_factory()
4559
+ # Build the app with the configured host. MCP 2.0.0's factories default to
4560
+ # host="127.0.0.1", which turns on a localhost-only DNS-rebinding allowlist;
4561
+ # without this, a non-loopback server_host is served by uvicorn but the app
4562
+ # rejects the configured host with 421 before auth runs (GH #174).
4563
+ app = app_factory(host=config.server_host)
4554
4564
  guarded = BearerAuthMiddleware(app, token)
4555
4565
  print(
4556
4566
  f"[SECURITY] Bearer auth enforced on {transport} transport ({config.server_host}:{config.server_port})",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "knowledge-rag"
7
- version = "4.8.3"
7
+ version = "4.8.4"
8
8
  description = "Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 13 MCP Tools + 20 Format Parsers. Zero external servers."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes