mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-24 09:54:17 +02:00
* fix(mcp): stop assuming http://localhost:7000 for the OAuth callback The MCP OAuth callback origin is wrong on any install not reached at http://localhost:7000, and on Docker it cannot be corrected at all. Three sites, one assumption: - The redirect base fell back to a fixed port 7000. The app binds APP_PORT natively (app.py, launcher.py) and the macOS launcher defaults to 7860, where 7000 is AirPlay Receiver, so the callback lands on another service entirely. The fallback now follows APP_PORT. The hostname stays localhost rather than internal_api_base()'s 127.0.0.1: this URI is registered with the authorization server, so changing the host would invalidate the registrations that already exist. - The paste-back form hardcoded an http:// action. Serving the page over HTTPS, Chrome raises its insecure-form interstitial, and overriding that posts plain HTTP at a TLS port, which fails too. Either way the authorization code never reaches Odysseus. The action now carries the scheme the request arrived on. - OAUTH_REDIRECT_BASE_URL is the only fix available to a Docker install, because the container listens on 7000 and cannot see the host port map, but compose never forwarded it and nothing documented it. Both fixed. * fix(mcp): make the paste-back form action relative and export APP_PORT Answers the review on #6032. Three of the fixes did not survive contact with the deployments they targeted. - The form action derived its scheme from request.url.scheme. uvicorn only honours X-Forwarded-Proto from a peer inside --forwarded-allow-ips, which defaults to 127.0.0.1; the Dockerfile CMD sets no override, so a proxy arriving over the Docker bridge is untrusted and the scheme stays http. That is mixed content on exactly the HTTPS installs paste-back exists for. A relative action is resolved by the browser against the origin the page came from, which is right under every proxy setup, and it drops the Host header from the page entirely. - The APP_PORT fallback never fired for the shipped launchers. start-macos.sh, the generated .app launcher and launch-windows.ps1 all pass --port to uvicorn without putting the value in the environment, so the motivating case, macOS on 7860, still registered localhost:7000. Each now exports it. internal_api_base() and companion pairing read APP_PORT too and were wrong in the same way. - .env.example pointed Google MCP servers at OAUTH_REDIRECT_BASE_URL. add_server writes Desktop App credentials, and Google only accepts loopback redirects for that client type, so a public origin comes back as redirect_uri_mismatch. The variable is for the DCR flow; Google stays on the loopback default and finishes remotely through paste-back. The Host header is no longer reflected into the page, so the escaping regression test asserts its absence instead of its escaping.
188 lines
8.5 KiB
YAML
188 lines
8.5 KiB
YAML
# Standalone AMD ROCm GPU Compose file for stack-management UIs (Portainer,
|
|
# Coolify, Dockhand, etc.) that accept only a single Compose file and do not
|
|
# reliably honor COMPOSE_FILE or multiple `-f` overlays.
|
|
#
|
|
# This is equivalent to: docker-compose.yml + docker/gpu.amd.yml.
|
|
# The base docker-compose.yml plus the docker/gpu.amd.yml overlay remain the
|
|
# source of truth — CLI users should keep using the COMPOSE_FILE overlay
|
|
# workflow. Keep this file in sync with both when either changes.
|
|
#
|
|
# Requires ROCm drivers on the host (kfd + DRI devices) and the host user
|
|
# running Docker in the `video` and `render` groups. Set RENDER_GID to your
|
|
# host's numeric render group id when needed. See docker/gpu.amd.yml for details.
|
|
services:
|
|
odysseus:
|
|
build: .
|
|
ports:
|
|
- "${APP_BIND:-127.0.0.1}:${APP_PORT:-7000}:7000"
|
|
volumes:
|
|
- ${APP_DATA_DIR:-./data}:/app/data:z
|
|
- ${APP_LOGS_DIR:-./logs}:/app/logs:z
|
|
# Cookbook remote-server SSH identity. Odysseus can generate a key here;
|
|
# add the shown public key to each remote server's authorized_keys.
|
|
- ${APP_DATA_DIR:-./data}/ssh:/app/.ssh:z
|
|
# Cookbook local model cache. Inside Docker, "Local" means the Odysseus
|
|
# container, so persist its HuggingFace cache under ./data/huggingface.
|
|
- ${APP_DATA_DIR:-./data}/huggingface:/app/.cache/huggingface:z
|
|
# Cookbook-installed Python CLIs/packages (vLLM, llama-cpp-python, etc.)
|
|
# land under /app/.local for the odysseus user. Persist them so a
|
|
# container recreate does not silently remove installed serve engines.
|
|
- ${APP_DATA_DIR:-./data}/local:/app/.local:z
|
|
extra_hosts:
|
|
# Lets the container reach local services on the Docker host, including
|
|
# Ollama at http://host.docker.internal:11434.
|
|
- "host.docker.internal:host-gateway"
|
|
environment:
|
|
- LLM_HOST=${LLM_HOST:-localhost}
|
|
- LLM_HOSTS=${LLM_HOSTS:-}
|
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-}
|
|
- RESEARCH_LLM_ENDPOINT=${RESEARCH_LLM_ENDPOINT:-}
|
|
- HF_TOKEN=${HF_TOKEN:-}
|
|
- HUGGING_FACE_HUB_TOKEN=${HUGGING_FACE_HUB_TOKEN:-}
|
|
- SEARXNG_INSTANCE=http://searxng:8080
|
|
- CHROMADB_HOST=chromadb
|
|
- CHROMADB_PORT=8000
|
|
- DATABASE_URL=${DATABASE_URL:-sqlite:///./data/app.db}
|
|
- AUTH_ENABLED=${AUTH_ENABLED:-true}
|
|
- LOCALHOST_BYPASS=${LOCALHOST_BYPASS:-false}
|
|
- COMPANION_BASE_URL=${COMPANION_BASE_URL:-}
|
|
- ODYSSEUS_ADMIN_USER=${ODYSSEUS_ADMIN_USER:-admin}
|
|
- ODYSSEUS_ADMIN_PASSWORD=${ODYSSEUS_ADMIN_PASSWORD:-}
|
|
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost,http://127.0.0.1}
|
|
- SECURE_COOKIES=${SECURE_COOKIES:-false}
|
|
- EMBEDDING_URL=${EMBEDDING_URL:-}
|
|
- EMBEDDING_MODEL=${EMBEDDING_MODEL:-}
|
|
- EMBEDDING_API_KEY=${EMBEDDING_API_KEY:-}
|
|
- FASTEMBED_MODEL=${FASTEMBED_MODEL:-sentence-transformers/all-MiniLM-L6-v2}
|
|
- FASTEMBED_CACHE_PATH=${FASTEMBED_CACHE_PATH:-}
|
|
- CLEANUP_INTERVAL_HOURS=${CLEANUP_INTERVAL_HOURS:-24}
|
|
- ODYSSEUS_INPROCESS_POLLERS=${ODYSSEUS_INPROCESS_POLLERS:-1}
|
|
- ODYSSEUS_INPROCESS_TASKS=${ODYSSEUS_INPROCESS_TASKS:-1}
|
|
- ODYSSEUS_SCRIPT_HOST=${ODYSSEUS_SCRIPT_HOST:-localhost}
|
|
- ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=${ODYSSEUS_CHAT_UPLOAD_MAX_BYTES:-10485760}
|
|
- ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES=${ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES:-104857600}
|
|
- ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES=${ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES:-26214400}
|
|
- ODYSSEUS_MEMORY_IMPORT_MAX_BYTES=${ODYSSEUS_MEMORY_IMPORT_MAX_BYTES:-10485760}
|
|
- ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES=${ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES:-26214400}
|
|
- ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=${ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES:-26214400}
|
|
- ODYSSEUS_STT_MAX_AUDIO_BYTES=${ODYSSEUS_STT_MAX_AUDIO_BYTES:-26214400}
|
|
- ODYSSEUS_ICS_MAX_BYTES=${ODYSSEUS_ICS_MAX_BYTES:-10485760}
|
|
- ODYSSEUS_TTS_CACHE_MAX_BYTES=${ODYSSEUS_TTS_CACHE_MAX_BYTES}
|
|
- DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-}
|
|
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
|
|
- GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-}
|
|
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
|
|
- GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-}
|
|
- GOOGLE_OAUTH_REDIRECT_URI=${GOOGLE_OAUTH_REDIRECT_URI:-}
|
|
# Externally reachable origin for MCP OAuth callbacks. The container
|
|
# always listens on 7000 and cannot see the host port map above, so
|
|
# remote MCP OAuth needs this set whenever the browser reaches
|
|
# Odysseus on anything other than http://localhost:7000.
|
|
- OAUTH_REDIRECT_BASE_URL=${OAUTH_REDIRECT_BASE_URL:-}
|
|
- TAVILY_API_KEY=${TAVILY_API_KEY:-}
|
|
- SERPER_API_KEY=${SERPER_API_KEY:-}
|
|
# PUID / PGID — the user/group the container drops to before
|
|
# running uvicorn (entrypoint also chowns /app/data + /app/logs
|
|
# to match, so bind-mounted files stay editable from the host).
|
|
# 1000 is the default first user on most Linux installs. If your
|
|
# host user has a different id, override here or via .env, e.g.:
|
|
# PUID=1001
|
|
# PGID=1001
|
|
# Find yours with: id -u / id -g
|
|
- PUID=${PUID:-1000}
|
|
- PGID=${PGID:-1000}
|
|
depends_on:
|
|
searxng:
|
|
condition: service_healthy
|
|
chromadb:
|
|
condition: service_started
|
|
restart: unless-stopped
|
|
# AMD ROCm overlay (from docker/gpu.amd.yml).
|
|
devices:
|
|
- /dev/kfd
|
|
- /dev/dri
|
|
group_add:
|
|
- video
|
|
- ${RENDER_GID:-render}
|
|
|
|
chromadb:
|
|
image: docker.io/chromadb/chroma:latest
|
|
ports:
|
|
- "${CHROMADB_BIND:-127.0.0.1}:8100:8000"
|
|
volumes:
|
|
- chromadb-data:/chroma/chroma
|
|
environment:
|
|
- ANONYMIZED_TELEMETRY=FALSE
|
|
restart: unless-stopped
|
|
|
|
searxng:
|
|
# Pinned, not :latest — odysseus waits on searxng's healthcheck
|
|
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
|
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
|
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
|
# Bump this deliberately after verifying a newer tag boots clean.
|
|
image: docker.io/searxng/searxng:2026.5.31-7159b8aed
|
|
entrypoint:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
set -eu
|
|
if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then
|
|
secret="$${SEARXNG_SECRET:-}"
|
|
if [ -z "$$secret" ]; then
|
|
secret="$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
|
|
fi
|
|
sed "s|__SEARXNG_SECRET__|$$secret|g" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml
|
|
fi
|
|
# Advisory: a settings file the migration cannot parse or rewrite must
|
|
# not be what stops searxng from booting. It explains itself on stderr
|
|
# and we carry on, letting searxng report anything genuinely wrong.
|
|
/usr/local/searxng/.venv/bin/python /tmp/migrate-searxng-settings.py /etc/searxng/settings.yml || true
|
|
exec /usr/local/searxng/entrypoint.sh
|
|
ports:
|
|
- "127.0.0.1:8080:8080"
|
|
volumes:
|
|
- searxng-data:/etc/searxng
|
|
- ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z
|
|
- ./scripts/migrate_searxng_settings.py:/tmp/migrate-searxng-settings.py:ro,z
|
|
environment:
|
|
- SEARXNG_BASE_URL=http://localhost:8080/
|
|
- SEARXNG_SECRET=${SEARXNG_SECRET:-}
|
|
# The official searxng image runs as the non-root `searxng` user, but its
|
|
# entrypoint still needs to chown /etc/searxng on first boot, drop privs via
|
|
# su-exec, and (with our wrapper above) write settings.yml into the named
|
|
# volume. Without these capabilities the wrapper aborts at the redirection
|
|
# with EACCES and the container fails its healthcheck with permission
|
|
# errors during setup. Mirrors the cap set recommended by the upstream
|
|
# searxng-docker compose file. See issue #721.
|
|
cap_drop:
|
|
- ALL
|
|
cap_add:
|
|
- CHOWN
|
|
- SETGID
|
|
- SETUID
|
|
- DAC_OVERRIDE
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\""]
|
|
interval: 5s
|
|
timeout: 6s
|
|
retries: 20
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
ntfy:
|
|
image: docker.io/binwiederhier/ntfy
|
|
command: serve
|
|
ports:
|
|
- "${NTFY_BIND:-127.0.0.1}:8091:80"
|
|
volumes:
|
|
- ntfy-cache:/var/cache/ntfy
|
|
environment:
|
|
- NTFY_BASE_URL=${NTFY_BASE_URL:-http://localhost:8091}
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
searxng-data:
|
|
chromadb-data:
|
|
ntfy-cache:
|