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.
- doxtr_rag-0.1.0/LICENSE +21 -0
- doxtr_rag-0.1.0/PKG-INFO +502 -0
- doxtr_rag-0.1.0/README.md +455 -0
- doxtr_rag-0.1.0/doxtr_rag/__init__.py +98 -0
- doxtr_rag-0.1.0/doxtr_rag/_lifecycle.py +95 -0
- doxtr_rag-0.1.0/doxtr_rag/agent_install.py +248 -0
- doxtr_rag-0.1.0/doxtr_rag/audit.py +93 -0
- doxtr_rag-0.1.0/doxtr_rag/base_store.py +118 -0
- doxtr_rag-0.1.0/doxtr_rag/cache.py +165 -0
- doxtr_rag-0.1.0/doxtr_rag/cli.py +179 -0
- doxtr_rag-0.1.0/doxtr_rag/config.py +297 -0
- doxtr_rag-0.1.0/doxtr_rag/embed_server.py +117 -0
- doxtr_rag-0.1.0/doxtr_rag/embedding.py +86 -0
- doxtr_rag-0.1.0/doxtr_rag/embedding_provider.py +141 -0
- doxtr_rag-0.1.0/doxtr_rag/extractors.py +442 -0
- doxtr_rag-0.1.0/doxtr_rag/harvester.py +276 -0
- doxtr_rag-0.1.0/doxtr_rag/pathscope.py +81 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/addressing.ts +64 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/chroma-rag.ts +336 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/package-lock.json +4377 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/package.json +26 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/rrf.ts +46 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/scrub.ts +53 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/tests/test-extension.test.ts +249 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/tsconfig.json +24 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/vitest.config.ts +8 -0
- doxtr_rag-0.1.0/doxtr_rag/pi_extension/xlang-query.ts +72 -0
- doxtr_rag-0.1.0/doxtr_rag/pipeline.py +345 -0
- doxtr_rag-0.1.0/doxtr_rag/py.typed +0 -0
- doxtr_rag-0.1.0/doxtr_rag/schema.py +237 -0
- doxtr_rag-0.1.0/doxtr_rag/scrub.py +91 -0
- doxtr_rag-0.1.0/doxtr_rag/security.py +350 -0
- doxtr_rag-0.1.0/doxtr_rag/server.py +112 -0
- doxtr_rag-0.1.0/doxtr_rag/storage.py +497 -0
- doxtr_rag-0.1.0/doxtr_rag/weights.py +132 -0
- doxtr_rag-0.1.0/doxtr_rag.egg-info/PKG-INFO +502 -0
- doxtr_rag-0.1.0/doxtr_rag.egg-info/SOURCES.txt +61 -0
- doxtr_rag-0.1.0/doxtr_rag.egg-info/dependency_links.txt +1 -0
- doxtr_rag-0.1.0/doxtr_rag.egg-info/entry_points.txt +3 -0
- doxtr_rag-0.1.0/doxtr_rag.egg-info/requires.txt +19 -0
- doxtr_rag-0.1.0/doxtr_rag.egg-info/top_level.txt +1 -0
- doxtr_rag-0.1.0/pyproject.toml +127 -0
- doxtr_rag-0.1.0/setup.cfg +4 -0
- doxtr_rag-0.1.0/tests/test_agent_install.py +131 -0
- doxtr_rag-0.1.0/tests/test_audit.py +104 -0
- doxtr_rag-0.1.0/tests/test_base_store.py +99 -0
- doxtr_rag-0.1.0/tests/test_cache.py +73 -0
- doxtr_rag-0.1.0/tests/test_config.py +188 -0
- doxtr_rag-0.1.0/tests/test_cross_language.py +381 -0
- doxtr_rag-0.1.0/tests/test_dox_pdf_ingest.py +121 -0
- doxtr_rag-0.1.0/tests/test_dox_reference_build.py +90 -0
- doxtr_rag-0.1.0/tests/test_embed_server.py +85 -0
- doxtr_rag-0.1.0/tests/test_embedding.py +40 -0
- doxtr_rag-0.1.0/tests/test_extractors.py +208 -0
- doxtr_rag-0.1.0/tests/test_harvester.py +264 -0
- doxtr_rag-0.1.0/tests/test_init.py +99 -0
- doxtr_rag-0.1.0/tests/test_lifecycle.py +89 -0
- doxtr_rag-0.1.0/tests/test_pipeline.py +555 -0
- doxtr_rag-0.1.0/tests/test_schema.py +167 -0
- doxtr_rag-0.1.0/tests/test_scrub.py +76 -0
- doxtr_rag-0.1.0/tests/test_security.py +241 -0
- doxtr_rag-0.1.0/tests/test_storage.py +250 -0
- doxtr_rag-0.1.0/tests/test_weights.py +122 -0
doxtr_rag-0.1.0/LICENSE
ADDED
|
@@ -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.
|
doxtr_rag-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|