Security — Architecture, Policies & Best Practices
1. Security principles
Local-first by design
Documents never leave your machine by default.
- Local mode — 100% on-device. Models run on your GPU/CPU. No network calls.
- API mode — Only the tiny piece of text needed for the answer is sent to the provider. Your full files never upload.
Zero telemetry
- No usage tracking
- No analytics
- No crash reporting to external servers
- No phone-home behavior
Open source
- Full source code available for audit
- No obfuscated code
- Dependencies are well-known, vetted libraries
2. Content Security Policy (CSP)
The app enforces a strict CSP in production to prevent XSS, code injection, and unauthorized resource loading.
CSP rules (production build)
html<meta
http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' http://localhost:* ws://localhost:*;
object-src 'none';
base-uri 'self';
form-action 'self';"
/>
What it prevents
| Directive | What it blocks |
|---|---|
script-src 'self' | ❌ Inline <script> tags<br>❌ eval(), new Function()<br>❌ External scripts from CDNs |
style-src 'self' 'unsafe-inline' | ⚠️ Allows inline styles (needed for Tailwind)<br>✅ Blocks external stylesheets |
connect-src 'self' http://localhost:* | ❌ Requests to external domains<br>✅ Allows backend (8765) + Vite HMR (5173) |
object-src 'none' | ❌ <object>, <embed>, <applet> |
Development mode relaxations
During development (npm run dev), CSP allows:
script-src 'self' 'unsafe-inline'— React Fast Refresh needs inline scriptsconnect-src ... ws://localhost:*— Vite HMR websocket
Production builds never allow unsafe-inline for scripts or unsafe-eval.
CSP injection (automated)
CSP is injected into dist/index.html during build by a Vite plugin:
File: frontend/vite.config.ts
typescriptfunction contentSecurityPolicy(): Plugin {
return {
name: 'inject-csp',
transformIndexHtml(html) {
const isDev = process.env.NODE_ENV === 'development';
const csp = isDev
? "default-src 'self'; script-src 'self' 'unsafe-inline'; ..."
: "default-src 'self'; script-src 'self'; ...";
return html.replace(
'<head>',
`<head>\n <meta http-equiv="Content-Security-Policy" content="${csp}">`
);
},
};
}
CSP regression guard
A CI check (frontend/scripts/check-csp.mjs) fails the build if:
dist/index.htmlis missing the CSP<meta>tag- Production CSP contains
unsafe-eval
Run locally:
bashcd frontend
npm run build
npm run check:csp
CI workflow (.github/workflows/ci.yml):
yaml- name: Check CSP
run: |
cd frontend
npm run build
npm run check:csp
A CSP violation breaks the build — the security posture can't regress silently.
3. Electron security model
Renderer sandbox
The renderer process runs in a strict sandbox with no Node.js access:
File: frontend/electron/src/main/window.ts
typescriptconst win = new BrowserWindow({
webPreferences: {
sandbox: true, // ✅ Renderer isolated from OS
contextIsolation: true, // ✅ Renderer can't touch Electron APIs
nodeIntegration: false, // ✅ No require() in renderer
preload: path.join(__dirname, 'preload.js'),
},
});
What sandboxing prevents
| Attack | How it's prevented |
|---|---|
| Arbitrary file access | Renderer has no fs module access |
| OS command execution | Renderer has no child_process access |
| Electron API abuse | contextIsolation=true isolates APIs |
| Node.js module loading | nodeIntegration=false disables require() |
Preload script (controlled bridge)
The preload script (preload.ts) exposes only safe, controlled IPC:
typescriptcontextBridge.exposeInMainWorld('electron', {
// Safe IPC — main process validates all requests
send: (channel: string, data: any) => ipcRenderer.send(channel, data),
invoke: (channel: string, data: any) => ipcRenderer.invoke(channel, data),
on: (channel: string, func: Function) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
});
Renderer cannot call arbitrary Node.js APIs — only the pre-defined IPC channels.
No --no-sandbox flag
The main process never uses the --no-sandbox Chromium flag (which disables the sandbox entirely). Using it anywhere in the codebase breaks the security check.
Electron security regression guard
A CI check (frontend/electron/scripts/check-electron-security.mjs) fails the build if:
- Any
BrowserWindowdoesn't setsandbox: true,contextIsolation: true, andnodeIntegration: falseexplicitly - The
--no-sandboxflag appears anywhere in the main process sources
Run locally:
bashcd frontend/electron
npm run build
npm run check:security
CI workflow:
yaml- name: Check Electron Security
run: |
cd frontend/electron
npm run build
npm run check:security
An insecure webPreferences configuration breaks the build.
4. API key handling
Storage
API keys (backend/api_config.json) are:
- ✅ Git-ignored — never committed to the repository
- ✅ File-permission protected — readable only by the user running the app
- ✅ Never logged — keys are redacted from all log output
- ✅ Never echoed back — the
/api/providers/statusendpoint returns"api_key": "[redacted]", not the real key
Transmission
- Keys are sent only to the configured provider's API endpoint (OpenAI, Anthropic, etc.)
- Keys are never sent to any other domain
- All API provider communication is over HTTPS (TLS-encrypted)
User control
The launcher's API provider card:
- ✅ Shows saved keys as
✓ <provider>chips (key is never displayed) - ✅ Allows deleting a saved key (removes it from
api_config.json) - ✅ Test connection validates the key without persisting it
5. File upload security
Upload constraints
| Constraint | Limit | Enforcement |
|---|---|---|
| Max files per message | 5 | Frontend pre-check + backend validation |
| Max folder files | 50 | Frontend file picker filter |
| Scan upload size cap | 50 MB | Backend /api/scan/upload rejects larger |
| Supported extensions only | .pdf, .docx, .txt, .png, .jpg, ... | Backend SUPPORTED_EXTENSIONS filter |
Path validation
Uploaded files are saved to the active workspace root — never to arbitrary paths supplied by the user:
python# Backend: main.py
@app.post("/api/upload")
async def upload_file(file: UploadFile):
root = get_workspace_root()
if root is None:
raise HTTPException(400, "No workspace selected")
# Safe: sanitize filename, save under workspace root only
safe_name = secure_filename(file.filename)
dest = root / safe_name
# Write file
with dest.open("wb") as f:
f.write(await file.read())
return {"path": str(dest), "filename": safe_name}
No path traversal: ../../etc/passwd in a filename is sanitized to etcpasswd.
Temp file exclusion
Temp/lock files are never ingested (ignored during workspace scanning):
Patterns excluded:
~$*— Word lock files.~lock*#— LibreOffice lock files.tmp,.temp,.bak,.swp,*.part— temporary artifacts
This prevents indexing transient metadata files that could leak into search results.
6. Network security
Backend binds to localhost only
The FastAPI server binds to 127.0.0.1:8765 — not 0.0.0.0:
python# Backend: main.py
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8765)
The backend is never exposed to the network — only the local Electron app can reach it.
CORS policy
CORS allows only:
http://localhost:5173(Vite dev server)file://(Electron in production)
External origins cannot call the API.
pythonapp.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_origin_regex=r"^file://.*",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
No external network calls (local mode)
In local mode:
- ✅ No outbound HTTP requests (models run locally)
- ✅ Ollama runs on
localhost:11434(not exposed to network) - ✅ ChromaDB is a local file database
The app is fully functional offline.
7. Data retention
What's stored where
| Data | Location | Persistence |
|---|---|---|
| Raw uploads | <workspace>/ | Permanent (user's files) |
| Vector index | <workspace>/workspace/chroma_db/ | Permanent (per project) |
| Chat history | <workspace>/workspace/history.json | Permanent (per project) |
| LangGraph threads | data/conversations.db | Permanent (project-independent) |
| Compliance flags | data/flags.db | Permanent (project-independent) |
| Logs | logs/legal_retrieval.log | Rotating (10 MB, 5 backups) |
What's NOT stored
- ❌ API keys are in
api_config.json(git-ignored), not committed - ❌ User queries are not logged (only logged locally in rotating log files)
- ❌ Documents are never uploaded to external servers (local mode)
- ❌ Telemetry/analytics data (none collected)
Clearing data
Per-project data (workspace):
bashrm -rf /path/to/project/workspace/
Global data (conversations, flags):
bashrm -rf data/conversations.db data/flags.db
Logs:
bashrm -rf logs/
Models (14 GB):
bashrm -rf models/
8. Dependency security
Dependency auditing
The project uses well-known, actively maintained dependencies:
Backend (Python):
fastapi— modern web framework, actively maintainedchromadb— vector database, Chroma teamsentence-transformers— HuggingFace, widely usedtorch— PyTorch, Facebook/Meta
Frontend (TypeScript):
react— Facebook/Metavite— Evan You (Vue.js creator)electron— GitHub/Microsoft
Audit commands:
bash# Python
cd backend
pip-audit
# Node.js
npm audit
npm audit fix
Run audits in CI to catch vulnerable dependencies.
Dependency pinning
Python dependencies (backend/requirements.txt) use version pins:
fastapi==0.109.0
chromadb==0.4.22
sentence-transformers==2.2.2
Not ranges (>=0.109.0) — exact versions for reproducibility.
Supply chain attacks
Defense:
- All Python packages from PyPI (official index)
- All Node packages from npmjs.com (official registry)
- No dependencies from git repos or unknown sources
package-lock.json/requirements.txtlock transitive deps
9. Secure coding practices
Input validation
All API inputs are validated via Pydantic models:
pythonclass ChatRequest(BaseModel):
query: str = Field(..., min_length=1, max_length=10000)
mode: Literal["rag", "retrieval_only", "direct"] = "rag"
top_k: int = Field(default=5, ge=1, le=100)
# ... validates types, ranges, required fields
Invalid input is rejected before processing.
SQL injection prevention
The app uses:
- SQLite with parameterized queries (LangGraph, flags persistence)
- ChromaDB (vector database, no SQL)
No raw SQL string concatenation anywhere in the codebase.
Path traversal prevention
File operations use safe path resolution:
python# Safe
workspace_root = Path("/absolute/path/to/project")
user_file = "contract.pdf"
safe_path = (workspace_root / user_file).resolve()
# Check: must be under workspace root
if not safe_path.is_relative_to(workspace_root):
raise ValueError("Path traversal attempt")
User-supplied paths are never used directly.
Command injection prevention
The app does not execute user-supplied shell commands. The only shell commands are:
ollama serve(hardcoded, no user input)ollama pull <model>(model name is from config, not user input)python main.py(Electron spawns backend, hardcoded)
XSS prevention
- CSP blocks inline scripts (production)
- React escapes by default (JSX auto-escapes)
- Markdown rendering uses
react-markdown(sanitized by default)
User input is never inserted into HTML via dangerouslySetInnerHTML.
10. Electron-specific security
Remote module disabled
The deprecated remote module is not used — all main↔renderer communication is via IPC.
webSecurity enabled
webSecurity: true (default) is never disabled — prevents:
- Loading insecure content (mixed HTTP/HTTPS)
- CORS bypass
- File protocol abuse
No allowRunningInsecureContent
Production builds never set allowRunningInsecureContent: true (which would allow HTTP content in HTTPS pages).
Native modules sandboxed
Electron's native modules (require('electron')) are not accessible in the renderer due to nodeIntegration: false + contextIsolation: true.
11. Security testing
Automated checks in CI
Two security checks run on every push/PR:
1. CSP validation (.github/workflows/ci.yml):
yaml- name: Build frontend
run: cd frontend && npm run build
- name: Check CSP
run: cd frontend && npm run check:csp
2. Electron security validation:
yaml- name: Build Electron
run: cd frontend/electron && npm run build
- name: Check Electron Security
run: cd frontend/electron && npm run check:security
Both checks fail the build on violations.
Manual security testing
Test CSP enforcement:
bashcd frontend
npm run build
npm run check:csp
Test Electron sandbox:
bashcd frontend/electron
npm run build
npm run check:security
Test dependency vulnerabilities:
bashcd backend
pip-audit
cd ..
npm audit
12. Threat model
In-scope threats
✅ XSS via user input — mitigated by CSP + React escaping ✅ Code injection — mitigated by sandbox + CSP ✅ Path traversal — mitigated by safe path resolution ✅ API key leakage — mitigated by git-ignore + log redaction ✅ Dependency vulnerabilities — mitigated by audits + pinning ✅ Electron sandbox escape — mitigated by strict webPreferences
Out-of-scope threats
⚠️ Physical access to machine — user's OS-level security responsibility ⚠️ Malicious Ollama models — user must trust model sources ⚠️ Compromised PyPI/npm packages — supply chain risk (audits help) ⚠️ OS kernel exploits — outside app's control
13. Incident response
Reporting a vulnerability
Email: [your-security-email@example.com]
What to include:
- Description of the vulnerability
- Steps to reproduce
- Affected versions
- Suggested fix (if any)
Response SLA:
- Acknowledgment within 48 hours
- Initial assessment within 7 days
- Fix timeline provided within 14 days
Disclosure policy
- Coordinated disclosure — 90 days from report to public disclosure
- Security fixes released as patch versions (e.g., 1.0.1)
- CVE assigned if applicable
- Security advisories published on GitHub
14. Security checklist for contributors
Before submitting a PR:
- [ ] No API keys, credentials, or secrets in code
- [ ] All user input validated via Pydantic models
- [ ] No raw SQL string concatenation
- [ ] No user-supplied paths used directly (use safe resolution)
- [ ] No
eval(),exec(), ornew Function() - [ ] No
dangerouslySetInnerHTMLin React components - [ ] CSP check passes (
npm run check:csp) - [ ] Electron security check passes (
npm run check:security) - [ ]
npm auditshows no high/critical vulnerabilities - [ ] New dependencies justified and from official registries
15. Security updates
Where to watch:
- GitHub Security Advisories
- Release notes — security fixes noted
- Dependency audit reports (
npm audit,pip-audit)
How to update:
bash# Pull latest
git pull origin main
# Update dependencies
cd backend && pip install -r requirements.txt
cd frontend && npm install
cd frontend/electron && npm install
16. Privacy guarantees
Local mode
✅ 100% private — no network calls, no data leaves your machine ✅ Offline capable — works without internet ✅ No telemetry — zero usage tracking
API mode
⚠️ Selective data sent — only the small text snippet needed for the answer ✅ Documents stay local — full files never uploaded ✅ User controls provider — choose OpenAI, Anthropic, etc. ✅ No telemetry — app still doesn't track usage
What's NEVER sent
- ❌ Your full documents
- ❌ Your file names or directory structure (unless part of a citation in the answer text)
- ❌ Your workspace folder paths
- ❌ Your API keys (sent only to the configured provider)
- ❌ Usage analytics or telemetry
17. Compliance notes
GDPR
The app does not collect, process, or transmit personal data — all processing is local (user's machine). API mode sends only text snippets to the chosen provider (user-controlled).
HIPAA / PHI
The app is not HIPAA-compliant out-of-the-box. Healthcare organizations must:
- Use local mode only (no API providers)
- Encrypt the workspace folder at rest (OS-level encryption)
- Ensure secure local machine (full disk encryption, access controls)
Legal documents
The app is designed for legal professionals handling confidential client documents. Local-first architecture ensures client privilege is maintained.
18. Related documentation
| Doc | Coverage |
|---|---|
ARCHITECTURE.md | System overview, ports |
BACKEND.md | API endpoints, validation |
FRONTEND.md | UI security, CSP |
API_PROVIDER.md | Cloud mode security |
DEVELOPMENT.md | Secure development practices |
DEPLOYMENT.md | Production security hardening |