doxtr-rag 0.1.0__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 (63) hide show
  1. doxtr_rag-0.1.0/LICENSE +21 -0
  2. doxtr_rag-0.1.0/PKG-INFO +502 -0
  3. doxtr_rag-0.1.0/README.md +455 -0
  4. doxtr_rag-0.1.0/doxtr_rag/__init__.py +98 -0
  5. doxtr_rag-0.1.0/doxtr_rag/_lifecycle.py +95 -0
  6. doxtr_rag-0.1.0/doxtr_rag/agent_install.py +248 -0
  7. doxtr_rag-0.1.0/doxtr_rag/audit.py +93 -0
  8. doxtr_rag-0.1.0/doxtr_rag/base_store.py +118 -0
  9. doxtr_rag-0.1.0/doxtr_rag/cache.py +165 -0
  10. doxtr_rag-0.1.0/doxtr_rag/cli.py +179 -0
  11. doxtr_rag-0.1.0/doxtr_rag/config.py +297 -0
  12. doxtr_rag-0.1.0/doxtr_rag/embed_server.py +117 -0
  13. doxtr_rag-0.1.0/doxtr_rag/embedding.py +86 -0
  14. doxtr_rag-0.1.0/doxtr_rag/embedding_provider.py +141 -0
  15. doxtr_rag-0.1.0/doxtr_rag/extractors.py +442 -0
  16. doxtr_rag-0.1.0/doxtr_rag/harvester.py +276 -0
  17. doxtr_rag-0.1.0/doxtr_rag/pathscope.py +81 -0
  18. doxtr_rag-0.1.0/doxtr_rag/pi_extension/addressing.ts +64 -0
  19. doxtr_rag-0.1.0/doxtr_rag/pi_extension/chroma-rag.ts +336 -0
  20. doxtr_rag-0.1.0/doxtr_rag/pi_extension/package-lock.json +4377 -0
  21. doxtr_rag-0.1.0/doxtr_rag/pi_extension/package.json +26 -0
  22. doxtr_rag-0.1.0/doxtr_rag/pi_extension/rrf.ts +46 -0
  23. doxtr_rag-0.1.0/doxtr_rag/pi_extension/scrub.ts +53 -0
  24. doxtr_rag-0.1.0/doxtr_rag/pi_extension/tests/test-extension.test.ts +249 -0
  25. doxtr_rag-0.1.0/doxtr_rag/pi_extension/tsconfig.json +24 -0
  26. doxtr_rag-0.1.0/doxtr_rag/pi_extension/vitest.config.ts +8 -0
  27. doxtr_rag-0.1.0/doxtr_rag/pi_extension/xlang-query.ts +72 -0
  28. doxtr_rag-0.1.0/doxtr_rag/pipeline.py +345 -0
  29. doxtr_rag-0.1.0/doxtr_rag/py.typed +0 -0
  30. doxtr_rag-0.1.0/doxtr_rag/schema.py +237 -0
  31. doxtr_rag-0.1.0/doxtr_rag/scrub.py +91 -0
  32. doxtr_rag-0.1.0/doxtr_rag/security.py +350 -0
  33. doxtr_rag-0.1.0/doxtr_rag/server.py +112 -0
  34. doxtr_rag-0.1.0/doxtr_rag/storage.py +497 -0
  35. doxtr_rag-0.1.0/doxtr_rag/weights.py +132 -0
  36. doxtr_rag-0.1.0/doxtr_rag.egg-info/PKG-INFO +502 -0
  37. doxtr_rag-0.1.0/doxtr_rag.egg-info/SOURCES.txt +61 -0
  38. doxtr_rag-0.1.0/doxtr_rag.egg-info/dependency_links.txt +1 -0
  39. doxtr_rag-0.1.0/doxtr_rag.egg-info/entry_points.txt +3 -0
  40. doxtr_rag-0.1.0/doxtr_rag.egg-info/requires.txt +19 -0
  41. doxtr_rag-0.1.0/doxtr_rag.egg-info/top_level.txt +1 -0
  42. doxtr_rag-0.1.0/pyproject.toml +127 -0
  43. doxtr_rag-0.1.0/setup.cfg +4 -0
  44. doxtr_rag-0.1.0/tests/test_agent_install.py +131 -0
  45. doxtr_rag-0.1.0/tests/test_audit.py +104 -0
  46. doxtr_rag-0.1.0/tests/test_base_store.py +99 -0
  47. doxtr_rag-0.1.0/tests/test_cache.py +73 -0
  48. doxtr_rag-0.1.0/tests/test_config.py +188 -0
  49. doxtr_rag-0.1.0/tests/test_cross_language.py +381 -0
  50. doxtr_rag-0.1.0/tests/test_dox_pdf_ingest.py +121 -0
  51. doxtr_rag-0.1.0/tests/test_dox_reference_build.py +90 -0
  52. doxtr_rag-0.1.0/tests/test_embed_server.py +85 -0
  53. doxtr_rag-0.1.0/tests/test_embedding.py +40 -0
  54. doxtr_rag-0.1.0/tests/test_extractors.py +208 -0
  55. doxtr_rag-0.1.0/tests/test_harvester.py +264 -0
  56. doxtr_rag-0.1.0/tests/test_init.py +99 -0
  57. doxtr_rag-0.1.0/tests/test_lifecycle.py +89 -0
  58. doxtr_rag-0.1.0/tests/test_pipeline.py +555 -0
  59. doxtr_rag-0.1.0/tests/test_schema.py +167 -0
  60. doxtr_rag-0.1.0/tests/test_scrub.py +76 -0
  61. doxtr_rag-0.1.0/tests/test_security.py +241 -0
  62. doxtr_rag-0.1.0/tests/test_storage.py +250 -0
  63. doxtr_rag-0.1.0/tests/test_weights.py +122 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Doxtr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,502 @@
