16 KiB
Documents, RAG, And Uploads
Last updated: dev@df2fad2 | 2026-07-12
Scope
This spec covers file/document context, document storage, and vector retrieval in:
app.pyandsrc/app_initializer.pyroute/manager wiring;routes/upload_routes.py,routes/personal_routes.py,routes/embedding_routes.py,routes/document_routes.py, androutes/document_helpers.py;- chat attachment paths in
routes/chat_routes.py,routes/chat_helpers.py,src/chat_handler.py, andsrc/chat_processor.py; core/session_manager.py,src/attachment_refs.py,src/upload_handler.py,src/upload_limits.py, and the public reference contract indocs/attachments.md;src/document_processor.py,src/document_actions.py,src/personal_docs.py, andsrc/markitdown_runtime.py;src/rag_singleton.py,src/rag_vector.py,src/rag_manager.py,src/chroma_client.py,src/embeddings.py, andsrc/embedding_lanes.py;- PDF/form helpers in
src/pdf_runtime.py,src/pdf_forms.py, andsrc/pdf_form_doc.py; services/docs/service.py;- document, upload, RAG, chat, email, and admin frontend callers in
static/app.js,static/js/chat.js,static/js/chatRenderer.js,static/js/fileHandler.js,static/js/document.js,static/js/documentLibrary.js,static/js/rag.js,static/js/admin.js,static/js/emailInbox.js, andstatic/js/slashCommands.js; - tests covering upload, document, attachment, PDF, RAG, Chroma, MarkItDown, and embedding behavior.
Runtime Integration
app.py registers upload, personal-doc/RAG, embedding, document, diagnostics, and Codex document routes. src.app_initializer.initialize_managers() creates UploadHandler and PersonalDocsManager, installs the upload handler on SessionManager and the shared tool helper, and startup attempts to initialize the RAG singleton. App route wiring passes that same handler to session/history, document, note, and calendar writers that can persist upload references.
src.rag_singleton.get_rag_manager() returns the live VectorRAG instance when Chroma/embedding dependencies are reachable. Personal routes can retry the singleton and return explicit 503s when unavailable. Chat RAG uses the PersonalDocsManager.rag_manager captured during app initialization and can silently skip RAG if that manager is absent.
Uploads And Attachments
src.upload_handler.UploadHandler owns upload IDs, safe filenames, upload metadata, owner rename rewrites, atomic uploads.json writes, content-type detection, and file storage under data/uploads. Upload IDs accept extensionless values or one sanitized alphanumeric extension.
src.upload_limits owns central upload-size caps and environment overrides for chat attachments, gallery, transforms, memory import, personal uploads, email compose, STT audio, and ICS imports. Invalid configured limits fail fast at import so routes do not silently accept unsafe sizes. Docker installs libmagic1 plus python-magic so UploadHandler.detect_content_type() can sniff bytes in the official image; native installs can fall back to extension/MIME guesses when python-magic is unavailable.
routes/upload_routes.py owns:
POST /api/upload, returning uploaded file metadata;- reference-aware admin upload cleanup and stats;
GET /api/upload/{file_id};GET/PUT /api/upload/{file_id}/visionfor editable OCR/vision cache;- thumbnail and masked owner/admin access behavior.
It does not currently expose a general upload list/delete route. Download/preview responses that serve uploaded content should include X-Content-Type-Options: nosniff where route code owns the response so browser MIME sniffing does not widen accepted upload types.
Readable/code-like upload handling includes common text/code extensions plus .nix; document processing renders recognized code-like text into fenced blocks with language metadata.
Chat does not own attachment extraction. Runtime flow:
- the frontend uploads files and submits attachment IDs;
ChatHandler.preprocess_message()resolves IDs with the session owner throughUploadHandler.resolve_upload(), which enforces owner/admin access and no longer treats missing owner context as permission to read owned uploads;- vision/OCR cache and attachment metadata are prepared before model calls;
- text-only models receive stripped multimodal blocks;
src.document_processor.build_user_content()produces model-ready text, PDF text, Office/EPUB text when MarkItDown or the DOCX fallback is available, image/multimodal blocks, truncation, and PDF/Office auto-document updates;- chat streams attachment, PDF-created
doc_update, andrag_sourcesevents where applicable.
Extensionless image and audio attachments derive their data-URI subtype from
the detected MIME type, so image/png and audio/mpeg uploads do not become
invalid data:image/;base64 or data:audio/;base64 blocks when the filename
has no extension.
Durable References And Cleanup
src.attachment_refs owns the stable attachment_ref shape used outside raw
upload storage: attachment id, name, MIME type, size, and optional checksum,
creation time, dimensions, vision text/model, and gallery id. Live provider
calls may still receive multimodal data URLs for the current turn, but durable
chat content is normalized to readable text plus compact reference lines.
Structured references remain in message attachment metadata, and chat FTS
triggers omit inline media while startup migration scrubs legacy indexed data
URLs.
Agent/tool manifests expose odysseus://attachment/<id> with
read_policy: "owner_checked_upload". A compatibility filesystem path is
included only after owner-aware upload resolution, upload-root confinement, and
tool-readable-root checks; the stable contract for external tools is the URI
and attachment id, not host layout.
Writers reserve referenced uploads before committing durable state. This includes session message append/replace and history rewrites, document create/update and native document edits, note route/tool create/update, calendar/event route/tool create/update, and attachment-bearing session updates. A missing or wrong-owner reference aborts before destructive replacement and surfaces a route conflict or tool error. Reservations serialize with cleanup through the upload-index lock and refresh access time.
Admin cleanup first scans chat content and attachment metadata, current and versioned documents including PDF markers, gallery filenames/hashes, note image/color/content/checklist fields, and calendar color/description/location fields. Reference discovery or index-integrity failure aborts cleanup; the lower-level API removes nothing without both completed id and hash snapshots. Only expired, unreferenced files with coherent id/path/owner/checksum/timestamp metadata are candidates. Matching index rows are persisted away before byte deletion and restored if deletion fails. This lock is process-local, so the documented race protection assumes the current single-worker deployment.
Living Documents And PDF
routes/document_routes.py owns the HTTP document API: create/read/update/archive/delete, library listing, import/export, version history, tidy/AI tidy, PDF rendering/export, PDF form helpers, and email-attachment reply preparation.
static/js/documentLibrary.js owns local library state after archive/delete actions, including total counts and language chips. Server route truth still owns durable document state.
static/js/document.js owns the browser document editor and markdown preview. Preview rendering applies code highlighting when highlight.js is present, renders Mermaid diagrams when the Mermaid runtime is available, refreshes after AI edits, and discards pending AI diffs before switching the active document.
Document mutations also happen through agent tools, Codex document routes, email attachment import, and scripts. HTTP and native-agent document writers owner-reserve any internal upload/PDF references before persisting new current content or versions. Native document tool outputs include metadata that the browser can use to open/update the editor if a later stream update is missed. Those callers must preserve document owner, attachment, and version semantics.
Email draft documents are a first-class document language. Create/update paths
detect the To/Subject/header shape, coerce language to email, and preserve
protected reply/forward headers such as In-Reply-To, References,
X-Source-UID, X-Source-Folder, attachment headers, and quoted/original
history when model or UI edits replace the draft body. Creating a draft for the
same source UID/folder in the same session updates the active draft instead of
creating a duplicate.
Document rows own current content and owner. DocumentVersion rows own immutable snapshots. Document access should be owner-filtered, not session-id-only; the session document listing path still needs regression coverage for per-document owner filtering after the session owner check.
PDF runtime behavior:
- direct PDF import stores the upload through
UploadHandler; - PDF library entries preserve metadata/preview behavior for source PDFs;
- pypdf text extraction remains core;
- PyMuPDF enables form detection, page rendering, page PNGs, annotation fill, render/export PDF, and form filling;
- PDF render routes should return a shaped 503 when PyMuPDF is absent and use same-origin framing/download behavior for rendered pages;
- imported PDFs become either plain
pdf_sourcemarkdown orpdf_form_sourcemarkdown with sidecar field data; - PDF markers must resolve back through an upload owned by the caller;
- signed-reply preparation uses document
source_email_*provenance and verifies the document owner and signature owner. Source email account resolution still needs explicit owner-scoped coverage.
Office/EPUB attachment extraction is optional and MarkItDown-backed for .docx, .pptx, .xlsx, .xls, and .epub; a pure-Python DOCX fallback can extract word/document.xml. When a session id is present, full extraction can be saved as a markdown Document while the chat-inline copy remains capped.
Personal Docs And RAG
src.personal_docs.PersonalDocsManager owns personal-directory indexing and keyword retrieval.
src.rag_vector.VectorRAG owns Chroma/embedding-backed indexing and owner-filtered retrieval. Chunk ids are owner-scoped so byte-identical chunks from different owners do not suppress each other. src.rag_singleton owns lazy initialization, retry throttling, and reset behavior.
routes/personal_routes.py owns personal-doc and direct RAG-upload routes. Directory list/index/delete routes are admin-gated. Direct RAG upload is user-authenticated, requires document privilege, forwards owner into the manager wrapper, writes unique files under per-owner subdirectories of data/personal_uploads, and has looser file-type validation than normal uploads.
Current call sites include:
- admin RAG pages and slash commands;
- chat RAG preface building;
- AI interaction and MCP RAG management tools;
- CLI scripts for document/personal indexing.
Some non-route tool/script paths can index ownerless or arbitrary directories and should be treated as compatibility-sensitive management surfaces.
Embedding Models
routes/embedding_routes.py owns admin-gated embedding model and custom endpoint management. It validates custom endpoints with outbound URL checks, can persist and process-expose EMBEDDING_API_KEY, resets embedding/RAG/tool-index/Chroma state, and does not own document extraction.
src.embeddings owns HTTP embedding fallback to FastEmbed and process-level endpoint state. src.embedding_lanes keeps custom HTTP embedding vectors separate from FastEmbed fallback vectors with lane-specific Chroma collections, migrates legacy unsuffixed collections into empty lanes, and dedupes query results across lanes. src.chroma_client owns native Chroma defaults and fast reachability checks.
Compatibility State
src.rag_manager.RAGManager is a backward-compat wrapper. The live owner-aware vector path is VectorRAG.
services/docs/service.py is a separate facade and currently has result-shape drift from VectorRAG: it maps legacy text/content and indexed/failed keys while the live vector path returns document/similarity and indexed_count/failed_count.
src.database re-exports core.database; document models and migrations live in core.database.
Optional And Degraded Behavior
- ChromaDB/FastEmbed are default installed dependencies, but Chroma can be offline or unreachable.
- Native Chroma defaults to
localhost:8100; Docker uses thechromadb:8000compose service and persistent Chroma storage. - HTTP embeddings can fall back to FastEmbed; when both lanes exist, lane separation avoids Chroma dimension conflicts.
- MarkItDown is optional for Office/EPUB extraction; chat attachments and personal directory indexing have clear degraded behavior, while direct RAG upload does not share the same extraction path.
- PyMuPDF is optional, unlocks PDF form/render/fill paths, and carries AGPL implications when installed.
- PyMuPDF-dependent document routes should use the shared runtime helper/error text so missing-dependency and license policy stay visible.
- pypdf text extraction is core and should remain available without PyMuPDF.
Security And Provenance
Uploaded files, documents, RAG chunks, extracted attachment text, OCR/vision text, PDF marker content, and source-email metadata are untrusted external or user-provided context when sent to an LLM.
Concrete enforcement points include:
UploadHandler.resolve_upload()for upload ID validation, owner/admin access, and upload-dir confinement;- owner-checked write reservations before durable attachment references are stored, sharing the upload-index lock with reference-aware cleanup;
- PDF marker ownership checks before resolving source uploads;
- personal-directory and personal-upload confinement helpers, including symlink/realpath checks before deleting uploaded files or removing indexed directories;
- owner-filtered
VectorRAG.search(owner=...); - shared untrusted-context wrappers for RAG preface insertion.
Extracted attachment text is currently appended into the user message rather than wrapped as a separate untrusted-context message. That is current behavior and a prompt-injection hardening gap.
Bearer-token callers are not a scoped document/upload API surface today. Routes that treat token-authenticated users as owners need explicit scope/effective-user policy before they are considered safe token APIs.
Testing Coverage
Existing useful coverage includes upload owner scope, upload IDs, upload atomicity, durable attachment reference normalization, message/document/note/calendar write reservations, fail-closed reference-aware cleanup, attachment budgets, .nix text upload handling, upload/PDF security regressions, Docker libmagic/python-magic upload detection, RAG owner fallback, Chroma fast-fail, MarkItDown runtime, PDF runtime, document-library counter updates, and selected document helper behavior.
Route-level coverage is thinner for document CRUD, PDF import/render/export/fill, direct RAG upload, embedding admin/security behavior, and RAG unavailable states.
Current Gaps
- Direct RAG upload still needs clearer file-type validation and MarkItDown/PDF extraction parity decisions.
- Document
session_idrelinking and session document listing need owner-scope regressions. services/docs/service.pyreturn-shape mapping is stale relative toVectorRAG.- Chat RAG can remain degraded after startup even if personal routes later initialize the RAG singleton.
- PyMuPDF-dependent routes do not all share the same optional-runtime helper/error behavior.
- Signed-reply preparation needs owner-scoped source email account/signature regression coverage.
- Document/upload routes need explicit bearer-token scope/effective-user policy.
- User-facing document/PDF/RAG route matrices need more regression coverage for owner denial, admin gates, unavailable services, and degraded optional dependencies.