From d792b61722f6266c63b7e0c28080c592630e993f Mon Sep 17 00:00:00 2001 From: Ashvin <76151462+ashvinctrl@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:37:49 +0530 Subject: [PATCH 1/3] test(gallery): point delete-ordering tests at the tmp image dir (#4300) The two delete-ordering tests did monkeypatch.chdir(tmp_path) and wrote the image under tmp_path/data/generated_images, but DATA_DIR (and therefore gallery_routes.GALLERY_IMAGE_DIR) is always an absolute path, so the delete resolver pointed at the repo's real data dir and ignored the chdir. test_file_removed_on_successful_delete therefore failed on dev (the file at the tmp path was never the one being removed), and test_file_kept_when_commit_fails passed only by accident. Set GALLERY_IMAGE_DIR to the seeded tmp dir via monkeypatch so both tests exercise the real path and pass deterministically. --- tests/test_gallery_delete_file_ordering.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_gallery_delete_file_ordering.py b/tests/test_gallery_delete_file_ordering.py index 03e0ef73e..468a2e7c4 100644 --- a/tests/test_gallery_delete_file_ordering.py +++ b/tests/test_gallery_delete_file_ordering.py @@ -41,8 +41,10 @@ def _seed(tmp_path): def test_file_kept_when_commit_fails(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) SessionLocal = _seed(tmp_path) + # GALLERY_IMAGE_DIR is an absolute path fixed at import, so a chdir can't + # redirect the delete; point the resolver at the seeded tmp dir directly. + monkeypatch.setattr(gallery_routes, "GALLERY_IMAGE_DIR", tmp_path / "data" / "generated_images") monkeypatch.setattr(gallery_routes, "get_current_user", lambda r: "alice") # A session whose commit always fails, to simulate a DB error mid-delete. @@ -67,8 +69,8 @@ def test_file_kept_when_commit_fails(tmp_path, monkeypatch): def test_file_removed_on_successful_delete(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) SessionLocal = _seed(tmp_path) + monkeypatch.setattr(gallery_routes, "GALLERY_IMAGE_DIR", tmp_path / "data" / "generated_images") monkeypatch.setattr(gallery_routes, "get_current_user", lambda r: "alice") monkeypatch.setattr(gallery_routes, "SessionLocal", SessionLocal) From b118c33e378ad51b4a43d36eca69eba9865c9b73 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Mon, 15 Jun 2026 23:21:49 +0900 Subject: [PATCH 2/3] test(provider): align lookalike-host URL expectations with /models behavior build_models_url returns /models (no /v1 prefix) for non-local generic OpenAI-compatible hosts (intentional, see endpoint_resolver.py:206). The tests added in #4272 expected /v1/models, which is the local/deepseek behavior. Match production semantics. --- tests/test_provider_detection.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_provider_detection.py b/tests/test_provider_detection.py index 997f2c572..a97b419d6 100644 --- a/tests/test_provider_detection.py +++ b/tests/test_provider_detection.py @@ -107,10 +107,8 @@ class TestBuildersRejectLookalikeHosts: assert build_chat_url("https://notanthropic.com") == "https://notanthropic.com/chat/completions" def test_lookalike_anthropic_models_is_openai(self): - # Must hit the generic OpenAI branch, not Anthropic — assert the - # provider directly since both branches now end in /v1/models. assert llm_core._detect_provider("https://anthropic.com.evil.com") == "openai" - assert build_models_url("https://anthropic.com.evil.com") == "https://anthropic.com.evil.com/v1/models" + assert build_models_url("https://anthropic.com.evil.com") == "https://anthropic.com.evil.com/models" def test_anthropic_domain_in_path_is_openai(self): assert build_chat_url("https://myproxy.internal/anthropic.com/v1") == "https://myproxy.internal/anthropic.com/v1/chat/completions" @@ -122,9 +120,8 @@ class TestBuildersRejectLookalikeHosts: assert build_chat_url("https://notollama.com") == "https://notollama.com/chat/completions" def test_lookalike_ollama_models_is_openai(self): - # Must hit the generic OpenAI branch, not Ollama. assert llm_core._detect_provider("https://notollama.com") == "openai" - assert build_models_url("https://notollama.com") == "https://notollama.com/v1/models" + assert build_models_url("https://notollama.com") == "https://notollama.com/models" class TestBuildersLocalAndDockerEndpoints: From d9ebdd6fbba31b5f3c2cbae90f650f183958660e Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Mon, 15 Jun 2026 23:24:41 +0900 Subject: [PATCH 3/3] Refresh README presentation --- README.md | 501 +++---------------------------------- docs/odysseus-wordmark.png | Bin 0 -> 16877 bytes docs/odysseus.jpg | Bin 45964 -> 53198 bytes docs/setup.md | 425 +++++++++++++++++++++++++++++++ 4 files changed, 463 insertions(+), 463 deletions(-) create mode 100644 docs/odysseus-wordmark.png create mode 100644 docs/setup.md diff --git a/README.md b/README.md index 8eb85229b..dcf07f761 100644 --- a/README.md +++ b/README.md @@ -1,476 +1,65 @@ -# Odysseus +

+ Odysseus +

-> **Branch note:** `dev` is the default branch and contains the latest development changes, but it may be unstable. For the more stable curated branch, use [`main`](https://github.com/pewdiepie-archdaemon/odysseus/tree/main). +

+ A self-hosted AI workspace for chat, agents, research, documents, email, notes, calendar, and local model workflows. +

-``` -─────────────────────────────────────────────── - ⊹ ࣪ ˖ ૮( ˶ᵔ ᵕ ᵔ˶ )っ Odysseus vers. 1.0 -─────────────────────────────────────────────── -``` +

+ Quick Start · + Setup Guide · + Contributing · + Roadmap +

-![Odysseus](docs/odysseus.jpg) +

+ Packaging status +

-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. +

+ Odysseus interface +

-[![Packaging status](https://repology.org/badge/vertical-allrepos/odysseus-ai.svg)](https://repology.org/project/odysseus-ai/versions) - -## Features - - **Chat** -- chat with any local model or API; adding them is super simple.
 vLLM · llama.cpp · Ollama · OpenRouter · OpenAI · GitHub Copilot - - **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`). - -
-Screenshots / clips - -### Chat & Agents -![Chat & Agents](docs/chat.gif) -### Deep Research -![Deep Research](docs/research.gif) -### Compare -![Compare](docs/compare.gif) -### Documents -![Documents](docs/document.gif) -### Notes & Tasks -![Notes & Tasks](docs/notes.gif) - -
+--- ## Quick Start -Defaults work out of the box: clone, run, then configure models/search/email -inside **Settings**. Only edit `.env` for deployment-level overrides like -`APP_BIND`, `APP_PORT`, `AUTH_ENABLED`, `DATABASE_URL`, or a pre-seeded admin password. +> `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. -On first setup, Odysseus creates an admin account (`admin` unless -`ODYSSEUS_ADMIN_USER` is set) and prints a temporary password in the terminal. -For Docker installs, the same line is in `docker compose logs odysseus`. -Use that for the first login, then change it in **Settings**. - -Contributing? See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, testing, and -pull request guidelines. - -### 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 ``` -To include optional extras in the image (PDF viewer, Office extraction; includes AGPL PyMuPDF), build with `docker compose build --build-arg INSTALL_OPTIONAL=true` before `up`. -Open `http://localhost:7000` when the containers are healthy. Docker Compose -binds the web UI to `127.0.0.1` by default. If the port is taken, set -`APP_PORT=7001` in `.env` and recreate the container. Set `APP_BIND=0.0.0.0` -only when you intentionally want LAN/reverse-proxy access. +Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`. -> **On Apple Silicon (M-series) Macs:** Docker can't reach the Metal GPU, so -> Cookbook serves local models on CPU only. For GPU-accelerated model serving, -> run natively instead — see [Apple Silicon](#apple-silicon) below. +Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md). -### Native Linux / macOS -```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 -python -m uvicorn app:app --host 127.0.0.1 --port 7000 -``` -Requirements: Python 3.11+. Cookbook also needs `tmux` for background model -downloads and serves. The app itself is lightweight; local model serving is the -heavy part and depends on the model, runtime, GPU, and VRAM, so small hosts can -connect to API or remote model servers instead. Use `--host 0.0.0.0` only when you intentionally want LAN/reverse-proxy access. +## Features -### Apple Silicon -Docker on macOS cannot use the Metal GPU. For GPU-accelerated Cookbook on an -M-series Mac, run Odysseus natively: +- **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. -```bash -git clone https://github.com/pewdiepie-archdaemon/odysseus.git -cd odysseus -./start-macos.sh -``` +## Demo -It launches at `http://127.0.0.1:7860`. To expose it to your phone over a trusted LAN/VPN such as Tailscale, bind all interfaces: - -```bash -ODYSSEUS_HOST=0.0.0.0 ./start-macos.sh -# then open http://:7860 -``` - -The script also reads `.env` at startup, so `APP_BIND=0.0.0.0` and `APP_PORT` -set there are picked up automatically without a command-line override each run. - -Keep `AUTH_ENABLED=true` (the default) before binding outside loopback. Do not -expose this port directly to the public internet. To build a clickable app wrapper: - -```bash -./build-macos-app.sh -``` - -
-Cookbook, GPU, Ollama, and troubleshooting notes - -**Docker bundled services.** Compose starts Odysseus, ChromaDB, SearXNG, and -ntfy. Odysseus and the bundled service ports bind to `127.0.0.1` by default, so -they are reachable from the host but not exposed to your LAN/public internet -unless you opt in. - -**Cookbook storage in Docker.** Downloads live in `./data/huggingface` -(`~/.cache/huggingface` in the container). Cookbook-installed Python CLIs and -serve engines live in `./data/local` (`~/.local` in the container), so they -survive container recreation. - -**Remote servers.** In **Cookbook -> Settings -> Servers**, generate the -Odysseus SSH key and add the public key to the remote server's -`~/.ssh/authorized_keys`. From the host you can also run: - -```bash -ssh-copy-id -i data/ssh/id_ed25519.pub user@server -``` - -**Docker GPU overlays.** CPU-only users can skip this section. Cookbook can -only detect GPUs that Docker exposes to the container — if the host runtime or -device passthrough is not configured, Cookbook sees the iGPU, another card, or -CPU instead of your intended GPU. - -For NVIDIA, `scripts/check-docker-gpu.sh` diagnoses GPU passthrough and can -optionally install the host runtime or update `.env`. - -```bash -# Read-only diagnostic (default — installs nothing, never edits .env): -scripts/check-docker-gpu.sh - -# Print OS-specific install commands without running them: -scripts/check-docker-gpu.sh --print-install-commands - -# Install NVIDIA Container Toolkit on Ubuntu/Debian (requires sudo): -scripts/check-docker-gpu.sh --install-nvidia-toolkit - -# Write COMPOSE_FILE to .env (only when GPU passthrough is confirmed working): -scripts/check-docker-gpu.sh --enable-nvidia-overlay - -# Full assisted setup — install toolkit, then enable overlay if passthrough works: -scripts/check-docker-gpu.sh --install-nvidia-toolkit --enable-nvidia-overlay -``` - -Safety notes: -- The app never installs host GPU runtime automatically. -- The app never edits `.env` automatically. -- `.env` is only modified when `--enable-nvidia-overlay` is explicitly passed, - and only after GPU passthrough succeeds. `--yes` skips prompts but does not - bypass the passthrough gate. -- `.env.bak.*` backups created by `--enable-nvidia-overlay` are ignored by - Git and the Docker build context. - -To enable manually without the script, add this to `.env`: - -```bash -COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml -``` - -**AMD / ROCm.** AMD setup is read-only diagnostic plus manual `.env` edit. Run: - -```bash -scripts/check-docker-amd-gpu.sh -``` - -Then add the reported values to `.env`, replacing `RENDER_GID` with your host's -numeric render group id: - -```bash -COMPOSE_FILE=docker-compose.yml:docker/gpu.amd.yml -RENDER_GID=989 -``` - -For NVIDIA/AMD GPU support, also read the comments in the selected overlay file: docker/gpu.nvidia.yml or docker/gpu.amd.yml. - -**Stack-management UIs (Portainer, Coolify, Dockhand, etc.).** These tools -often accept only a single Compose file and do not reliably honor `COMPOSE_FILE` -or multiple `-f` overlays. CLI users should keep using the `COMPOSE_FILE` -overlay workflow above. For stack UIs, point the stack at one of the standalone -files instead, which bundle the base stack plus the GPU settings: - -- `docker-compose.gpu-nvidia.yml` — still requires the NVIDIA Container Toolkit - on the host. -- `docker-compose.gpu-amd.yml` — still requires host ROCm/kfd/DRI setup, the - `video`/`render` group membership, and `RENDER_GID` when needed. - -The base `docker-compose.yml` plus the `docker/gpu.*.yml` overlays remain the -source of truth; the standalone files mirror them for single-file deployments. - -Verify after enabling either overlay: - -```bash -docker compose exec odysseus nvidia-smi -L # NVIDIA -docker compose exec odysseus sh -lc 'test -e /dev/kfd && test -d /dev/dri && ls -l /dev/kfd /dev/dri/renderD*' # AMD -``` - -> **GPU passthrough ≠ llama.cpp CUDA.** `nvidia-smi` passing inside the -> container confirms Docker GPU access, but llama.cpp also needs `cudart` and -> the CUDA Toolkit at runtime. If Cookbook logs show `Unable to find cudart -> library`, `Could NOT find CUDAToolkit`, `CUDA Toolkit not found`, or -> tensors/layers assigned to CPU, that is a Cookbook/llama.cpp build issue — -> not a Docker passthrough failure. Reinstall the serve engine via -> **Cookbook → Dependencies** to get a CUDA-enabled build. -> -> The same split applies to AMD/ROCm: seeing `/dev/kfd` and `/dev/dri` inside -> the container confirms device passthrough, not ROCm userspace or a -> ROCm-enabled vLLM/llama.cpp build. `rocm-smi` and `rocminfo` are not expected -> inside the slim Odysseus image. - -**Ollama with Docker.** If Ollama runs on the host, add this endpoint in -Settings: - -```text -http://host.docker.internal:11434/v1 -``` - -Ollama must listen outside its own loopback interface: - -```bash -OLLAMA_HOST=0.0.0.0:11434 ollama serve -``` - -This connects Odysseus in Docker to an Ollama server that is already running on -your host machine; it does not start Ollama inside the container. -`host.docker.internal` is Docker's hostname for the host machine from inside the -container. Cookbook **Serve** is a separate workflow for serving downloaded -models through Odysseus/llama.cpp, so Windows users with an existing Ollama -install usually only need to add the endpoint in Settings. - -**Useful checks.** - -```bash -docker compose ps -docker compose logs --tail=120 odysseus -docker compose logs odysseus | grep -E 'ChromaDB|MemoryVectorStore|DEGRADED' -``` - -**macOS details.** `start-macos.sh` installs Homebrew deps, creates the venv, -runs setup, and starts uvicorn on port `7860` because AirPlay often holds -`7000`. It uses llama.cpp/Ollama for Metal. vLLM/SGLang are CUDA/ROCm-only and -do not run on macOS. MLX-only models are not served by Odysseus. - -
- -### Native Windows - -**One-command launcher** (creates the venv, installs deps, runs setup, starts the -server; safe to re-run): - -```powershell -git clone https://github.com/pewdiepie-archdaemon/odysseus.git -cd odysseus -powershell -ExecutionPolicy Bypass -File .\launch-windows.ps1 -``` - -Or do it by hand: - -```powershell -git clone https://github.com/pewdiepie-archdaemon/odysseus.git -cd odysseus -py -3.11 -m venv venv -venv\Scripts\Activate.ps1 -pip install -r requirements.txt -python setup.py -python -m uvicorn app:app --host 127.0.0.1 --port 7000 -``` - -If `python` points at an older interpreter, use `py -3.12` (or another installed -3.11+ version) for the venv step. - -**Requirements:** Python 3.11+. The core app (chat, agent, memory, documents, -email, calendar, deep research) runs fully native. For full **Cookbook** background -model downloads and the agent shell tool, also install -[Git for Windows](https://git-scm.com/download/win) (provides `bash.exe`). -Local GPU *serving* of vLLM/SGLang needs Linux/WSL2; for a local model on Windows, -[Ollama](https://ollama.com/download) is the easiest path — point Odysseus at -`http://localhost:11434/v1` in Settings. - -Open `http://localhost:7000`, log in with the generated admin password, -and configure everything else inside **Settings**. - -## Troubleshooting & Advanced Setup - -### `chromadb-client` conflicts with embedded ChromaDB -If `chromadb-client` (the lightweight HTTP-only package) is installed alongside the full `chromadb` package, Odysseus starts but ChromaDB silently falls back to HTTP-only mode and fails. - -**Fix:** uninstall `chromadb-client` and force-reinstall the full package: -```bash -./venv/bin/pip uninstall chromadb-client -y -./venv/bin/pip install --force-reinstall chromadb -``` - -### HTTPS + LAN/Tailscale exposure -To expose Odysseus on a local network or Tailscale with HTTPS: -1. Change the bind address to `0.0.0.0` in `.env` (`APP_BIND=0.0.0.0` or `ODYSSEUS_HOST=0.0.0.0`). -2. Generate a locally-trusted cert for your LAN/Tailscale IPs using [mkcert](https://github.com/FiloSottile/mkcert): - ```bash - mkcert -install - mkcert -cert-file cert.pem -key-file key.pem 192.168.1.100 tailscale-ip - ``` -3. Run `uvicorn` with the generated certs: - ```bash - python -m uvicorn app:app --host 0.0.0.0 --port 7000 --ssl-certfile=cert.pem --ssl-keyfile=key.pem - ``` -4. Install the `mkcert` CA on any other device you want to access Odysseus from (e.g., for iOS, email the `rootCA.pem` to yourself, install the profile, and trust it in Certificate Trust Settings). - -### Optional Dependencies -`requirements-optional.txt` contains packages that unlock extra features. It is not installed by default. - -| Package | Feature unlocked | -|---------|-----------------| -| `faster-whisper` | Local speech-to-text (microphone -> text) via the "local" STT provider. | -| `ddgs` | DuckDuckGo as a search provider option. | -| `PyMuPDF` | PDF page rendering in the side viewer panel and form-filling. (Note: AGPL-3.0) | -| `markitdown` | Office/EPUB document text extraction (converts .docx/.xlsx/.pptx/.xls/.epub to Markdown). | - -### Faster, reproducible installs with uv (optional) -[uv](https://docs.astral.sh/uv/) works as a drop-in replacement for the -venv + pip steps in the native install guides, no project changes are needed but this change results in faster installs along with a lockfile for reproducible environments. After [installing `uv`](https://docs.astral.sh/uv/getting-started/installation/), use: - -```bash -uv venv venv --python 3.13 -uv pip install -r requirements.txt -# then continue as usual: python setup.py, uvicorn, ... -``` - -`requirements.txt` is intentionally unpinned, so two installs at different times can produce different package versions. If you want a reproducible environment (e.g. across your own machines, or to roll back after a bad upgrade), snapshot and restore exact versions with: - -```bash -uv pip compile requirements.txt -o requirements.lock # snapshot current resolution -uv pip sync requirements.lock # reproduce it exactly later -``` - -`requirements.lock` is gitignored and platform-specific (compile it on the OS you deploy to). Regenerate it deliberately when you want to take upgrades. The plain `uv pip install -r requirements.txt` keeps following the unpinned requirements like pip does. - -### Outlook / Office 365 email -Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook -and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox -passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the -current limitation and the planned integration direction. - -## 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. - -- Keep `AUTH_ENABLED=true` for any network-accessible deployment. -- Keep `LOCALHOST_BYPASS=false` outside local development. -- Use `SECURE_COOKIES=true` when Odysseus is served through HTTPS by a trusted reverse proxy or private access gateway. -- Do not expose it directly to the public internet without HTTPS and a trusted reverse proxy or private access layer. -- Keep `.env`, `data/`, `logs/`, databases, uploads, generated media, backups, auth/session files, API keys, and model/provider tokens out of Git and private shares. 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. -- Keep ChromaDB, SearXNG, ntfy, Ollama, vLLM, llama.cpp, databases, and raw model/provider APIs internal-only. Expose only the authenticated Odysseus web/API entrypoint through your trusted proxy or private access layer. -- Before publishing a fork, run `git status --short` and confirm no private files from `.env`, `data/`, `logs/`, uploads, backups, or local databases are staged. - -### Private or proxied deployments -Odysseus serves plain HTTP on its app port. Docker Compose binds Odysseus and the bundled services to `127.0.0.1` by default, so a typical production/private setup is: - -1. Keep Odysseus on localhost, for example `127.0.0.1:7000`. -2. Terminate HTTPS at a trusted reverse proxy or private access gateway. -3. Put the authenticated Odysseus web/API entrypoint behind that layer. -4. Keep raw service and model ports internal-only. - -Cloudflare Access, Tailscale, Caddy, nginx, and Traefik can all fit this pattern; none are required by Odysseus. If your access layer reaches Odysseus on the same host, proxy to `http://127.0.0.1:7000` and keep `AUTH_ENABLED=true`, `LOCALHOST_BYPASS=false`, and `SECURE_COOKIES=true`. -`ALLOWED_ORIGINS` lists exact permitted origins for cross-origin browser/API clients; ordinary same-origin reverse-proxy access usually does not need a special CORS entry. - -Common internal-only ports from the default docs/compose setup: - -| Port | Service | -|---|---| -| `7000` | Odysseus raw app port | -| `8080` | SearXNG | -| `8091` | ntfy | -| `8100` | ChromaDB host port for manual/compose access | -| `11434` | Ollama | -| `8000-8020` | Common local model/provider APIs | +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: +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). -| 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. | -| `APP_BIND` | `127.0.0.1` | Docker Compose host bind address for the web UI. Use `0.0.0.0` only for intentional LAN/reverse-proxy access. | -| `APP_PORT` | `7000` | Docker Compose host port for the web UI. | -| `APP_DATA_DIR` | `./data` | Docker Compose host directory for application data volumes. | -| `APP_LOGS_DIR` | `./logs` | Docker Compose host directory for application logs. | -| `AUTH_ENABLED` | `true` | Enable/disable login | -| `LOCALHOST_BYPASS` | `false` | Development-only auth bypass for loopback requests. Keep false for shared/network deployments. | -| `ALLOWED_ORIGINS` | `http://localhost,http://127.0.0.1` | Comma-separated exact permitted origins for cross-origin browser/API clients. | -| `SECURE_COOKIES` | `false` | Set true when serving Odysseus through HTTPS at a trusted proxy or private access gateway. | -| `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 | -| `ODYSSEUS_CHAT_UPLOAD_MAX_BYTES` | `10485760` | Chat/agent attachment cap in bytes. Raise for larger local PDFs or text documents. | -| `ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES` | `104857600` | Gallery image upload cap in bytes (100 MB). | -| `ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES` | `26214400` | Gallery transform input cap in bytes (25 MB). | -| `ODYSSEUS_MEMORY_IMPORT_MAX_BYTES` | `10485760` | Memory import file cap in bytes (10 MB). | -| `ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES` | `26214400` | Personal document upload cap in bytes (25 MB). | -| `ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES` | `26214400` | Email compose attachment cap in bytes (25 MB). | -| `ODYSSEUS_STT_MAX_AUDIO_BYTES` | `26214400` | Speech-to-text audio cap in bytes (25 MB). | -| `ODYSSEUS_ICS_MAX_BYTES` | `10485760` | Calendar `.ics` import cap in bytes (10 MB). | +## Security -All upload-limit vars are validated (must be a positive integer) and optional; an invalid value fails fast at startup. - -### Built-in MCP servers (optional setup) - -Odysseus auto-registers a few built-in MCP servers at startup. The npx-based ones (currently the browser server, `@playwright/mcp`) only start when their npm package is already in the local npx cache. If a package isn't cached, that server is skipped with a startup log message explaining what to do, so a fresh install does not block on a multi-minute npm download or hang if Playwright system deps are missing. - -To enable the browser MCP (page navigation, screenshots, vision), run once: - -```bash -npx -y @playwright/mcp@latest --version -``` - -That installs `@playwright/mcp` plus Playwright (~300MB total). Restart Odysseus and the server will register at startup. - -## 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`. - -To back up or restore everything in `data/`, see the -[Backup & Restore guide](docs/backup-restore.md). +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). ## Star History @@ -483,19 +72,5 @@ To back up or restore everything in `data/`, see the ## License -AGPL-3.0-or-later -- 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/docs/odysseus-wordmark.png b/docs/odysseus-wordmark.png new file mode 100644 index 0000000000000000000000000000000000000000..dce21eb660d1a77868c4872dda95a021645f958c GIT binary patch literal 16877 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 literal 0 HcmV?d00001 diff --git a/docs/odysseus.jpg b/docs/odysseus.jpg index 982a00f77df6ad3e9bc2f703a74eda677d3bd9b8..7a70bc5fc5c126f1242acb65bb7956c877152ada 100644 GIT binary patch literal 53198 zcmex=3yBhzSCiS(!nAjfI7om7R^99fUbJ zxj8vFxH#C^xp=v_xOsSZc-T4l_<4EwKx~j9j7&^S%*@Pe%*<>&9PAt*6{O++2N(o7 zn7**8Ff%GKFbOg;3o`yc!XO87CJQsjy$n#m#>mRf!NknT!u9_MLy7CxoN`(s#e%Q!n z9MmZ4Bo>@hH0jaD|F;-;m>C(E1epaH>>0j)fO>DRQ|xDrBc@I6SkozNdiL7PRZE-Y zQde`O?B)!~{xNB<-@XYOYFfDpwW__d{C|EkuhKmVAv zEq^7#({&;3Kf|9@Q_~$g!>eUhOupRF)wMpq|kHInme-t#oYPV_*~r*5io0|u`%!Z&7rb4Zr)rMwlivN^`3{P!ei|> zFV^_6NY$&qZ-s@F?^!ohz226U>!&6yTJ>D%ywi;SQdgJDg27vT#doc14PGgrne(r6 z<@{{ztHPGS7oYk6^_3B~ve#ejuhaL;UAoan_40~Y$;JYmbDWE= zuTyx{x#(umfsU2`8NS9CUpQ2H_-~-;e}=D(`%WkJwIzt{47JrNPFk0h865BL-ES88 zzQ*>d@KcDa`Qf{Pb`jEVwn@B5W8L*}#jcMlZEZE4owm(y-?gY_gZa%I!wbiLRIgG$ z&0IU}uq@Ak?Qy2!ai+4i>SmFz@tU*Z!rHaOZCm0JX_~0k`ts6E;DAjTX7|} z;!4SvIXBbSBAg(BsQe~+wo@l&`_c8p`R2ZayrFcXY{%)y&7uE3^Ls2J&pqDTy<1m z%$Ln#r)$eCR^7c7_iQ;^FBiDbxQC*d3EOr^^nWFTMOKj zJ3WtXP}pVjPH3%)h3QO-+^|JQ&$RvM>E_;+^!!lu#GP-mFG;^Ua_dCIJU6SdjTiI( zGZfxG@yF*x;Od>y$3@3nfno!%IWk}+p0ISWAdjL-PuY<+HPF9 z{8z-IXP1&olH}{{Cf`e!t=a3=C_C9;;+vRfyMt~#tC2|3yYJlaw*1_G2BGA0 z7}(onEB2k&JTpDzHvbcSzR-BxkWMht znss_DR~8J6X3Q-5e5!BP+Ut3~C=fKd&8|z$SWw^4tMb1>z�Umdhf>HRoO95 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 IF#rFX0Mf3Y@Bjb+ literal 45964 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 p!qN0Intn#p57w+PDoBTx_GtMtTK **Branch note:** `dev` is the default branch and contains the latest development changes, but it may be unstable. For the more stable curated branch, use [`main`](https://github.com/pewdiepie-archdaemon/odysseus/tree/main). + +Defaults work out of the box: clone, run, then configure models/search/email +inside **Settings**. Only edit `.env` for deployment-level overrides like +`APP_BIND`, `APP_PORT`, `AUTH_ENABLED`, `DATABASE_URL`, or a pre-seeded admin password. + +On first setup, Odysseus creates an admin account (`admin` unless +`ODYSSEUS_ADMIN_USER` is set) and prints a temporary password in the terminal. +For Docker installs, the same line is in `docker compose logs odysseus`. +Use that for the first login, then change it in **Settings**. + +Contributing? See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, testing, and +pull request guidelines. + +### Docker (recommended) +```bash +git clone https://github.com/pewdiepie-archdaemon/odysseus.git +cd odysseus +cp .env.example .env # optional, but recommended for explicit defaults +docker compose up -d --build +``` +To include optional extras in the image (PDF viewer, Office extraction; includes AGPL PyMuPDF), build with `docker compose build --build-arg INSTALL_OPTIONAL=true` before `up`. + +Open `http://localhost:7000` when the containers are healthy. Docker Compose +binds the web UI to `127.0.0.1` by default. If the port is taken, set +`APP_PORT=7001` in `.env` and recreate the container. Set `APP_BIND=0.0.0.0` +only when you intentionally want LAN/reverse-proxy access. + +> **On Apple Silicon (M-series) Macs:** Docker can't reach the Metal GPU, so +> Cookbook serves local models on CPU only. For GPU-accelerated model serving, +> run natively instead — see [Apple Silicon](#apple-silicon) below. + +### Native Linux / macOS +```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 +python -m uvicorn app:app --host 127.0.0.1 --port 7000 +``` +Requirements: Python 3.11+. Cookbook also needs `tmux` for background model +downloads and serves. The app itself is lightweight; local model serving is the +heavy part and depends on the model, runtime, GPU, and VRAM, so small hosts can +connect to API or remote model servers instead. Use `--host 0.0.0.0` only when you intentionally want LAN/reverse-proxy access. + +### Apple Silicon +Docker on macOS cannot use the Metal GPU. For GPU-accelerated Cookbook on an +M-series Mac, run Odysseus natively: + +```bash +git clone https://github.com/pewdiepie-archdaemon/odysseus.git +cd odysseus +./start-macos.sh +``` + +It launches at `http://127.0.0.1:7860`. To expose it to your phone over a trusted LAN/VPN such as Tailscale, bind all interfaces: + +```bash +ODYSSEUS_HOST=0.0.0.0 ./start-macos.sh +# then open http://:7860 +``` + +The script also reads `.env` at startup, so `APP_BIND=0.0.0.0` and `APP_PORT` +set there are picked up automatically without a command-line override each run. + +Keep `AUTH_ENABLED=true` (the default) before binding outside loopback. Do not +expose this port directly to the public internet. To build a clickable app wrapper: + +```bash +./build-macos-app.sh +``` + +
+Cookbook, GPU, Ollama, and troubleshooting notes + +**Docker bundled services.** Compose starts Odysseus, ChromaDB, SearXNG, and +ntfy. Odysseus and the bundled service ports bind to `127.0.0.1` by default, so +they are reachable from the host but not exposed to your LAN/public internet +unless you opt in. + +**Cookbook storage in Docker.** Downloads live in `./data/huggingface` +(`~/.cache/huggingface` in the container). Cookbook-installed Python CLIs and +serve engines live in `./data/local` (`~/.local` in the container), so they +survive container recreation. + +**Remote servers.** In **Cookbook -> Settings -> Servers**, generate the +Odysseus SSH key and add the public key to the remote server's +`~/.ssh/authorized_keys`. From the host you can also run: + +```bash +ssh-copy-id -i data/ssh/id_ed25519.pub user@server +``` + +**Docker GPU overlays.** CPU-only users can skip this section. Cookbook can +only detect GPUs that Docker exposes to the container — if the host runtime or +device passthrough is not configured, Cookbook sees the iGPU, another card, or +CPU instead of your intended GPU. + +For NVIDIA, `scripts/check-docker-gpu.sh` diagnoses GPU passthrough and can +optionally install the host runtime or update `.env`. + +```bash +# Read-only diagnostic (default — installs nothing, never edits .env): +scripts/check-docker-gpu.sh + +# Print OS-specific install commands without running them: +scripts/check-docker-gpu.sh --print-install-commands + +# Install NVIDIA Container Toolkit on Ubuntu/Debian (requires sudo): +scripts/check-docker-gpu.sh --install-nvidia-toolkit + +# Write COMPOSE_FILE to .env (only when GPU passthrough is confirmed working): +scripts/check-docker-gpu.sh --enable-nvidia-overlay + +# Full assisted setup — install toolkit, then enable overlay if passthrough works: +scripts/check-docker-gpu.sh --install-nvidia-toolkit --enable-nvidia-overlay +``` + +Safety notes: +- The app never installs host GPU runtime automatically. +- The app never edits `.env` automatically. +- `.env` is only modified when `--enable-nvidia-overlay` is explicitly passed, + and only after GPU passthrough succeeds. `--yes` skips prompts but does not + bypass the passthrough gate. +- `.env.bak.*` backups created by `--enable-nvidia-overlay` are ignored by + Git and the Docker build context. + +To enable manually without the script, add this to `.env`: + +```bash +COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml +``` + +**AMD / ROCm.** AMD setup is read-only diagnostic plus manual `.env` edit. Run: + +```bash +scripts/check-docker-amd-gpu.sh +``` + +Then add the reported values to `.env`, replacing `RENDER_GID` with your host's +numeric render group id: + +```bash +COMPOSE_FILE=docker-compose.yml:docker/gpu.amd.yml +RENDER_GID=989 +``` + +For NVIDIA/AMD GPU support, also read the comments in the selected overlay file: docker/gpu.nvidia.yml or docker/gpu.amd.yml. + +**Stack-management UIs (Portainer, Coolify, Dockhand, etc.).** These tools +often accept only a single Compose file and do not reliably honor `COMPOSE_FILE` +or multiple `-f` overlays. CLI users should keep using the `COMPOSE_FILE` +overlay workflow above. For stack UIs, point the stack at one of the standalone +files instead, which bundle the base stack plus the GPU settings: + +- `docker-compose.gpu-nvidia.yml` — still requires the NVIDIA Container Toolkit + on the host. +- `docker-compose.gpu-amd.yml` — still requires host ROCm/kfd/DRI setup, the + `video`/`render` group membership, and `RENDER_GID` when needed. + +The base `docker-compose.yml` plus the `docker/gpu.*.yml` overlays remain the +source of truth; the standalone files mirror them for single-file deployments. + +Verify after enabling either overlay: + +```bash +docker compose exec odysseus nvidia-smi -L # NVIDIA +docker compose exec odysseus sh -lc 'test -e /dev/kfd && test -d /dev/dri && ls -l /dev/kfd /dev/dri/renderD*' # AMD +``` + +> **GPU passthrough ≠ llama.cpp CUDA.** `nvidia-smi` passing inside the +> container confirms Docker GPU access, but llama.cpp also needs `cudart` and +> the CUDA Toolkit at runtime. If Cookbook logs show `Unable to find cudart +> library`, `Could NOT find CUDAToolkit`, `CUDA Toolkit not found`, or +> tensors/layers assigned to CPU, that is a Cookbook/llama.cpp build issue — +> not a Docker passthrough failure. Reinstall the serve engine via +> **Cookbook → Dependencies** to get a CUDA-enabled build. +> +> The same split applies to AMD/ROCm: seeing `/dev/kfd` and `/dev/dri` inside +> the container confirms device passthrough, not ROCm userspace or a +> ROCm-enabled vLLM/llama.cpp build. `rocm-smi` and `rocminfo` are not expected +> inside the slim Odysseus image. + +**Ollama with Docker.** If Ollama runs on the host, add this endpoint in +Settings: + +```text +http://host.docker.internal:11434/v1 +``` + +Ollama must listen outside its own loopback interface: + +```bash +OLLAMA_HOST=0.0.0.0:11434 ollama serve +``` + +This connects Odysseus in Docker to an Ollama server that is already running on +your host machine; it does not start Ollama inside the container. +`host.docker.internal` is Docker's hostname for the host machine from inside the +container. Cookbook **Serve** is a separate workflow for serving downloaded +models through Odysseus/llama.cpp, so Windows users with an existing Ollama +install usually only need to add the endpoint in Settings. + +**Useful checks.** + +```bash +docker compose ps +docker compose logs --tail=120 odysseus +docker compose logs odysseus | grep -E 'ChromaDB|MemoryVectorStore|DEGRADED' +``` + +**macOS details.** `start-macos.sh` installs Homebrew deps, creates the venv, +runs setup, and starts uvicorn on port `7860` because AirPlay often holds +`7000`. It uses llama.cpp/Ollama for Metal. vLLM/SGLang are CUDA/ROCm-only and +do not run on macOS. MLX-only models are not served by Odysseus. + +
+ +### Native Windows + +**One-command launcher** (creates the venv, installs deps, runs setup, starts the +server; safe to re-run): + +```powershell +git clone https://github.com/pewdiepie-archdaemon/odysseus.git +cd odysseus +powershell -ExecutionPolicy Bypass -File .\launch-windows.ps1 +``` + +Or do it by hand: + +```powershell +git clone https://github.com/pewdiepie-archdaemon/odysseus.git +cd odysseus +py -3.11 -m venv venv +venv\Scripts\Activate.ps1 +pip install -r requirements.txt +python setup.py +python -m uvicorn app:app --host 127.0.0.1 --port 7000 +``` + +If `python` points at an older interpreter, use `py -3.12` (or another installed +3.11+ version) for the venv step. + +**Requirements:** Python 3.11+. The core app (chat, agent, memory, documents, +email, calendar, deep research) runs fully native. For full **Cookbook** background +model downloads and the agent shell tool, also install +[Git for Windows](https://git-scm.com/download/win) (provides `bash.exe`). +Local GPU *serving* of vLLM/SGLang needs Linux/WSL2; for a local model on Windows, +[Ollama](https://ollama.com/download) is the easiest path — point Odysseus at +`http://localhost:11434/v1` in Settings. + +Open `http://localhost:7000`, log in with the generated admin password, +and configure everything else inside **Settings**. + +## Troubleshooting & Advanced Setup + +### `chromadb-client` conflicts with embedded ChromaDB +If `chromadb-client` (the lightweight HTTP-only package) is installed alongside the full `chromadb` package, Odysseus starts but ChromaDB silently falls back to HTTP-only mode and fails. + +**Fix:** uninstall `chromadb-client` and force-reinstall the full package: +```bash +./venv/bin/pip uninstall chromadb-client -y +./venv/bin/pip install --force-reinstall chromadb +``` + +### HTTPS + LAN/Tailscale exposure +To expose Odysseus on a local network or Tailscale with HTTPS: +1. Change the bind address to `0.0.0.0` in `.env` (`APP_BIND=0.0.0.0` or `ODYSSEUS_HOST=0.0.0.0`). +2. Generate a locally-trusted cert for your LAN/Tailscale IPs using [mkcert](https://github.com/FiloSottile/mkcert): + ```bash + mkcert -install + mkcert -cert-file cert.pem -key-file key.pem 192.168.1.100 tailscale-ip + ``` +3. Run `uvicorn` with the generated certs: + ```bash + python -m uvicorn app:app --host 0.0.0.0 --port 7000 --ssl-certfile=cert.pem --ssl-keyfile=key.pem + ``` +4. Install the `mkcert` CA on any other device you want to access Odysseus from (e.g., for iOS, email the `rootCA.pem` to yourself, install the profile, and trust it in Certificate Trust Settings). + +### Optional Dependencies +`requirements-optional.txt` contains packages that unlock extra features. It is not installed by default. + +| Package | Feature unlocked | +|---------|-----------------| +| `faster-whisper` | Local speech-to-text (microphone -> text) via the "local" STT provider. | +| `ddgs` | DuckDuckGo as a search provider option. | +| `PyMuPDF` | PDF page rendering in the side viewer panel and form-filling. (Note: AGPL-3.0) | +| `markitdown` | Office/EPUB document text extraction (converts .docx/.xlsx/.pptx/.xls/.epub to Markdown). | + +### Faster, reproducible installs with uv (optional) +[uv](https://docs.astral.sh/uv/) works as a drop-in replacement for the +venv + pip steps in the native install guides, no project changes are needed but this change results in faster installs along with a lockfile for reproducible environments. After [installing `uv`](https://docs.astral.sh/uv/getting-started/installation/), use: + +```bash +uv venv venv --python 3.13 +uv pip install -r requirements.txt +# then continue as usual: python setup.py, uvicorn, ... +``` + +`requirements.txt` is intentionally unpinned, so two installs at different times can produce different package versions. If you want a reproducible environment (e.g. across your own machines, or to roll back after a bad upgrade), snapshot and restore exact versions with: + +```bash +uv pip compile requirements.txt -o requirements.lock # snapshot current resolution +uv pip sync requirements.lock # reproduce it exactly later +``` + +`requirements.lock` is gitignored and platform-specific (compile it on the OS you deploy to). Regenerate it deliberately when you want to take upgrades. The plain `uv pip install -r requirements.txt` keeps following the unpinned requirements like pip does. + +### Outlook / Office 365 email +Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook +and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox +passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the +current limitation and the planned integration direction. + +## 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. + +- Keep `AUTH_ENABLED=true` for any network-accessible deployment. +- Keep `LOCALHOST_BYPASS=false` outside local development. +- Use `SECURE_COOKIES=true` when Odysseus is served through HTTPS by a trusted reverse proxy or private access gateway. +- Do not expose it directly to the public internet without HTTPS and a trusted reverse proxy or private access layer. +- Keep `.env`, `data/`, `logs/`, databases, uploads, generated media, backups, auth/session files, API keys, and model/provider tokens out of Git and private shares. 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. +- Keep ChromaDB, SearXNG, ntfy, Ollama, vLLM, llama.cpp, databases, and raw model/provider APIs internal-only. Expose only the authenticated Odysseus web/API entrypoint through your trusted proxy or private access layer. +- Before publishing a fork, run `git status --short` and confirm no private files from `.env`, `data/`, `logs/`, uploads, backups, or local databases are staged. + +### Private or proxied deployments +Odysseus serves plain HTTP on its app port. Docker Compose binds Odysseus and the bundled services to `127.0.0.1` by default, so a typical production/private setup is: + +1. Keep Odysseus on localhost, for example `127.0.0.1:7000`. +2. Terminate HTTPS at a trusted reverse proxy or private access gateway. +3. Put the authenticated Odysseus web/API entrypoint behind that layer. +4. Keep raw service and model ports internal-only. + +Cloudflare Access, Tailscale, Caddy, nginx, and Traefik can all fit this pattern; none are required by Odysseus. If your access layer reaches Odysseus on the same host, proxy to `http://127.0.0.1:7000` and keep `AUTH_ENABLED=true`, `LOCALHOST_BYPASS=false`, and `SECURE_COOKIES=true`. +`ALLOWED_ORIGINS` lists exact permitted origins for cross-origin browser/API clients; ordinary same-origin reverse-proxy access usually does not need a special CORS entry. + +Common internal-only ports from the default docs/compose setup: + +| Port | Service | +|---|---| +| `7000` | Odysseus raw app port | +| `8080` | SearXNG | +| `8091` | ntfy | +| `8100` | ChromaDB host port for manual/compose access | +| `11434` | Ollama | +| `8000-8020` | Common local model/provider APIs | + +## 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. | +| `APP_BIND` | `127.0.0.1` | Docker Compose host bind address for the web UI. Use `0.0.0.0` only for intentional LAN/reverse-proxy access. | +| `APP_PORT` | `7000` | Docker Compose host port for the web UI. | +| `APP_DATA_DIR` | `./data` | Docker Compose host directory for application data volumes. | +| `APP_LOGS_DIR` | `./logs` | Docker Compose host directory for application logs. | +| `AUTH_ENABLED` | `true` | Enable/disable login | +| `LOCALHOST_BYPASS` | `false` | Development-only auth bypass for loopback requests. Keep false for shared/network deployments. | +| `ALLOWED_ORIGINS` | `http://localhost,http://127.0.0.1` | Comma-separated exact permitted origins for cross-origin browser/API clients. | +| `SECURE_COOKIES` | `false` | Set true when serving Odysseus through HTTPS at a trusted proxy or private access gateway. | +| `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 | +| `ODYSSEUS_CHAT_UPLOAD_MAX_BYTES` | `10485760` | Chat/agent attachment cap in bytes. Raise for larger local PDFs or text documents. | +| `ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES` | `104857600` | Gallery image upload cap in bytes (100 MB). | +| `ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES` | `26214400` | Gallery transform input cap in bytes (25 MB). | +| `ODYSSEUS_MEMORY_IMPORT_MAX_BYTES` | `10485760` | Memory import file cap in bytes (10 MB). | +| `ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES` | `26214400` | Personal document upload cap in bytes (25 MB). | +| `ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES` | `26214400` | Email compose attachment cap in bytes (25 MB). | +| `ODYSSEUS_STT_MAX_AUDIO_BYTES` | `26214400` | Speech-to-text audio cap in bytes (25 MB). | +| `ODYSSEUS_ICS_MAX_BYTES` | `10485760` | Calendar `.ics` import cap in bytes (10 MB). | + +All upload-limit vars are validated (must be a positive integer) and optional; an invalid value fails fast at startup. + +### Built-in MCP servers (optional setup) + +Odysseus auto-registers a few built-in MCP servers at startup. The npx-based ones (currently the browser server, `@playwright/mcp`) only start when their npm package is already in the local npx cache. If a package isn't cached, that server is skipped with a startup log message explaining what to do, so a fresh install does not block on a multi-minute npm download or hang if Playwright system deps are missing. + +To enable the browser MCP (page navigation, screenshots, vision), run once: + +```bash +npx -y @playwright/mcp@latest --version +``` + +That installs `@playwright/mcp` plus Playwright (~300MB total). Restart Odysseus and the server will register at startup. + +## 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`. + +To back up or restore everything in `data/`, see the +[Backup & Restore guide](docs/backup-restore.md).