1
+ Metadata-Version: 2.4
2
+ Name: doxtr-rag
3
+ Version: 0.1.0
4
+ Summary: Multi-tenant, fail-closed Sphinx RAG extension for the doxtr ecosystem (dual-store ChromaDB knowledge base with security-classified xlinks).
5
+ Author: Jens Frey
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/doxtr/doxtr-rag
8
+ Project-URL: Repository, https://github.com/doxtr/doxtr-rag
9
+ Project-URL: Changelog, https://github.com/doxtr/doxtr-rag/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/doxtr/doxtr-rag/issues
11
+ Keywords: sphinx,rag,retrieval-augmented-generation,semantic-search,vector-search,chromadb,embeddings,bge-m3,knowledge-base,documentation,ai-agent,pi-agent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Framework :: Sphinx :: Extension
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Topic :: Documentation :: Sphinx
23
+ Classifier: Topic :: Text Processing :: Indexing
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: sphinx>=5.0
29
+ Requires-Dist: pydantic<3,>=2
30
+ Requires-Dist: chromadb<2,>=1.0
31
+ Requires-Dist: onnxruntime>=1.17
32
+ Requires-Dist: tokenizers>=0.15
33
+ Requires-Dist: numpy>=1.24
34
+ Requires-Dist: huggingface-hub>=0.20
35
+ Requires-Dist: pypdf>=4.0
36
+ Requires-Dist: openpyxl>=3.1
37
+ Requires-Dist: python-docx>=1.1
38
+ Requires-Dist: python-pptx>=0.6.23
39
+ Requires-Dist: odfpy>=1.4.1
40
+ Requires-Dist: httpx>=0.27
41
+ Requires-Dist: sphinxcontrib-xlink>=1.3
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=7.0; extra == "dev"
44
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
45
+ Requires-Dist: mypy>=1.8; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # doxtr-rag
49
+
50
+ A multi-tenant, **fail-closed** Sphinx RAG (retrieval-augmented generation)
51
+ extension for the `doxtr` ecosystem. It builds a **dual-store** knowledge base
52
+ from your Sphinx documentation, mounted multi-format files, and
53
+ security-classified Jira/Confluence cross-links (`xlink`s), then serves it to an
54
+ Pi Agent TypeScript extension at agent time.
55
+
56
+ ## 1. What the project is
57
+
58
+ `doxtr-rag` is a Sphinx extension plus a companion Pi Agent TypeScript extension
59
+ that together implement a retrieval knowledge base with a hard security
60
+ boundary between two physically-isolated ChromaDB stores:
61
+
62
+ - **`SHARED_ORG`** — organization-wide, safe-to-share knowledge (the Sphinx
63
+ `source/` tree and mounts explicitly declared shared).
64
+ - **`LOCAL_PRIVATE`** — everything else, by default. Any document, node, mount,
65
+ or `xlink` with an ambiguous, unverifiable, or failed permission check routes
66
+ here. There is no code path where an unresolved scope becomes `SHARED_ORG`.
67
+
68
+ During `sphinx-build` the extension:
69
+
70
+ 1. **Harvests** the resolved Sphinx AST (`doctree-resolved`) into structural,
71
+ section-boundary chunks with breadcrumbs and resolved `:ref:`/`:doc:`/`:term:`
72
+ cross-references.
73
+ 2. **Extracts** mounted multi-format files — PDF (page-level), spreadsheets
74
+ (`.xlsx`/`.ods` → Markdown tables), presentations (`.pptx` slide title + body
75
+ + speaker notes; `.keynote`/`.key` best-effort), and Word/OpenOffice
76
+ (`.docx`/`.odt` heading-aware).
77
+ 3. **Classifies** Jira/Confluence `xlink` targets by API-level access
78
+ restriction (allow-list: only affirmatively-unrestricted → `SHARED_ORG`).
79
+ 4. **Reconciles** every chunk's final scope through a single
80
+ `AccessControl.most_restrictive(...)` resolver and **ingests** it into the
81
+ correct ChromaDB store, addressed by a canonical `(tenant, database,
82
+ collection)` triple, with cache-aware incremental upserts and deletion
83
+ pruning.
84
+
85
+ At agent time, the Pi Agent TypeScript extension (shipped in the package at
86
+ `doxtr_rag/pi_extension/`, installed to `~/.pi/agent/extensions/chroma-rag`)
87
+ registers a
88
+ `search_knowledge_base` tool that queries **both** stores concurrently, merges
89
+ results with **Reciprocal Rank Fusion (RRF)**, scrubs credentials from returned
90
+ context, and **degrades gracefully** to local-only results when the shared store
91
+ is unreachable.
92
+
93
+ All embeddings use a pinned `bge-m3` model standardized on **1024-dim** dense
94
+ vectors; parity (same model + dimension) is enforced across ingestion (Python)
95
+ and query (TypeScript).
96
+
97
+ ## 2. Installation
98
+
99
+ Requirements:
100
+
101
+ - **Python ≥ 3.10**
102
+ - The **`doxtr/reactor`** devcontainer (`docker.io/doxtr/reactor:0.1.5`) — run
103
+ all Python/pytest/mypy and Node/tsc/vitest commands inside it.
104
+ - Node ≥ 20 (bundled in the devcontainer) for the TypeScript extension.
105
+ - The pinned **`bge-m3` (1024-dim) ONNX weights** (~2.3 GB). In the
106
+ `doxtr/reactor` image these are **pre-baked** at `/opt/models/bge-m3`
107
+ (`DOXTR_RAG_BGE_M3_ONNX_DIR` is set for you). Elsewhere, download them once
108
+ with the built-in, Python-only command (no shell/curl needed):
109
+
110
+ ```bash
111
+ doxtr-rag fetch-weights # -> $DOXTR_RAG_BGE_M3_ONNX_DIR or /opt/models/bge-m3
112
+ ```
113
+
114
+ If the weights are absent at build/query time, doxtr-rag **fails with a clear
115
+ error naming `doxtr-rag fetch-weights`** rather than silently substituting a
116
+ different model (that would break embedding parity).
117
+
118
+ Install the Python package (editable, with dev extras):
119
+
120
+ ```bash
121
+ pip install -e .[dev]
122
+ ```
123
+
124
+ The recommended way to install the query extension is the CLI (it copies the
125
+ packaged extension into your Pi agent dir and wires the reactor defaults):
126
+
127
+ ```bash
128
+ doxtr-rag --install-agent pi
129
+ ```
130
+
131
+ To work on the TypeScript extension source directly (in a checkout):
132
+
133
+ ```bash
134
+ cd doxtr_rag/pi_extension && npm install
135
+ ```
136
+
137
+ Enable the extension in your `conf.py`:
138
+
139
+ ```python
140
+ extensions = [
141
+ # ...
142
+ "doxtr_rag",
143
+ ]
144
+ ```
145
+
146
+ ## 3. Configuration reference
147
+
148
+ All keys are registered by `doxtr_rag.setup(app)` and parsed by
149
+ `doxtr_rag.config.SphinxRAGConfig`. Set them in your Sphinx `conf.py`.
150
+
151
+ ### The six original keys
152
+
153
+ | key | type | default | effect |
154
+ |-----|------|---------|--------|
155
+ | `rag_shared_store_uri` | `str \| None` | `None` | Shared store location. In `http` mode a scheme-qualified URI (`http(s)://host:port`); in `embedded` mode an on-disk path. Secrets are **never** embedded here (env-sourced). |
156
+ | `rag_local_store_path` | `str` | `.doxtr/chroma` | Embedded on-disk path for the `LOCAL_PRIVATE` store. |
157
+ | `rag_external_mounts` | `list[dict]` | `[]` | Multi-format file mounts to scan. Each entry is `{"path": "...", "scope": "SHARED_ORG"?}`. Content defaults to `LOCAL_PRIVATE` unless the mount **affirmatively** declares `"scope": "SHARED_ORG"`. |
158
+ | `rag_cache_db` | `str` | `.doxtr/cache.db` | SQLite incremental-build cache (three SHA-256 hash domains: nodes, external files, xlink payloads). |
159
+ | `rag_audit_log_path` | `str` | `.doxtr/security_audit.jsonl` | Append-only, `0600` JSON Lines security audit log. |
160
+ | `rag_embedding_provider` | `str` | `bge-m3` | Embedding provider/revision token. |
161
+
162
+ ### ChromaDB deployment keys
163
+
164
+ | key | type | default | effect |
165
+ |-----|------|---------|--------|
166
+ | `rag_shared_store_mode` | `"embedded" \| "http"` | `http` | How the `SHARED_ORG` store is reached. Independent of the local store. |
167
+ | `rag_local_store_mode` | `"embedded" \| "http"` | `embedded` | How the `LOCAL_PRIVATE` store is reached. |
168
+ | `rag_shared_store_database` | `str` | `doxtr_shared` | ChromaDB database name for the shared scope. |
169
+ | `rag_local_store_database` | `str` | `doxtr_private` | ChromaDB database name for the private scope. |
170
+ | `rag_store_tenant` | `str` | `default_tenant` | ChromaDB tenant shared by both scopes. |
171
+ | `rag_local_server_uri` | `str \| None` | `None` | URI for the `LOCAL_PRIVATE` store when `rag_local_store_mode = "http"`. |
172
+
173
+ ### Derived (exposed for parity enforcement)
174
+
175
+ | key | value | effect |
176
+ |-----|-------|--------|
177
+ | `embedding_model` | `BAAI/bge-m3` | Pinned dense model. |
178
+ | `embedding_dimension` | `1024` | Fatal, build-stopping error on any mismatch. |
179
+
180
+ ### Per-mount `SHARED_ORG` declaration & fail-closed defaults
181
+
182
+ A mount is shared **only** when it declares it explicitly:
183
+
184
+ ```python
185
+ rag_external_mounts = [
186
+ {"path": "/data/handbook"}, # -> LOCAL_PRIVATE (default)
187
+ {"path": "/data/public-specs", "scope": "SHARED_ORG"}, # -> SHARED_ORG (affirmative)
188
+ ]
189
+ ```
190
+
191
+ Path routing is deny-list-before-allow-list: any resolved path containing
192
+ `noter/` or `NDA/` always routes `LOCAL_PRIVATE` regardless of AST hints, only
193
+ `source/` (and explicitly-shared mounts) map to `SHARED_ORG`, and every path is
194
+ resolved with `realpath` + confinement so `source/../NDA/x` traversal and
195
+ symlink laundering fail closed.
196
+
197
+ ### HTTP store auth (env-sourced, never in `conf.py`)
198
+
199
+ Connection secrets for `http` stores come from the environment as `k=v;k=v`
200
+ header pairs, never from the URI or `conf.py`:
201
+
202
+ ```bash
203
+ export DOXTR_RAG_SHARED_STORE_HEADERS="Authorization=Bearer $SHARED_TOKEN"
204
+ export DOXTR_RAG_LOCAL_STORE_HEADERS="Authorization=Bearer $LOCAL_TOKEN"
205
+ ```
206
+
207
+ The resolved URI/headers are scrubbed before any log line.
208
+
209
+ ## 4. ChromaDB deployment guide
210
+
211
+ Each store can run **embedded** (on-disk) or against an **external HTTP server**,
212
+ independently.
213
+
214
+ ### Embedded (on-disk)
215
+
216
+ ```python
217
+ rag_local_store_mode = "embedded"
218
+ rag_local_store_path = ".doxtr/chroma"
219
+ rag_shared_store_mode = "embedded"
220
+ rag_shared_store_uri = "/srv/doxtr/chroma_shared" # embedded path in embedded mode
221
+ ```
222
+
223
+ Because the Pi Agent (JS) client cannot read an embedded on-disk store directly,
224
+ the build starts a **loopback-bound Chroma HTTP server** over each embedded
225
+ store's path; both ingestion and query then go through that HTTP endpoint (one
226
+ process — the server — owns the on-disk store). The resolved per-scope endpoints
227
+ are written to `.doxtr/endpoints.json` for the query extension to read.
228
+
229
+ ### External HTTP server (worked example)
230
+
231
+ Run a persistent `chromadb/chroma` server in Docker:
232
+
233
+ ```bash
234
+ docker run -d --name doxtr-chroma \
235
+ -p 8000:8000 \
236
+ -v /srv/doxtr/chroma-data:/data \
237
+ -e CHROMA_SERVER_AUTHN_CREDENTIALS="$CHROMA_TOKEN" \
238
+ -e CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.token_authn.TokenAuthenticationServerProvider" \
239
+ chromadb/chroma:latest
240
+ ```
241
+
242
+ Or via `docker-compose.yml`:
243
+
244
+ ```yaml
245
+ services:
246
+ chroma:
247
+ image: chromadb/chroma:latest
248
+ ports:
249
+ - "8000:8000"
250
+ volumes:
251
+ - /srv/doxtr/chroma-data:/data # persistent volume
252
+ environment:
253
+ CHROMA_SERVER_AUTHN_CREDENTIALS: ${CHROMA_TOKEN}
254
+ CHROMA_SERVER_AUTHN_PROVIDER: chromadb.auth.token_authn.TokenAuthenticationServerProvider
255
+ restart: unless-stopped
256
+ ```
257
+
258
+ > **TLS:** for any non-loopback remote, terminate TLS (a reverse proxy or
259
+ > `--ssl`) and use an `https://` URI so `StoreTarget.ssl` is `true`.
260
+
261
+ Point the stores at it in `conf.py`:
262
+
263
+ ```python
264
+ rag_shared_store_mode = "http"
265
+ rag_shared_store_uri = "https://chroma.internal.example.com:8000"
266
+ rag_local_store_mode = "http"
267
+ rag_local_server_uri = "https://chroma.internal.example.com:8000"
268
+ ```
269
+
270
+ with the auth token supplied via env (see §3).
271
+
272
+ ### Single server hosting both scopes (separate databases, one tenant)
273
+
274
+ A single external server MAY host both scopes as separate **databases** under
275
+ one **tenant** — physical isolation is enforced at the resolved
276
+ database/collection boundary, not merely by process:
277
+
278
+ ```python
279
+ rag_store_tenant = "default_tenant"
280
+ rag_shared_store_mode = "http"
281
+ rag_shared_store_uri = "https://chroma.internal.example.com:8000"
282
+ rag_shared_store_database = "doxtr_shared"
283
+ rag_local_store_mode = "http"
284
+ rag_local_server_uri = "https://chroma.internal.example.com:8000"
285
+ rag_local_store_database = "doxtr_private"
286
+ ```
287
+
288
+ The shared-store adapter raises on any non-`SHARED_ORG` chunk addressed to the
289
+ shared database, so `LOCAL_PRIVATE` vectors can never reach the shared scope even
290
+ when both live on one host.
291
+
292
+ ## 5. Shared-store writability
293
+
294
+ Whether the shared store is ingested into is decided by a **runtime writability
295
+ probe** of the actual mount/endpoint — **not** by the store mode:
296
+
297
+ - **`rw` mount / write-accepting server → locally writable:** the build ingests
298
+ `source/` (and shared mounts) into the shared store in place.
299
+ - **`ro` mount / read-only server → query-only:** shared ingestion is **skipped
300
+ gracefully** with an `[INFO]` message and the store stays fully queryable.
301
+ Never a hard error.
302
+
303
+ The `LOCAL_PRIVATE` store (`.doxtr/chroma`) is always read-write.
304
+
305
+ ### `.devcontainer` mount options (requirement #22)
306
+
307
+ Mount the shared Chroma data directory `rw` for local shared ingestion or `ro`
308
+ for query-only nodes:
309
+
310
+ ```jsonc
311
+ // .devcontainer/devcontainer.json
312
+ "mounts": [
313
+ // read-only (query-only node): shared ingestion is skipped, still queryable
314
+ "source=/srv/doxtr/chroma_shared,target=/workspaces/docs/.doxtr/chroma_shared,type=bind,readonly"
315
+ // ...or omit ",readonly" for a read-write (ingesting) node.
316
+ ]
317
+ ```
318
+
319
+ For an external `chromadb/chroma` server, apply the same `rw`/`ro` policy to
320
+ that container's **own** persistent volume instead of a bind mount.
321
+
322
+ ## 6. Pi Agent extension setup
323
+
324
+ The extension source ships inside the package at `doxtr_rag/pi_extension/`
325
+ (single source of truth). Install it into your Pi Agent config with the CLI —
326
+ it copies the packaged extension and wires reactor defaults:
327
+
328
+ ```bash
329
+ doxtr-rag --install-agent pi
330
+ ```
331
+
332
+ This populates `~/.pi/agent/extensions/chroma-rag/` (a subdirectory whose
333
+ `package.json` `pi.extensions` manifest makes Pi load **only** `chroma-rag.ts`
334
+ and ignore the helper modules `addressing.ts`, `rrf.ts`, `scrub.ts`,
335
+ `xlang-query.ts`). Works from a plain `pip install` — no source checkout needed.
336
+
337
+ The extension registers the **`search_knowledge_base`** tool:
338
+
339
+ ```
340
+ search_knowledge_base({ query: string, top_k?: number }) -> Markdown context blocks
341
+ ```
342
+
343
+ Behavior:
344
+
345
+ - Reads the resolved per-scope endpoints from `.doxtr/endpoints.json` (or the
346
+ path in `DOXTR_RAG_ENDPOINTS`) — no addressing is re-derived, no loopback port
347
+ is guessed.
348
+ - Queries both stores **concurrently** (`Promise.allSettled`), embeds the query
349
+ with the same pinned `bge-m3`/1024 model as ingestion (dimension **and** model
350
+ parity asserted before any query), and merges with **RRF** (`1/(k+rank)`,
351
+ rank-based, distances ignored, identical chunks deduped).
352
+ - Returns credential-scrubbed Markdown context blocks (the TS `scrub()` mirrors
353
+ the Python pattern set verbatim).
354
+ - **Offline fallback:** if the shared store is unreachable or times out, returns
355
+ local-only results — no crash, no unhandled rejection, no stack traces in LLM
356
+ context.
357
+
358
+ **KB-first default.** The tool ships with `promptSnippet` + `promptGuidelines`
359
+ that instruct the agent to consult `search_knowledge_base` **before** answering
360
+ factual questions on *any* subject — because your knowledge base may hold
361
+ authoritative, domain-specific, or more recent information than the model's
362
+ training data (your own docs, an indexed website, product specs, or whatever you
363
+ ingested). The guidance is domain-agnostic and also tells the agent to cite the
364
+ returned sources, to fall back to general knowledge when results are empty or
365
+ unrelated (never fabricating a citation), and to treat the KB as only as fresh
366
+ as its last build (reconcile rather than blindly trust). This is a strong
367
+ default, not a hard gate — tool use is agent-invoked, so a model can still skip
368
+ it; for a hard guarantee you would add a harness-level turn hook.
369
+
370
+ **Query-time embedding.** The query side must embed with the same pinned
371
+ `bge-m3`/1024 model as ingestion. By default this is **automatic**: when a build
372
+ runs with the extension enabled, the pipeline auto-starts a loopback `bge-m3`
373
+ embed server (serving the same weights as ingestion) and publishes its URL into
374
+ `.doxtr/endpoints.json` (`embedding.embed_url`). The query extension reads that
375
+ URL — no manual endpoint or `DOXTR_RAG_EMBED_URL` needed for the common
376
+ embedded deployment.
377
+
378
+ For deployments where the embed endpoint runs elsewhere, start it yourself and
379
+ point the extension at it (overrides the auto-started one):
380
+
381
+ ```bash
382
+ # standalone bge-m3 embed server (uses DOXTR_RAG_BGE_M3_ONNX_DIR weights)
383
+ doxtr-rag-embed-server --host 127.0.0.1 --port 8519 &
384
+ export DOXTR_RAG_EMBED_URL="http://127.0.0.1:8519/embed"
385
+ ```
386
+
387
+ ### One-command installer (reactor-tailored)
388
+
389
+ Inside the `doxtr/reactor` container, install the extension into a Pi agent with
390
+ defaults tailored to the container:
391
+
392
+ ```bash
393
+ doxtr-rag --install-agent pi # copy the packaged extension (default)
394
+ doxtr-rag --install-agent pi --symlink # or symlink to a dev checkout (live edits)
395
+ # if the console script isn't on PATH:
396
+ # python -m doxtr_rag.cli --install-agent pi
397
+ ```
398
+
399
+ The installer (`doxtr_rag.agent_install`, exercised by the test suite):
400
+
401
+ - copies (default) the packaged `doxtr_rag/pi_extension/` into `$PI_AGENT_DIR`
402
+ (default `~/.pi/agent`) under `extensions/chroma-rag` — or `--symlink` to a
403
+ writable dev checkout,
404
+ - runs `npm install` for the extension's Node deps at the destination,
405
+ - **adds** reactor defaults to `settings.json` without overwriting an existing
406
+ provider/model/auth choice,
407
+ - writes a sourceable `~/.pi/agent/doxtr-rag.env` with reactor defaults
408
+ (`DOXTR_RAG_BGE_M3_ONNX_DIR=/opt/models/bge-m3`,
409
+ `DOXTR_RAG_EMBED_URL=http://127.0.0.1:8519/embed`,
410
+ `DOXTR_RAG_ENDPOINTS=.doxtr/endpoints.json`), each overridable from the shell.
411
+
412
+ Then `source ~/.pi/agent/doxtr-rag.env` and start `pi`.
413
+
414
+ **Other agents.** `opencode`, `claude`, `cursor`, and `codex` do **not** consume
415
+ Pi's TypeScript `ExtensionAPI`, so the installer reports them as not-yet-supported
416
+ rather than faking a config copy. Exposing `search_knowledge_base` to them is a
417
+ planned follow-up via an **MCP server** wrapping the tool (opencode and Claude
418
+ Code speak MCP) or each agent's native plugin format.
419
+
420
+ ## 7. Security model
421
+
422
+ - **Fail-closed scope routing:** ambiguous/unverifiable → `LOCAL_PRIVATE`;
423
+ `SHARED_ORG` only from an affirmative signal. `AccessControl.most_restrictive`
424
+ is the single final-scope decision.
425
+ - **Path confinement:** every path resolved with `realpath` and asserted within
426
+ an allowed root; deny-list (`noter/`, `NDA/`) before allow-list (`source/`);
427
+ escaping symlinks are skipped fail-closed.
428
+ - **Credential scrubbing:** a single value-level scrubber (URLs-with-creds,
429
+ JWTs, `Bearer`, `ghp_`/`github_pat_`, `xox[baprs]-`, `AKIA`/`ASIA`, PEM keys)
430
+ runs over all chunk text, metadata values, audit records, and returned LLM
431
+ context — mirrored verbatim in TypeScript. A metadata-key deny-list adds
432
+ defense-in-depth.
433
+ - **Physical store isolation:** the shared-store adapter raises on any
434
+ non-`SHARED_ORG` chunk at the resolved database boundary (holds under retries,
435
+ errors, and the one-server-two-databases topology).
436
+ - **Resilience:** 5s connect / 10s read timeouts, ≤2 retries only on `5xx`/`429`
437
+ (never `401`/`403`), a per-build network budget, and fail-closed HTTP mapping.
438
+ - **Audit log:** restricted-`xlink` routing is recorded to
439
+ `.doxtr/security_audit.jsonl` (`0600`, scrubbed, one `[INFO]` per redirect).
440
+
441
+ ## 8. Maintenance CLI
442
+
443
+ ```bash
444
+ doxtr-rag fetch-weights # download the pinned bge-m3 ONNX weights
445
+ doxtr-rag fetch-weights --force # re-download even if present
446
+ doxtr-rag --conf conf.py --rebuild --scope all # drop & reindex collections
447
+ doxtr-rag --conf conf.py --rebuild --scope shared # or just one scope
448
+ doxtr-rag --conf conf.py cache-info # inspect the SQLite cache
449
+ doxtr-rag --conf conf.py audit-info # summarize the audit log
450
+ doxtr-rag --install-agent pi # install the query extension into Pi Agent
451
+ ```
452
+
453
+ Collections are tagged with the model revision + dimension; a model/dimension
454
+ change drops and rebuilds the affected collection.
455
+
456
+ ## 9. Development & QA
457
+
458
+ Run the full quality gate (what CI enforces):
459
+
460
+ ```bash
461
+ make qa
462
+ ```
463
+
464
+ `make qa` runs, and fails on any of:
465
+
466
+ - `pytest` with **≥ 90 %** coverage (`--cov-fail-under=90`), including the
467
+ Python↔TypeScript **cross-language integration round-trip** and the **dox
468
+ reference full-build regression** (a real `sphinx-build` of
469
+ `github.com/doxtr/dox` with the extension enabled, asserting the store is
470
+ populated through the real event lifecycle against a committed baseline);
471
+ - `mypy --strict` over `doxtr_rag/`;
472
+ - `tsc --noEmit --strict` over the Pi extension;
473
+ - `vitest` for the extension;
474
+ - the **extension-verifier** regression smoke check (the host `dox` HTML +
475
+ light/dark PDF build must still succeed within its warning baseline).
476
+
477
+ Individual targets: `make test`, `make mypy`, `make tsc`, `make tstest`,
478
+ `make xlang`, `make dox-build`, `make dox-pdf`, `make verifier`.
479
+
480
+ The **dox-build regression** (`make dox-build` /
481
+ `tests/test_dox_reference_build.py`) is the guard that tells you *if a new
482
+ documentation construct or extension breaks doxtr-rag*: it builds the whole
483
+ reference project and fails if ingestion drops below the baseline floors in
484
+ `tests/baselines/dox_reference.json`. Update that baseline only when the dox
485
+ project legitimately changes. The **dox-pdf regression** (`make dox-pdf` /
486
+ `tests/test_dox_pdf_ingest.py`) additionally compiles the reference project to a
487
+ real PDF (LaTeX → lualatex) and ingests it through the PDF extractor, exercising
488
+ the binary-document path against a real-world file. Both run in the
489
+ `doxtr/reactor` container (full extension stack + LaTeX + pre-baked bge-m3
490
+ weights) and skip gracefully elsewhere.
491
+
492
+ ### Test layout
493
+
494
+ - `tests/` — fast unit tests (schema, scrub, cache, harvester, extractors,
495
+ security, audit, storage, pipeline, config, embedding) **plus** the
496
+ cross-language round-trip.
497
+ - `test_harness/` — a real Sphinx project (`conf.py`, `source/` with
498
+ `source/`/`noter/`/`NDA/` mounts, `conf_overrides/`, `assertions.py`,
499
+ `test_runner.py`) for integration/regression, mirroring the reference
500
+ `doxtr-pdf-theme-core` harness.
501
+ - `doxtr_rag/pi_extension/tests/` — the TypeScript `vitest` suite (scrub
502
+ parity, RRF, metadata inflation, dimension/model parity, offline fallback).