+ A self-hosted AI workspace for chat, agents, research, documents, email, notes, calendar, and local model workflows.
+
-A self-hosted AI workspace -- meant to be the self-hosted version of the UI experience you get from ChatGPT and Claude. But with more jank and fun. Running on your own hardware, with your own data -- local-first, privacy-first, and no trojan.
+
-## Features
- - **Chat** -- chat with any local model or API; adding them is super simple. vLLM · llama.cpp · Ollama · OpenRouter · OpenAI
- - **Agent** -- hand it tools and let it run the whole task itself. built on [opencode](https://github.com/anomalyco/opencode) · MCP · web · files · shell · skills · memory
- - **Cookbook** -- Scans your hardware, recommends models, click to download and serve.. easy! built on [llmfit](https://github.com/AlexsJones/llmfit) · VRAM-aware · GGUF / FP8 / AWQ · fit scoring · vLLM / llama.cpp serving
- - **Deep Research** -- multi-step runs that gather, read, and synthesize sources into a nice visual report. adapted from [Tongyi DeepResearch](https://github.com/Alibaba-NLP/DeepResearch)
- - **Compare** -- a fun tool to compare models side by side. Test completely blind, no bias! multi-model · blind test · synthesis
- - **Documents** -- YOU write the text, AI is there to assist, not the opposite. multi-tab editor · markdown · HTML · CSV · syntax highlighting · AI edits · suggestions
- - **Memory / Skills** -- Persistent memory and skills, your agent evolves over time as it better understands you and your tasks! ChromaDB · fastembed (ONNX) · vector + keyword retrieval · import/export
- - **Email** -- IMAP/SMTP inbox with AI triage built in: urgency reminders, auto-tag, auto-summary, auto-reply drafts, auto-spam. IMAP · SMTP · per-account routing · CalDAV-aware
- - **Notes & Tasks** -- Quick notes with reminders, a todo list, and scheduled tasks the agent can act on. note pings · checklist · cron-style tasks · ntfy / browser / email channels
- - **Calendar** -- Local-first calendar with CalDAV sync to Radicale / Nextcloud / Apple / Fastmail. CalDAV pull · .ics import/export · per-calendar colors · agent-aware
- - **Works on mobile** -- looks and runs great on your phone, not just desktop. responsive · installable (PWA) · touch gestures
- - **Extras** -- more to explore, happy if you give it a go! image editor · theme editor · file uploads (vision + PDF) · web search · presets · sessions · 2FA
+
+
+
-## Demo
-A full, hover-to-play tour lives on the landing page (`docs/index.html`). A few looks:
-
-### Chat & Agents
-
-### Deep Research
-
-### Compare
-
-### Documents
-
-### Notes & Tasks
-
+
+
+
-## Quick Start
+---
-Defaults work out of the box — clone, run, configure inside the app.
-Open the **Settings** panel after first login to point Odysseus at your LLM
-server, search provider, email account, etc. Only touch `.env` if you need
-to override deployment-level things like `AUTH_ENABLED`, `DATABASE_URL`,
-or pre-seed `ODYSSEUS_ADMIN_PASSWORD` (otherwise an initial password is
-generated and printed on first boot).
+## Quick Start
-Contributing? See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, testing, and
-pull request guidelines.
+> `dev` is the default branch and gets the newest changes first. Use [`main`](https://github.com/pewdiepie-archdaemon/odysseus/tree/main) if you want the more curated branch.
-### Option 1: Docker (recommended)
```bash
git clone https://github.com/pewdiepie-archdaemon/odysseus.git
cd odysseus
-cp .env.example .env # optional, but recommended for explicit defaults
+cp .env.example .env
docker compose up -d --build
```
-Compose starts Odysseus, ChromaDB, SearXNG, and ntfy. First run does a full
-image build. Open `http://localhost:7000` after the containers are healthy.
-
-Cookbook remote servers use an Odysseus-owned SSH key from `./data/ssh`
-inside Docker. In **Cookbook -> Settings -> Servers**, generate/copy the
-public key and add it to the remote server's `~/.ssh/authorized_keys`.
-After generating the key, you can also install it from the host with:
-```bash
-ssh-copy-id -i data/ssh/id_ed25519.pub user@server
-```
-Cookbook local downloads are stored in `./data/huggingface`, mounted as
-`~/.cache/huggingface` inside the Odysseus container.
-
-Useful checks:
-```bash
-docker compose ps
-docker compose logs --tail=120 odysseus
-docker compose logs odysseus | grep -E 'ChromaDB|MemoryVectorStore|DEGRADED'
-docker compose exec odysseus python -c "from services.hwfit.models import get_models; print(len(get_models()))"
-```
-
-Expected vector-memory startup lines in Docker:
-```text
-ChromaDB connected: chromadb:8000
-MemoryVectorStore initialized
-```
-
-The Cookbook model catalog check should print a non-zero count. If it prints
-`0`, rebuild the Odysseus image with `docker compose build --no-cache odysseus`.
-
-### Option 2: Manual install — Linux / macOS
-**Requirements:** Python 3.11+. On Linux/Termux, Cookbook also requires `tmux`
-for background model downloads and serves.
-
-Install system packages first:
-```bash
-# Debian/Ubuntu
-sudo apt install tmux
-
-# Arch
-sudo pacman -S tmux
-
-# Fedora
-sudo dnf install tmux
-```
-
-Then install Odysseus:
-```bash
-git clone https://github.com/pewdiepie-archdaemon/odysseus.git
-cd odysseus
-python3 -m venv venv
-source venv/bin/activate
-pip install -r requirements.txt
-python setup.py # creates data dirs and prints an initial admin password
-python -m uvicorn app:app --host 0.0.0.0 --port 7000
-```
-
-### Option 3: Manual install — Windows (PowerShell)
-Windows support is not actively tested. Use it with caution; Docker on Linux
-or a Linux/macOS manual install is the safer path for now.
-
-```powershell
-git clone https://github.com/pewdiepie-archdaemon/odysseus.git
-cd odysseus
-python -m venv venv
-venv\Scripts\Activate.ps1
-pip install -r requirements.txt
-python setup.py
-python -m uvicorn app:app --host 0.0.0.0 --port 7000
-```
-
-Open `http://localhost:7000`, log in with the generated admin password,
-and configure everything else inside **Settings**.
-## Security Notes
-Odysseus is a self-hosted workspace with powerful local tools: shell access, file uploads, model downloads, web research, email/calendar integrations, and API tokens. Treat it like an admin console.
+Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`.
-- Keep `AUTH_ENABLED=true` for any network-accessible deployment.
-- Do not expose it directly to the public internet without HTTPS and a trusted reverse proxy.
-- Keep `data/`, `.env`, logs, databases, and uploaded/generated media out of Git. They are ignored by default.
-- Review `data/auth.json` after first boot: disable open signup unless you intentionally want it, make only your own account admin, and keep demo/test accounts non-admin.
-- Non-admin users do not get shell/Python/file read/write by default, and admin-only routes/tools such as MCP management, API tokens, webhooks, model/cookbook serving, backup/vault, and app settings are admin-gated. Other features are controlled by per-user privileges, so review each user's privileges before exposing a deployment.
-- Rotate any API keys or tokens that were ever pasted into a shared chat, demo, screenshot, or log.
-- If you enable API tokens or webhooks, create separate tokens per integration and delete unused ones.
-- Prefer binding manual development runs to `127.0.0.1`; bind to `0.0.0.0` only when you intentionally want LAN/reverse-proxy access.
-- Before publishing a fork, run `git status --short` and confirm no private files from `.env`, `data/`, `logs/`, uploads, backups, or local databases are staged.
+Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md).
-### Putting it behind HTTPS
-Odysseus serves plain HTTP on its port. That's fine for `localhost` and trusted LAN/VPN use, but browsers will warn ("Password fields present on an insecure page") and the login + API tokens travel in cleartext. For anything reachable outside your machine — including a Tailscale IP shared with other devices — put a TLS-terminating reverse proxy in front.
+## Features
-Shortest path with [Caddy](https://caddyserver.com/) (auto-renews Let's Encrypt certs):
+- **Chat + Agents** — local/API models, tools, MCP, files, shell, skills, and memory.
+- **Cookbook** — hardware-aware model recommendations, downloads, and serving.
+- **Deep Research** — multi-step web research with source reading and report generation.
+- **Compare** — blind side-by-side model testing and synthesis.
+- **Documents** — writing-first editor with AI edits, suggestions, Markdown, HTML, CSV, and syntax highlighting.
+- **Email** — IMAP/SMTP inbox with triage, tags, summaries, reminders, and reply drafts.
+- **Notes, Tasks + Calendar** — reminders, todos, scheduled agent tasks, and CalDAV sync.
+- **Extras** — gallery/image editor, themes, uploads, web search, presets, sessions, and 2FA.
-```caddy
-odysseus.example.com {
- reverse_proxy localhost:7000
-}
-```
+## Demo
-For a LAN-only Tailscale deployment, Caddy + [tailscale-cert](https://caddyserver.com/docs/caddyfile/options#auto-https) or the built-in MagicDNS HTTPS feature both work. nginx/Traefik configs are similar — proxy `localhost:7000`, terminate TLS at the proxy. Once that's in place, the browser warning goes away and your login is encrypted.
+A full hover-to-play tour lives on the landing page: [`docs/index.html`](docs/index.html).
## Contributing
-Help is welcome. The best entry points are fresh-install testing, provider setup
-bugs, mobile/editor polish, docs, and small focused refactors. See
-[ROADMAP.md](ROADMAP.md) for the current help-wanted list.
-
-## Configuration
-Most setup is done inside the app with `/setup` or **Settings**. Use `.env`
-for deployment-level defaults and secrets you want present before first boot.
-Key settings:
-
-| Variable | Default | Description |
-|---|---|---|
-| `LLM_HOST` | `localhost` | Your LLM server (e.g. `llm-host.local:8000`) |
-| `LLM_HOSTS` | -- | Comma-separated list for model discovery |
-| `OPENAI_API_KEY` | -- | Optional OpenAI key. Prefer adding providers in the app unless pre-seeding. |
-| `SEARXNG_INSTANCE` | `http://localhost:8080` | SearXNG URL. Docker overrides this to `http://searxng:8080`. |
-| `SEARXNG_SECRET` | generated on first Docker boot | Optional SearXNG cookie/CSRF secret. Leave blank unless you need to pin it. |
-| `AUTH_ENABLED` | `true` | Enable/disable login |
-| `LOCALHOST_BYPASS` | `false` | Development-only auth bypass for loopback requests. Keep false for shared/network deployments. |
-| `DATABASE_URL` | `sqlite:///./data/app.db` | Database connection string |
-| `CHROMADB_HOST` | `localhost` | ChromaDB host for vector memory. Docker overrides this to `chromadb`. |
-| `CHROMADB_PORT` | `8100` | ChromaDB port for manual host runs. Docker overrides this to `8000`. |
-| `EMBEDDING_URL` | -- | OpenAI-compatible embeddings endpoint |
-
-### Bundled services
-Docker Compose includes these by default. The bundled service ports bind to `127.0.0.1` unless you opt in to a different bind address in `.env`, so they are reachable from the host machine but not from your LAN or the public internet by default:
-
- - **ChromaDB** → vector store for semantic memory. In Docker, Odysseus connects to `chromadb:8000`; from the host it is exposed as `${CHROMADB_BIND:-127.0.0.1}:8100`.
- - **SearXNG** → meta search for web search. In Docker, Odysseus connects to `searxng:8080`; from the host it is exposed as `127.0.0.1:8080`.
- - **ntfy** → local notification service, exposed as `${NTFY_BIND:-127.0.0.1}:8091`.
-
-**Phone push notifications via ntfy:** A phone cannot subscribe to `127.0.0.1` on your server. To expose ntfy safely without opening it on every interface:
-
- - **Tailscale (recommended)** — set `NTFY_BIND=` and `NTFY_BASE_URL=http://:8091` in `.env`, recreate ntfy, then point the ntfy Android/iOS app at `http://:8091/`.
- - **Enable ntfy auth and bind to LAN** — add `NTFY_AUTH_FILE` + `NTFY_AUTH_DEFAULT_ACCESS=deny-all` to the `ntfy` service, create a user with `docker compose exec ntfy ntfy user add ...`, then set `NTFY_BIND` to your LAN IP. See the [ntfy docs](https://docs.ntfy.sh/config/#access-control).
-### Optional external services
- - **Ollama** → local LLM server -- [ollama.ai](https://ollama.ai)
+Help is welcome. The best entry points are fresh-install testing, provider setup bugs, mobile/editor polish, docs, and small focused refactors. See [CONTRIBUTING.md](CONTRIBUTING.md) and [ROADMAP.md](ROADMAP.md).
-### Ollama with Docker
-If Odysseus is running in Docker and Ollama is running on the host, add the endpoint in Settings as:
+## Security
-`http://host.docker.internal:11434/v1`
+Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled, keep private data out of Git, and do not expose raw model/service ports publicly. Deployment details are in the [setup guide](docs/setup.md#security-notes).
-The default Compose file already maps `host.docker.internal` on Linux. Ollama also needs to listen outside its own loopback interface:
+## Star History
-```bash
-OLLAMA_HOST=0.0.0.0:11434 ollama serve
-```
-
-For a systemd Ollama install, set that in the Ollama service override. If Odysseus can see Ollama but requests hang or fail, check that your host firewall allows Docker bridge traffic to port `11434`.
-
-First-token latency is usually Ollama/model/hardware, not Odysseus. To compare, test Ollama directly:
-
-```bash
-curl http://127.0.0.1:11434/v1/models
-```
-
-## Architecture
-```
-app.py # FastAPI entry point
-core/ auth, database, middleware, constants
-src/ llm_core, agent_loop, agent_tools, chat_processor, search/
-routes/ chat, session, document, memory, model … endpoints
-services/ docs, memory, search, hwfit (Cookbook) …
-static/ index.html + app.js + style.css + js/ (modular front-end)
-docs/ landing page (index.html) + preview clips
-```
-
-## Data
-All user data lives in `data/` (gitignored): `app.db` (sessions, messages, documents),
-`memory.json`, `presets.json`, `uploads/`, `personal_docs/`, `chroma/`, `settings.json`.
+
+
+
+
+
+
+
## License
-MIT -- see [LICENSE](LICENSE) and [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md).
-```
- |
- |||
- |||||
- | | | |||||||
- )_) )_) )_) ~|~
- )___))___))___)\ |
- )____)____)_____)\\|
- _____|____|____|_____\\\__
- \ /
- ~^~^~~^~^~~^~^~~^~^~~^~^~~^~^~~^~^~~^~^~
- ~^~ all aboard! ~^~
- ~^~^~~^~^~~^~^~~^~^~~^~^~~^~^~~^~^~~^~^~
-```
+AGPL-3.0-or-later -- see [LICENSE](LICENSE) and [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md).
diff --git a/ROADMAP.md b/ROADMAP.md
index aa79c3088..7c59c1f6a 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,6 +1,6 @@
# Roadmap / Help Wanted
-Odysseus is on a voyage, but not home yet. It works great for me (lol), but this is ship is moving fast and feedback/help would be appreciated! (I dont know what I'm doing hlep).
+Odysseus is on a voyage, but not home yet. It works great for me (lol), but this ship is moving fast and feedback/help would be appreciated! (I don't know what I'm doing, help).
If you see weird CSS, strange layout behavior, or a suspiciously murky corner of
the codebase, you are probably right to stay away.
@@ -8,25 +8,60 @@ the codebase, you are probably right to stay away.
## High Priority
- SQUASH BUGS
-- Fresh Docker install smoke tests on Linux, macOS, and Windows!!
+- Fresh install smoke tests on Linux, macOS, and Windows. Docker, native Python,
+ and WSL all need coverage.
- Integration audit: do integrations even work? Confirm what works, what needs setup docs, and what should be removed or hidden.
- Self-host troubleshooting cookbook. Document the weird 30-second fixes that otherwise become 30-minute searches: Dovecot cleartext auth for local stacks, ntfy Android Instant Delivery for non-ntfy.sh servers, clipboard limits on plain-HTTP Tailscale URLs, Radicale collection URLs, and similar traps.
- Cookbook reliability on other computers. This is probably the area most likely to need work across different machines, GPUs, drivers, shells, and Python environments.
-- Tile/window management correctness. I had to brute force my way a bit here, I'm aware, popups, dropdowns, and fixed-position UI inside transformed modals can land in the wrong place.
-- Esc button, it's small but a lot of windows that arent still close on esc and alot of them doesnt.
-- Skill audit, how does your model respond to skill injection, does it follow? Does its parsing miss?
+- Cookbook SGLang support across platforms. Make sure SGLang setup/serve works
+ predictably on Linux, Windows/WSL, macOS where possible, Docker, and common
+ NVIDIA/AMD hardware paths.
+- Deep Research model presets by hardware. Recommend approved model/parameter
+ profiles for small, medium, and large local setups so people with different
+ hardware can use Deep Research without guessing. Surface this either in Deep
+ Research settings or as a Cookbook scan/dropdown suggestion.
+- Cookbook model scan/download ranking. Prioritize newer architectures and
+ better hardware-fit models instead of scoring everything almost the same.
+ Ranking should account for architecture age, quant format, VRAM/RAM fit,
+ backend support, vision/mmproj requirements, and likely serve reliability.
+- Cookbook error feedback and logging. Failed downloads, dependency installs,
+ preflights, and serve jobs should show the actual command/output/error in the
+ UI, with copyable logs and clear next steps instead of just "crashed".
+- Agent prompt/context bloat. Agent mode is too heavy for smaller local models:
+ tool schemas, skills, memory, documents, and instructions can eat the context
+ before the user request really starts. We need slimmer prompts, better tool
+ selection, smaller default tool sets, and clearer guidance for models with
+ 4k/8k/16k context windows.
+- Skill/tool prompt-injection audit. User-editable skills, notes, documents,
+ fetched pages, and memories should be treated as untrusted data. Keep testing
+ whether models follow malicious instructions from those surfaces.
- Better degraded-state reporting for ChromaDB, SearXNG, email, ntfy, and provider probes.
+- Email performance audit. Fetching, searching, opening, deleting, and sending
+ email can feel slow, especially over IMAP/SMTP providers with high latency.
+ Need someone who knows mail performance to profile the current flow, identify
+ whether the bottleneck is IMAP folder select/fetch, cache invalidation,
+ attachment/body loading, SMTP handshakes, or frontend refresh behavior, then
+ propose safer caching/prefetch/batching without breaking multi-account state.
- Provider setup/probing audit for Anthropic, Gemini, Groq, xAI, OpenRouter, OpenAI, and DeepSeek.
## Refactor Targets
- CSS cleanup. `static/style.css` basically Calypso's island atm.
- Tour core helper. The onboarding tours have too much copy-pasted scaffolding; promote a shared `tour-core.js` helper before adding more tours.
+- Modal/window positioning cleanup. Some window controls have improved, but the
+ underlying popup/dropdown/fixed-position behavior is still too fragile.
- Mobile media override discoverability. A lot of "CSS did not move" bugs are mobile `@media` overrides of the same selector; comments or linting around desktop/mobile paired rules would help.
- Dead code pass for old routes, stale feature flags, and unused UI states.
## Frontend
+- Expand the Editor for quicker, more robust everyday use. Better file/document
+ handling, smoother window behavior, clearer save/export flows, stronger image
+ editing affordances, and fewer brittle edge cases.
+- Better AI integration for Notes and Todos. Notes should be easier for the
+ agent to read, update, summarize, and turn into actions. Todos should be
+ assignable to an agent from the UI, possibly through a button, task action,
+ or dedicated skill/tool flow.
- Mobile gallery/editor polish. Easier to launch/download inpaint model or any missing pieces.
- Accessibility pass: keyboard navigation, focus states, contrast, reduced motion.
- Improve empty states and error messages on fresh installs.
diff --git a/SECURITY.md b/SECURITY.md
index 2cca34be9..1fa5b0b3b 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -8,16 +8,20 @@ Security fixes are handled on the default branch until formal releases are cut.
## Deployment Guidance
-- Keep `AUTH_ENABLED=true`.
+- Keep `AUTH_ENABLED=true` for any network-accessible deployment.
+- Keep `LOCALHOST_BYPASS=false` outside local development.
+- Set `SECURE_COOKIES=true` when Odysseus is served through HTTPS by a trusted reverse proxy or private access gateway.
- Use HTTPS when exposing the app beyond localhost.
-- Put the app behind a trusted reverse proxy or private network.
-- Protect `.env`, `data/`, logs, uploaded files, generated media, and database files.
+- Put the authenticated Odysseus web/API entrypoint behind a trusted reverse proxy or private access layer such as Cloudflare Access, Tailscale, or a VPN.
+- Keep ChromaDB, SearXNG, ntfy, Ollama, vLLM, llama.cpp, databases, and raw model/provider APIs internal-only.
+- Protect `.env`, `data/`, `logs/`, uploads, generated media, backups, auth/session files, database files, API keys, and model/provider tokens.
- Disable open signup unless you intentionally want new accounts.
- Keep demo/test users non-admin, and remove them entirely on serious deployments.
- Give admin accounts strong passwords and enable 2FA where possible.
- Leave high-risk agent tools restricted to admins: shell, Python, file read/write, email send/read, MCP, app API, task/skill/memory management, settings, tokens, and model serving.
- Rotate API keys, webhook secrets, and Odysseus API tokens if they appear in logs, screenshots, demos, or shared chats.
- Treat shell, model-serving, MCP, email, calendar, and vault features as privileged admin functionality.
+- Common internal-only ports are Odysseus `7000`, SearXNG `8080`, ntfy `8091`, ChromaDB `8100`, Ollama `11434`, and local model/provider APIs such as `8000-8020`.
## Publishing A Fork
@@ -29,7 +33,7 @@ git check-ignore -v .env data/auth.json data/app.db logs/compound.log odysseus.d
git grep -n -I -E "(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-|AIza[0-9A-Za-z_-]{20,}|Bearer [A-Za-z0-9._~+/-]{20,})" -- . ':!static/lib/**' ':!package-lock.json'
```
-Only `.env.example`, docs, source, tests, and static assets should be committed. Never commit live `data/` contents, local databases, uploaded files, generated media, logs, backups, API keys, password hashes, or personal documents.
+Only `.env.example`, docs, source, tests, and static assets should be committed. Never commit live `.env` values, `data/` contents, local databases, uploaded files, generated media, logs, backups, auth/session files, API keys, model/provider tokens, password hashes, or personal documents.
## Reporting
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
new file mode 100644
index 000000000..48665a61d
--- /dev/null
+++ b/THREAT_MODEL.md
@@ -0,0 +1,81 @@
+# Threat Model
+
+Odysseus is a **self-hosted AI workspace with privileged local access**. This document states the trust boundary so contributors can reason about security decisions without reading through the full auth and middleware stack.
+
+## Trust Boundary
+
+Odysseus is designed for **trusted users on a private network**, not public exposure. The README describes it as "treat it like an admin console" — that framing is accurate. A logged-in admin can execute shell commands, read and write files, send email, and control model serving. This is intentional. The threat model does not try to prevent admins from doing these things. It does try to prevent:
+
+- Unauthenticated access
+- Non-admins reaching admin-only capabilities
+- The AI agent acting on instructions injected through untrusted content (web results, emails, fetched pages, memories)
+- Internal services (ChromaDB, Ollama, SearXNG, etc.) being reachable from outside the host
+
+## Roles and Capabilities
+
+| Capability | Admin | Non-admin (default) |
+|---|---|---|
+| Chat with agent | ✓ | ✓ |
+| Browser tool | ✓ | ✓ |
+| Documents | ✓ | ✓ |
+| Research mode | ✓ | ✓ |
+| Image generation | ✓ | ✓ |
+| Memory management | ✓ | ✓ |
+| Shell / Python execution | ✓ | ✗ |
+| File read / write | ✓ | ✗ |
+| Email send / read | ✓ | ✗ |
+| MCP tools | ✓ | ✗ |
+| Calendar management | ✓ | ✗ |
+| Token / webhook management | ✓ | ✗ |
+| Model serving | ✓ | ✗ |
+| Vault | ✓ | ✗ |
+| Settings | ✓ | ✗ |
+
+Non-admin defaults are in `core/auth.py:DEFAULT_PRIVILEGES`. Tool enforcement is in `src/tool_security.py:NON_ADMIN_BLOCKED_TOOLS`. Any tool whose name starts with `mcp__` is also blocked for non-admins. Admins always get full access regardless of stored privilege values.
+
+## Authentication
+
+- **Sessions:** bcrypt passwords, 7-day session tokens stored atomically in `data/sessions.json` via `core/atomic_io.py`.
+- **2FA:** TOTP with 8 single-use backup codes. Verified after password check, before session issuance.
+- **Reserved usernames:** `internal-tool`, `api`, `demo`, `system` cannot be registered or renamed into. Defined in `core/auth.py:RESERVED_USERNAMES`.
+ - `internal-tool` is security-critical: `core/middleware.py:require_admin` treats any request where `request.state.current_user == "internal-tool"` as the in-process tool loopback and grants admin unconditionally. A real account with that name would silently pass every `require_admin` check.
+- **Orphan sessions:** `validate_token` re-checks that the user record still exists on every call. A deleted user's cookie is dropped on next request rather than continuing to authenticate.
+
+## Internal Tool Loopback
+
+Agent tool calls reach admin-gated HTTP routes over an in-process HTTP loopback. The mechanism:
+
+1. At app startup, `core/middleware.py` generates a random `INTERNAL_TOOL_TOKEN` via `secrets.token_hex(32)`. It is never persisted and never sent to clients.
+2. Loopback requests carry `X-Odysseus-Internal-Token: ` or have `request.state.current_user` already set to `"internal-tool"` by the auth middleware.
+3. `require_admin` recognises either signal and grants access without checking the session user.
+
+The agent may be running in a non-admin user's session, but tool dispatch first calls `src/tool_security.py:owner_is_admin_or_single_user` to verify the session owner is an admin before issuing any loopback call. Non-admin users cannot invoke admin tools even via the agent.
+
+## Prompt-Injection Hardening
+
+External content that reaches the LLM is treated as untrusted via `src/prompt_security.py`:
+
+- `untrusted_context_message(label, content)` wraps the content in a `user`-role message with a header block instructing the model not to follow instructions inside it. Content goes in as data, not as a system instruction.
+- `UNTRUSTED_CONTEXT_POLICY` is a system-prompt preamble that states the same policy at the top of every session where untrusted data may appear.
+
+**Untrusted surfaces that must go through this wrapper:** web search results, fetched URLs, emails (read), saved memories, skill text, notes, and any tool output sourced from outside the server. Injecting untrusted content directly into the system role is a security bug.
+
+## Security Headers
+
+`core/middleware.py:SecurityHeadersMiddleware` sets headers on every response:
+
+- `X-Frame-Options: DENY` + `frame-ancestors 'none'` on all routes except tool-render iframes (which are sandboxed at the HTML level).
+- `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer` everywhere.
+- **CSP:** nonce-based `script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net`. `style-src 'unsafe-inline'` is intentionally kept — `static/index.html` ships inline `
+
+
Pair a device
+
Generate a one-time pairing code (a chat-scoped API token) for a LAN client.
+
+
Admin only. Each code mints a new token, shown once. Manage or revoke under Settings → API tokens.