mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
Merge pull request #35 from o3LL/docs/env-configuration-reference
docs: generate the ODYSSEUS_* configuration reference from the source
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Odysseus UI — Environment Configuration
|
||||
# Copy this file to .env and fill in your values.
|
||||
#
|
||||
# This file stays deliberately short: it is for deployment-level overrides, and
|
||||
# most runtime configuration belongs in Settings inside the app. For the complete
|
||||
# list of ODYSSEUS_* variables the code reads, with the default each one falls
|
||||
# back to, see website/configuration-reference.md (generated from the source).
|
||||
|
||||
# ============================================================
|
||||
# LLM Configuration
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ PUBLIC_GUIDES = {
|
||||
"agent-migration.md",
|
||||
"attachments.md",
|
||||
"backup-restore.md",
|
||||
"configuration-reference.md",
|
||||
"email-outlook.md",
|
||||
"pr-blocker-audit.md",
|
||||
"security-ci.md",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Guards for the generated ODYSSEUS_* configuration reference.
|
||||
|
||||
`website/configuration-reference.md` is produced by
|
||||
`scripts/generate_env_reference.py`. The point of these tests is that adding a
|
||||
new `ODYSSEUS_*` read without documenting it fails the suite: the current state -
|
||||
most of the configuration surface undiscoverable - happened because nothing
|
||||
objected.
|
||||
|
||||
The generator's detection is exercised against a synthetic source tree rather
|
||||
than against the real one, so a test failure names a behavior rather than a
|
||||
count that drifted. Only the two whole-tree tests touch the repository, and they
|
||||
compare generator output to the committed page instead of asserting on source
|
||||
text.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.cli_loader import load_script
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
PAGE = REPO / "website" / "configuration-reference.md"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def generator():
|
||||
return load_script("generate_env_reference.py")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def built(generator):
|
||||
"""The generator run once against the real tree; reused by the slow tests."""
|
||||
return generator.build()
|
||||
|
||||
|
||||
def _write(root: Path, relative: str, body: str) -> None:
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(body, encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_tree(tmp_path, generator, monkeypatch):
|
||||
"""A miniature source tree covering every read pattern the generator claims."""
|
||||
monkeypatch.setattr(generator, "SOURCE_ROOTS", ("app.py", "src"))
|
||||
_write(tmp_path, "app.py", """
|
||||
import os
|
||||
|
||||
DIRECT = os.environ.get("ODYSSEUS_DIRECT_GET", "on")
|
||||
GETENV = os.getenv("ODYSSEUS_PLAIN_GETENV")
|
||||
SUBSCRIPT = os.environ["ODYSSEUS_SUBSCRIPT"]
|
||||
SPANNING = os.environ.get(
|
||||
"ODYSSEUS_SPANS_TWO_LINES", "spanned"
|
||||
)
|
||||
os.environ["ODYSSEUS_WRITE_ONLY"] = "1"
|
||||
""")
|
||||
_write(tmp_path, "src/indirect.py", '''
|
||||
import os
|
||||
|
||||
NAME_HELD_IN_CONSTANT = "ODYSSEUS_VIA_CONSTANT"
|
||||
FALLBACK = 7
|
||||
|
||||
value = os.environ.get(NAME_HELD_IN_CONSTANT, FALLBACK)
|
||||
|
||||
|
||||
def read_limit(name, default):
|
||||
"""An env-reader helper: the generator should follow calls to this."""
|
||||
raw = os.getenv(name)
|
||||
return default if raw is None else int(raw)
|
||||
|
||||
|
||||
LIMIT = read_limit("ODYSSEUS_VIA_HELPER", 5 * 1024)
|
||||
|
||||
|
||||
def flag(environ=None):
|
||||
source = os.environ if environ is None else environ
|
||||
return source.get("ODYSSEUS_VIA_MAPPING_ARG", "1")
|
||||
|
||||
|
||||
GENERATED = [
|
||||
"import os",
|
||||
"if os.environ.get('ODYSSEUS_INSIDE_A_STRING'): pass",
|
||||
]
|
||||
''')
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_finds_every_read_pattern_it_claims_to(generator, fake_tree):
|
||||
found = generator.collect(fake_tree)
|
||||
|
||||
assert set(found) == {
|
||||
"ODYSSEUS_DIRECT_GET",
|
||||
"ODYSSEUS_PLAIN_GETENV",
|
||||
"ODYSSEUS_SUBSCRIPT",
|
||||
"ODYSSEUS_SPANS_TWO_LINES",
|
||||
"ODYSSEUS_VIA_CONSTANT",
|
||||
"ODYSSEUS_VIA_HELPER",
|
||||
"ODYSSEUS_VIA_MAPPING_ARG",
|
||||
"ODYSSEUS_INSIDE_A_STRING",
|
||||
}
|
||||
|
||||
|
||||
def test_a_plain_grep_would_miss_what_the_extra_passes_find(generator, fake_tree):
|
||||
"""Pins why the generator is not a one-line grep."""
|
||||
naive = generator.naive_line_scan(fake_tree)
|
||||
found = set(generator.collect(fake_tree))
|
||||
|
||||
assert found - naive == {
|
||||
"ODYSSEUS_SPANS_TWO_LINES",
|
||||
"ODYSSEUS_VIA_CONSTANT",
|
||||
"ODYSSEUS_VIA_HELPER",
|
||||
"ODYSSEUS_VIA_MAPPING_ARG",
|
||||
}
|
||||
# And it over-counts in the other direction: a line-based scan cannot tell a
|
||||
# write from a read, which is why the page reports the intersection.
|
||||
assert naive - found == {"ODYSSEUS_WRITE_ONLY"}
|
||||
|
||||
|
||||
def test_records_defaults_and_locations_from_the_source(generator, fake_tree):
|
||||
found = generator.collect(fake_tree)
|
||||
|
||||
assert found["ODYSSEUS_DIRECT_GET"].primary.default == "'on'"
|
||||
assert found["ODYSSEUS_SPANS_TWO_LINES"].primary.default == "'spanned'"
|
||||
# One level of indirection is resolved: the name and the default both come
|
||||
# from module-level constants.
|
||||
assert found["ODYSSEUS_VIA_CONSTANT"].primary.default == "7"
|
||||
assert found["ODYSSEUS_VIA_HELPER"].primary.default == "5 * 1024"
|
||||
assert found["ODYSSEUS_PLAIN_GETENV"].primary.default is None
|
||||
|
||||
assert found["ODYSSEUS_DIRECT_GET"].primary.location == "app.py:4"
|
||||
assert found["ODYSSEUS_VIA_HELPER"].primary.path == "src/indirect.py"
|
||||
|
||||
|
||||
def test_environ_writes_are_not_reads(generator, fake_tree):
|
||||
assert "ODYSSEUS_WRITE_ONLY" not in generator.collect(fake_tree)
|
||||
|
||||
|
||||
def test_an_undocumented_variable_is_reported(generator, fake_tree):
|
||||
"""The whole point: a new variable with no notes entry must fail loudly."""
|
||||
problems = generator.check_notes(generator.collect(fake_tree))
|
||||
|
||||
assert problems
|
||||
offender = "ODYSSEUS_VIA_HELPER"
|
||||
assert any(offender in problem for problem in problems)
|
||||
assert any("VARIABLE_NOTES" in problem for problem in problems)
|
||||
assert any("src/indirect.py" in problem for problem in problems)
|
||||
|
||||
|
||||
def test_a_stale_notes_entry_is_reported(generator):
|
||||
problems = generator.check_notes({})
|
||||
|
||||
assert len(problems) == len(generator.VARIABLE_NOTES)
|
||||
assert all("no longer read anywhere" in problem for problem in problems)
|
||||
|
||||
|
||||
def test_renders_one_table_row_per_variable(generator, fake_tree, monkeypatch):
|
||||
monkeypatch.setitem(
|
||||
generator.VARIABLE_NOTES,
|
||||
"ODYSSEUS_VIA_HELPER",
|
||||
("Search", generator.USER, "A synthetic limit."),
|
||||
)
|
||||
found = {"ODYSSEUS_VIA_HELPER": generator.collect(fake_tree)["ODYSSEUS_VIA_HELPER"]}
|
||||
|
||||
page = generator.render(found, generator.naive_line_scan(fake_tree))
|
||||
|
||||
assert page.startswith("---\nlayout: default\n---\n")
|
||||
assert "| `ODYSSEUS_VIA_HELPER` | `5 * 1024` | `src/indirect.py:16` | A synthetic limit. |" in page
|
||||
assert "### Search" in page
|
||||
|
||||
|
||||
def test_every_variable_read_in_the_repository_is_documented(built):
|
||||
_, variables, problems = built
|
||||
|
||||
assert not problems, "\n".join(problems)
|
||||
assert variables, "expected the generator to find ODYSSEUS_* reads"
|
||||
|
||||
|
||||
def test_committed_page_matches_the_source(built):
|
||||
page, _, _ = built
|
||||
|
||||
assert PAGE.read_text(encoding="utf-8") == page, (
|
||||
"website/configuration-reference.md is stale - regenerate it with "
|
||||
"`python3 scripts/generate_env_reference.py`"
|
||||
)
|
||||
|
||||
|
||||
def test_page_has_no_emoji_or_other_non_ascii(built):
|
||||
page, _, _ = built
|
||||
|
||||
offenders = sorted({character for character in page if ord(character) > 126})
|
||||
assert not offenders, f"non-ASCII in the generated page: {offenders}"
|
||||
|
||||
|
||||
def test_check_mode_reports_a_stale_page(generator, tmp_path, monkeypatch):
|
||||
stale = tmp_path / "configuration-reference.md"
|
||||
stale.write_text("out of date\n", encoding="utf-8")
|
||||
monkeypatch.setattr(generator, "OUTPUT_PATH", stale)
|
||||
|
||||
assert generator.main(["--check"]) == 1
|
||||
assert generator.main([]) == 0
|
||||
assert generator.main(["--check"]) == 0
|
||||
|
||||
|
||||
def test_script_runs_as_a_subprocess_without_importing_the_app():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "scripts/generate_env_reference.py", "--check"],
|
||||
cwd=REPO, capture_output=True, text=True, timeout=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert re.search(r"\d+ variables", result.stdout), result.stdout
|
||||
|
||||
|
||||
def test_page_is_linked_from_the_places_a_reader_starts():
|
||||
assert "configuration-reference.md" in (REPO / "website" / "setup.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "configuration-reference.md" in (REPO / ".env.example").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Configuration reference: ODYSSEUS_* environment variables
|
||||
|
||||
<!-- This page is generated from the source tree by `scripts/generate_env_reference.py`. Do not edit it by hand: run the script instead. `tests/test_env_reference.py` fails when the committed page and the source disagree, or when a variable is read without an entry in the script's notes table. -->
|
||||
|
||||
Odysseus reads its runtime configuration from the Settings UI. The
|
||||
environment variables below are the deployment-level escape hatches underneath
|
||||
that: they are read directly from the process environment, mostly at import or
|
||||
startup, and most installs never need any of them.
|
||||
|
||||
`.env.example` stays a short, deployment-level example on purpose - `APP_BIND`,
|
||||
`APP_PORT`, `AUTH_ENABLED`, `DATABASE_URL` and a pre-seeded admin password. This
|
||||
page is the complete list, which is a different job.
|
||||
|
||||
Truthiness is not uniform across the codebase. Where a variable is described as
|
||||
"truthy" the read accepts `1`, `true`, `yes` and usually `on`; where it is
|
||||
described as a switch that turns something off, the read rejects `0`, `false`,
|
||||
`no` and `off` and treats everything else as on. The `Default` column is the
|
||||
value the code falls back to when the variable is unset, quoted from the source.
|
||||
|
||||
The source tree reads **98** `ODYSSEUS_*` variables: 78 an operator may want to set, and 20 that are internal - sentinels, fixture switches, capture hooks and development tooling. The internal ones are listed too, in their own section, so this page can be checked against the source mechanically.
|
||||
|
||||
> This page is generated. Edit `scripts/generate_env_reference.py` and
|
||||
> re-run it; `tests/test_env_reference.py` enforces that the committed page
|
||||
> matches the source.
|
||||
|
||||
## Variables you can set
|
||||
|
||||
### Deployment and first run
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_ADMIN_PASSWORD` | `''` | `setup.py:102` (+4 more) | Password for the admin account created on first run. Setup refuses a value shorter than its minimum length rather than silently falling back. |
|
||||
| `ODYSSEUS_ADMIN_USER` | `''` | `setup.py:101` (+2 more) | Username for the admin account created on first run. Setup uses env vars first, then an interactive prompt, then a random password. |
|
||||
| `ODYSSEUS_ALLOW_OLLAMA_CLI_SCAN` | *unset* | `routes/cookbook_helpers.py:544` (+1 more) | On Windows only, set truthy to let the Cookbook dependency probe shell out to `ollama list`. Ignored on other platforms, where the scan always runs. |
|
||||
| `ODYSSEUS_CONTAINER_NETWORK_MODE` | `''` | `app.py:1022` (+1 more) | Declares the container's Docker network mode. Set to `host` to skip host-gateway probing when discovering local model endpoints. |
|
||||
| `ODYSSEUS_ENABLE_HOST_DOCKER` | `''` | `src/host_docker_access.py:41` | Security-relevant. Must be exactly `true` before tools may use a mounted host Docker socket, and the socket itself must exist. |
|
||||
| `ODYSSEUS_INPROCESS_POLLERS` | `'1'` | `routes/email/email_pollers.py:1722` | The same off switch for the in-process email pollers, when `odysseus-mail poll-scheduled` is the sole external driver. |
|
||||
| `ODYSSEUS_INPROCESS_TASKS` | `'1'` | `app.py:1311` | Set to 0, false, no or off to stop the in-process scheduled-task runner, for deployments where an external worker drives task firing. |
|
||||
| `ODYSSEUS_MODEL_KEEPALIVE` | `''` | `app.py:1218` | Opt-in periodic model keep-alive pings. Off by default: the ping path runs model discovery, so stale LAN endpoints add background pressure. |
|
||||
| `ODYSSEUS_REQUIRE_TOOL_INDEX_READY` | `''` | `src/readiness.py:61` | Set truthy to make semantic tool-index readiness gate startup. Off by default so an install stays available on deterministic tool selection. |
|
||||
| `ODYSSEUS_SKIP_ADMIN_PROMPT` | *unset* | `setup.py:112` | Any non-empty value suppresses the interactive admin-credential prompt even on a TTY, for unattended installs. |
|
||||
| `ODYSSEUS_SLOW_REQUEST_LOG_SECONDS` | `'0.75'` | `app.py:239` | Request duration in seconds above which the middleware logs a slow-request warning. |
|
||||
| `ODYSSEUS_STARTUP_WARMUPS` | `''` | `app.py:1192` | Opt-in startup pings of the configured model endpoints. Off by default because they compete with the first seconds of UI use. |
|
||||
| `ODYSSEUS_TOOL_INDEX_PREWARM` | `'1'` | `src/tool_index.py:771` | Set to 0, false, no or off to skip background initialization of semantic tool retrieval at startup. |
|
||||
|
||||
### Data directories and paths
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_DATA_DIR` | `get_default_data_dir()` | `src/constants.py:56` (+1 more) | Root directory for every persisted file. Prefer this over the per-path overrides; the rest of `src/constants.py` derives from it. |
|
||||
| `ODYSSEUS_MAIL_ATTACHMENTS_DIR` | `os.path.join(DATA_DIR, 'mail-attachments')` | `src/constants.py:102` | Dedicated override for the mail attachment store, which otherwise lives under the data directory. |
|
||||
|
||||
### Model routing and providers
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_COPILOT_API_VERSION` | `'2026-06-01'` | `src/copilot.py:39` | Dated API-version header the Copilot models and chat endpoints require. |
|
||||
| `ODYSSEUS_COPILOT_CLIENT_ID` | `'01ab8ac9400c4e429b23'` | `src/copilot.py:34` | GitHub OAuth client id for the Copilot device flow. The default is the public VS Code client id; override it only with your own allow-listed app. |
|
||||
| `ODYSSEUS_DEEPSEEK_REASONING_EFFORT` | `'high'` | `src/llm_core.py:1727` | Reasoning effort for DeepSeek. Only `high` and `max` are accepted; any other value falls back to the default. |
|
||||
| `ODYSSEUS_FIRST_TOKEN_TIMEOUT` | `''` | `src/llm_core.py:223` | Seconds to wait for the first streamed token from a local endpoint before failing. Unset keeps the generous read timeout, which makes a stalled backend look like a hung agent. |
|
||||
| `ODYSSEUS_LOCAL_MODEL_GATE` | `'true'` | `src/llm_core.py:95` | On by default. Set 0, false, no or off to drop the gate that checks a local endpoint before routing a request to it. |
|
||||
| `ODYSSEUS_MISTRAL_REASONING_EFFORT` | `'high'` | `src/llm_core.py:1723` | Reasoning effort sent to Mistral thinking-capable models. The API accepts high, medium, low and none. |
|
||||
| `ODYSSEUS_MLX_IMAGE_VLM_MODEL` | *unset* | `scripts/mlx_image_server.py:299` | Vision-language model id for the MLX image server script. Required unless `--vlm-model` is passed on the command line. |
|
||||
| `ODYSSEUS_QWEN_ROUTE_THINKING` | `'auto'` | `src/agent_loop.py:166` | Thinking policy for the Qwen routing step. An unrecognized value falls back to `auto`. |
|
||||
|
||||
### Agent loop and tool execution
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_DISABLE_MCP` | `''` | `src/builtin_mcp.py:89` | Truthy disables MCP entirely, as an escape hatch for compatibility problems with a server. |
|
||||
| `ODYSSEUS_MAX_VISUAL_EVIDENCE_FRAMES` | `'3'` | `src/agent_loop.py:15360` | How many video frames one tool result may contribute. Clamped to 1-8. |
|
||||
| `ODYSSEUS_MAX_VISUAL_EVIDENCE_IMAGES` | `'1'` | `src/agent_loop.py:15328` | How many images one tool result may contribute to the model turn. Clamped to 1-8. |
|
||||
| `ODYSSEUS_MCP_ALLOWED_COMMANDS` | `''` | `src/agent_tools/admin_tools.py:140` | Security-relevant. Comma-separated allowlist of MCP launcher basenames the agent may start. Empty by default, and the deny list still wins. |
|
||||
| `ODYSSEUS_PYTHON_TOOL_SITE_PACKAGES` | `''` | `src/agent_tools/subprocess_tools.py:925` | Security-relevant. Absolute package roots, separated by the platform path separator, exposed to the sandboxed Python tool. Empty exposes none. |
|
||||
| `ODYSSEUS_SCRIPT_HOST` | `'localhost'` | `src/builtin_actions.py:919` | Default host for the run-script action. `localhost`, `127.0.0.1`, `local` and empty run locally; any other value runs over SSH. |
|
||||
| `ODYSSEUS_TOOL_APPROVAL_GATE` | `'0'` | `src/tool_capabilities.py:645` | Security-relevant. Truthy makes tool calls pass through the approval gate. Off by default. |
|
||||
|
||||
### Browser automation
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_BROWSER_EXECUTABLE` | `''` | `src/builtin_mcp.py:114` | Absolute path to the Chrome or Chromium binary. Empty searches the usual names, then lets Playwright MCP pick its own browser. |
|
||||
| `ODYSSEUS_BROWSER_ISOLATED` | `'1'` | `src/builtin_mcp.py:139` | Security-relevant. On by default, adding `--isolated` so each browser session starts clean. Set 0, false or no to keep a persistent profile. |
|
||||
| `ODYSSEUS_BROWSER_MCP_CACHE` | `os.path.join(base_dir, 'data', 'local', 'playwright-mcp-cache')` | `src/builtin_mcp.py:229` | Cache directory handed to the browser MCP server, so its npm download survives a container rebuild. |
|
||||
| `ODYSSEUS_BROWSER_MCP_REQUIRE_CACHE` | `''` | `src/builtin_mcp.py:90` | Truthy refuses to start the browser MCP server unless its npm package is already in the npx cache, instead of installing it at startup. |
|
||||
| `ODYSSEUS_BROWSER_NAMESPACE` | `'odysseus-ui'` | `src/agent_tools/web_tools.py:2355` (+5 more) | Namespace for the detached agent-browser daemon's pid files, so two runtimes on one machine do not terminate each other's browsers. |
|
||||
| `ODYSSEUS_BROWSER_NO_SANDBOX` | `'1'` | `src/builtin_mcp.py:142` | Security-relevant. On by default, adding `--no-sandbox` because the Docker image cannot use the Chromium sandbox. Set 0, false or no to keep it. |
|
||||
| `ODYSSEUS_BROWSER_SCREENSHOT_DIR` | *unset* | `src/agent_tools/web_tools.py:3010` | Where private-browser screenshots are written. Falls back to the container path, then the system temp directory. |
|
||||
|
||||
### Container and workspace mounts
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_WORKSPACE_CONTAINER_ROOT` | *unset* | `src/workspace_paths.py:31` | Container-side root that the host root maps onto. An `or` fallback, not a read default, supplies `/workspace` when it is unset. |
|
||||
| `ODYSSEUS_WORKSPACE_DEFAULT` | `''` | `routes/workspace_routes.py:97` | Default workspace path the admin-only workspace route reports. Empty means no default is configured. |
|
||||
| `ODYSSEUS_WORKSPACE_HOST_ROOT` | *unset* | `src/workspace_paths.py:29` | Single host-side root, paired with the container root below. Simpler than the explicit mount list when there is only one mount. |
|
||||
| `ODYSSEUS_WORKSPACE_MOUNTS` | `''` | `src/workspace_paths.py:19` | `host=container` path pairs separated by commas or semicolons, so the agent can translate a container path back to the host path a user typed. |
|
||||
|
||||
### Email
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_DOCUMENT_OWNER` | `''` | `mcp_servers/email_server.py:208` | Owner stamped on documents the email MCP server creates. Stdio MCP tools get no authenticated user, so without this a draft is invisible. |
|
||||
| `ODYSSEUS_IMAP_TIMEOUT_SECONDS` | *unset* | `routes/email/email_helpers.py:1163` | IMAP socket timeout in seconds, clamped to 5-300. A non-numeric value falls back to 30 rather than failing. |
|
||||
|
||||
### Calendar, notes and single-user mode
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_ALLOW_PRIVATE_CALDAV` | `'0'` | `src/caldav_sync.py:52` | Security-relevant. Truthy lets CalDAV sync reach private and link-local addresses. Off by default; this is an SSRF guard. |
|
||||
| `ODYSSEUS_FALLBACK_OWNER` | `'owner@localhost'` | `routes/calendar_routes.py:65` | Owner address that single-user mode attributes an unauthenticated request to. Only reachable while single-user mode is on. |
|
||||
| `ODYSSEUS_SINGLE_USER` | `'1'` | `routes/calendar_routes.py:66` | Security-relevant. On by default. Set to 0 on a real multi-user install so unauthenticated calendar writes are rejected rather than absorbed. |
|
||||
|
||||
### Upload and media limits
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_CHAT_UPLOAD_MAX_BYTES` | `10 * 1024 * 1024` | `src/upload_limits.py:33` | Maximum bytes accepted for a chat attachment. |
|
||||
| `ODYSSEUS_EDITOR_DRAFT_MAX_BYTES` | `256 * 1024 * 1024` | `src/upload_limits.py:47` | Maximum bytes accepted for a saved editor draft. |
|
||||
| `ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES` | `25 * 1024 * 1024` | `src/upload_limits.py:56` | Maximum bytes accepted for an attachment added while composing mail. |
|
||||
| `ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES` | `25 * 1024 * 1024` | `src/upload_limits.py:44` (+1 more) | Maximum bytes accepted for an image handed to a gallery transform. |
|
||||
| `ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES` | `100 * 1024 * 1024` | `src/upload_limits.py:41` (+1 more) | Maximum bytes accepted for a gallery upload. |
|
||||
| `ODYSSEUS_ICS_MAX_BYTES` | `10 * 1024 * 1024` | `src/upload_limits.py:62` | Maximum bytes accepted for an imported ICS file. |
|
||||
| `ODYSSEUS_MEDIA_FRAME_TIMEOUT` | `30` | `src/media_ingress.py:146` | Seconds allowed for extracting frames from a video before giving up. |
|
||||
| `ODYSSEUS_MEDIA_MAX_AUDIO_BYTES` | `32 * 1024 * 1024` | `src/media_ingress.py:129` | Largest source audio file the media pipeline will read. |
|
||||
| `ODYSSEUS_MEDIA_MAX_DIMENSION` | `1600` | `src/media_ingress.py:138` | Longest edge in pixels an image is resized down to before encoding. |
|
||||
| `ODYSSEUS_MEDIA_MAX_DOCUMENT_BYTES` | `32 * 1024 * 1024` | `src/media_ingress.py:126` | Largest source document the media pipeline will read. |
|
||||
| `ODYSSEUS_MEDIA_MAX_DOCUMENT_CHARS` | `24000` | `src/media_ingress.py:135` | How many characters of an ingested document are inlined into the turn. |
|
||||
| `ODYSSEUS_MEDIA_MAX_ENCODED_BYTES` | `24 * 1024 * 1024` | `src/media_ingress.py:132` | Budget for the encoded payload handed to the model, counted cumulatively across one turn's attachments rather than per file. |
|
||||
| `ODYSSEUS_MEDIA_MAX_FILES` | `4` | `src/media_ingress.py:119` | How many local media files one agent turn may ingest. |
|
||||
| `ODYSSEUS_MEDIA_MAX_IMAGE_BYTES` | `12 * 1024 * 1024` | `src/media_ingress.py:120` | Largest source image the media pipeline will read. |
|
||||
| `ODYSSEUS_MEDIA_MAX_PIXELS` | `40000000` | `src/media_ingress.py:139` | Total pixel budget for a source image, as a decompression-bomb guard. |
|
||||
| `ODYSSEUS_MEDIA_MAX_VIDEO_BYTES` | `128 * 1024 * 1024` | `src/media_ingress.py:123` | Largest source video the media pipeline will read. |
|
||||
| `ODYSSEUS_MEDIA_MAX_VIDEO_FRAMES` | `8` | `src/media_ingress.py:140` | How many frames are sampled from a video. |
|
||||
| `ODYSSEUS_MEDIA_PROBE_TIMEOUT` | `15` | `src/media_ingress.py:143` | Seconds allowed for probing a video's metadata before giving up. |
|
||||
| `ODYSSEUS_MEMORY_IMPORT_MAX_BYTES` | `10 * 1024 * 1024` | `src/upload_limits.py:50` (+1 more) | Maximum bytes accepted for a memory import file. |
|
||||
| `ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES` | `25 * 1024 * 1024` | `src/upload_limits.py:53` (+1 more) | Maximum bytes accepted for a personal-documents upload. |
|
||||
| `ODYSSEUS_STT_MAX_AUDIO_BYTES` | `25 * 1024 * 1024` | `src/upload_limits.py:59` | Maximum bytes accepted for an audio file submitted for transcription. |
|
||||
|
||||
### Search
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_SEARCH_PROVIDER` | `''` | `services/search/providers.py:43` | Forces the search provider, overriding the Settings value. Empty keeps the UI authoritative, which is what a normal install wants. |
|
||||
|
||||
### Memory and skills
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_SKILL_SEMANTIC_RETRIEVAL` | `'1'` | `services/memory/skills.py:789` | On by default. Set 0, false, no or off to fall back to keyword-only skill retrieval when no vector store is reachable. |
|
||||
| `ODYSSEUS_SKILL_SEMANTIC_THRESHOLD` | `'0.4'` | `services/memory/skills.py:800` | Minimum semantic score a skill needs to be retrieved. A non-numeric value falls back to the default. |
|
||||
|
||||
### Speech and vision models
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_GROUNDING_MODEL` | `'google/owlvit-base-patch32'` | `routes/gallery/gallery_routes.py:95` | Object-grounding model id the gallery loads for text-driven selection. |
|
||||
| `ODYSSEUS_SAM_MODEL` | `'facebook/sam-vit-base'` | `routes/gallery/gallery_routes.py:59` | Segmentation model id the gallery loads for subject selection. |
|
||||
| `ODYSSEUS_STT_MODEL` | *unset* | `src/agent_tools/media_tools.py:2184` | Default speech-to-text model for media transcription when the tool call does not name one. |
|
||||
| `ODYSSEUS_TTS_CACHE_MAX_BYTES` | `500 * 1024 * 1024` | `services/tts/tts_service.py:47` | Cap on the synthesized-speech cache. A non-numeric value falls back to the default. |
|
||||
|
||||
### Auth and internal API
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_INTERNAL_BASE` | *unset* | `src/constants.py:178` | Base URL the in-app tool layer uses for loopback HTTP calls. Set it when the app is not reachable at the port it thinks it is bound to. |
|
||||
| `ODYSSEUS_INTERNAL_TOKEN` | *unset* | `core/middleware.py:20` | Security-relevant. Token that lets the in-app tool layer reach admin-gated routes over loopback. Unset generates a fresh per-process token, which is what you want unless something outside the process needs the same value. |
|
||||
|
||||
### Integrations (Claude, Codex)
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_API_TOKEN` | `''` | `integrations/claude/skills/odysseus/scripts/odysseus_api.py:40` (+1 more) | API token those scripts authenticate with. Both this and the URL are required; the scripts name whichever is missing. |
|
||||
| `ODYSSEUS_URL` | `''` | `integrations/claude/skills/odysseus/scripts/odysseus_api.py:39` (+1 more) | Base URL of the Odysseus instance the bundled integration scripts call. |
|
||||
|
||||
## Internal and development-only variables
|
||||
|
||||
Listed for completeness. Setting one of these on a real install is either a no-op or a way to break something quietly.
|
||||
|
||||
### Model routing and providers
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_COPILOT_EDITOR_VERSION` | `'Odysseus/1.0'` | `src/copilot.py:54` | Editor-version header presented to the Copilot API. Kept stable on purpose. |
|
||||
| `ODYSSEUS_COPILOT_INTEGRATION_ID` | `'vscode-chat'` | `src/copilot.py:51` | Integration id presented to the Copilot API. Kept stable on purpose. |
|
||||
| `ODYSSEUS_COPILOT_USER_AGENT` | `'Odysseus/1.0'` | `src/copilot.py:48` | Editor-like User-Agent presented to the Copilot API. Kept stable on purpose. |
|
||||
| `ODYSSEUS_DEBUG_LLM_SHAPE` | `''` | `src/llm_core.py:3552` (+1 more) | Truthy logs the shape of streamed provider chunks. Debugging aid for provider response parsing. |
|
||||
|
||||
### Agent loop and tool execution
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_CAPTURE_MODEL_REQUESTS` | `''` | `src/agent_loop.py:3921` | Truthy writes model-request snapshots for local debugging. The marker file `/tmp/odysseus_capture_model_requests` enables the same thing. |
|
||||
| `ODYSSEUS_EXPOSE_RAW_BROWSER_MCP` | `''` | `src/agent_loop.py:4126` | Truthy stops hiding the raw Playwright MCP tools from agent prompts when the private-browser tool is available. |
|
||||
| `ODYSSEUS_TOOL_CONTRACT_ROOT` | `'<repo>/scripts'` | `src/clean_agent_preview.py:1952` (+1 more) | Directory holding the tool-contract scripts the clean-agent preview loads. The default is the repository's bundled scripts directory; set the variable to override it. |
|
||||
|
||||
### Email
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_EMAIL_FIXTURE` | *unset* | `mcp_servers/email_server.py:1122` (+7 more) | Exactly `1`, plus a fixture file on disk, makes the email MCP server serve fixtures instead of a real mailbox. |
|
||||
|
||||
### Testing, capture and development tooling
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_QA_TEACHER_ATTEMPTS` | `'3'` | `scripts/odysseus_conversation_qa.py:370` | Retry budget for the conversation-QA teacher model call. Clamped to 1-3. |
|
||||
| `ODYSSEUS_QA_TEACHER_TIMEOUT` | `'120'` | `scripts/odysseus_conversation_qa.py:372` | Timeout in seconds for that call. Clamped to 15-120. |
|
||||
| `ODYSSEUS_RUNTIME_REVISION` | `''` | `routes/chat_helpers.py:198` (+1 more) | Revision string stamped into each captured SFT trace record, so a trace can be tied back to the build that produced it. |
|
||||
| `ODYSSEUS_SFT_DISABLE_WORKSPACE_TOOLS` | `'1'` | `src/agent_loop.py:7404` | On by default. Keeps synthetic personal-assistant fixtures out of workspace mode; set 0, false, no or off to let them through. |
|
||||
| `ODYSSEUS_SFT_FORCE_UTC_TIMEZONE` | `'0'` | `routes/chat_routes.py:2070` | Truthy forces `sft_` accounts to UTC for deterministic batch generation. Interactive accounts still follow the browser timezone. |
|
||||
| `ODYSSEUS_SFT_TRACE_CAPTURE` | `'1'` | `routes/chat_helpers.py:161` (+1 more) | On by default, but only for owners whose name starts with `sft_`. Set 0, false, no or off to stop writing training traces. |
|
||||
| `ODYSSEUS_SFT_TRACE_DIR` | *unset* | `routes/chat_helpers.py:195` (+2 more) | Directory the SFT trace JSONL files are written to. Defaults to `sft_traces` under the data directory. |
|
||||
| `ODYSSEUS_SKIP_RUN_HINT` | *unset* | `setup.py:284` | Any non-empty value suppresses the `start the server with` hint at the end of setup. `start-macos.sh` sets it because it starts the server itself. |
|
||||
| `ODYSSEUS_TEST_STATIC_ORIGIN` | *unset* | `scripts/css_snapshot.py:249` (+6 more) | Origin an already-running static server is serving the repository from, so snapshot tooling reuses it instead of starting its own. |
|
||||
| `ODYSSEUS_TEST_STATIC_PORT` | *unset* | `tests/conftest.py:137` | Fixed port for the test suite's static server. Unset takes an ephemeral port, which is what keeps parallel runs from colliding. |
|
||||
|
||||
### Build and release metadata
|
||||
|
||||
| Variable | Default | Read in | What it does |
|
||||
|---|---|---|---|
|
||||
| `ODYSSEUS_BUILD_VERSION` | `''` | `src/constants.py:15` | Overrides the build-version string the API and UI report, without touching the public application version. |
|
||||
| `ODYSSEUS_SOURCE_COMMIT` | `''` | `src/constants.py:33` | Overrides the source commit reported for runtime provenance, for builds that ship without a git directory. |
|
||||
|
||||
## How this page is generated
|
||||
|
||||
The generator walks the Python sources under `app.py`, `launcher.py`, `setup.py`, `companion`, `config`, `core`, `integrations`, `mcp_servers`, `routes`, `scripts`, `services`, `src`, `tests` and finds
|
||||
reads three ways, because no single pattern covers the codebase:
|
||||
|
||||
- Direct reads: `os.getenv(...)`, and any `.get` / `.setdefault` / `.pop` call or
|
||||
subscript keyed by an `ODYSSEUS_*` literal, including names held in a
|
||||
module-level constant. The receiver is not required to be `os.environ`, because
|
||||
several call sites read through a mapping passed in as an argument
|
||||
(`src/tool_index.py`, `src/host_docker_access.py`).
|
||||
- Calls to env-reader helpers - any function that forwards one of its own
|
||||
parameters to an environment read. This is detected rather than hardcoded, so a
|
||||
new helper needs no change here. It is what finds the upload caps in
|
||||
`src/upload_limits.py` and the media-ingress overrides in
|
||||
`src/media_ingress.py`.
|
||||
- A regex sweep of the raw file text, for reads the AST cannot see.
|
||||
`routes/cookbook_helpers.py` builds an Ollama probe script as a list of source
|
||||
lines, so one read lives inside a string literal.
|
||||
|
||||
The three passes are not redundancy. A line-based grep for a direct
|
||||
`os.environ.get("ODYSSEUS_...` call finds 70 of the 98 variables on this
|
||||
page. What it misses is reads through an env-reader helper, reads whose call
|
||||
spans more than one line, reads whose variable name is held in a module
|
||||
constant, and reads through a mapping passed in as an argument - which is the
|
||||
whole reason this page is generated rather than maintained.
|
||||
|
||||
The `Default` column shows the expression as written, with one level of
|
||||
indirection resolved: a module-level constant and a dataclass field default are
|
||||
replaced by the literal they hold, so `defaults.max_media_files` shows as `4`.
|
||||
Anything computed at import time - `get_default_data_dir()` - is shown as
|
||||
written, because that is the honest answer. A few call sites supply their
|
||||
fallback with `or` rather than a default argument; those show as *unset* and say
|
||||
so in the last column.
|
||||
|
||||
Regenerate it with:
|
||||
|
||||
```bash
|
||||
python3 scripts/generate_env_reference.py
|
||||
```
|
||||
@@ -718,6 +718,12 @@ Key settings:
|
||||
|
||||
All upload-limit vars are validated (must be a positive integer) and optional; an invalid value fails fast at startup.
|
||||
|
||||
The table above is the short list. The source tree reads a lot more `ODYSSEUS_*`
|
||||
variables than this - browser automation, model routing, workspace mounts, media
|
||||
limits, a few security switches - and the complete list, generated from the code
|
||||
with the default each one falls back to, is in the
|
||||
[configuration reference](configuration-reference.md).
|
||||
|
||||
### 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.
|
||||
|
||||
Reference in New Issue
Block a user