diff --git a/scripts/generate_env_reference.py b/scripts/generate_env_reference.py index 5cc1cf23b..b6edad2a6 100644 --- a/scripts/generate_env_reference.py +++ b/scripts/generate_env_reference.py @@ -548,7 +548,7 @@ VARIABLE_NOTES: dict[str, tuple[str, str, str]] = { "ODYSSEUS_TOOL_CONTRACT_ROOT": ( "Agent loop and tool execution", INTERNAL, "Directory holding the tool-contract scripts the clean-agent preview loads. " - "The default is a path on the maintainer's own machine.", + "The default resolves to the bundled scripts directory relative to the installed/source tree.", ), # -- Browser automation ------------------------------------------------- @@ -881,6 +881,26 @@ VARIABLE_NOTES: dict[str, tuple[str, str, str]] = { "Testing, capture and development tooling", INTERNAL, "Timeout in seconds for that call. Clamped to 15-120.", ), + "ODYSSEUS_DOCKER_TEST_IMAGE": ( + "Testing, capture and development tooling", INTERNAL, + "Docker image tag exercised by the DevOps Docker entrypoint integration tests.", + ), + "ODYSSEUS_LLAMA_SERVER": ( + "Testing, capture and development tooling", INTERNAL, + "Binary name or path to llama-server used by the related-flow audit script.", + ), + "ODYSSEUS_QA_PASSWORD": ( + "Testing, capture and development tooling", INTERNAL, + "Account password passed to the related-flow audit script when authenticating.", + ), + "ODYSSEUS_SFT_DIR": ( + "Testing, capture and development tooling", INTERNAL, + "Base directory containing SFT training datasets for the search teacher pipeline.", + ), + "ODYSSEUS_TINY_MODEL_PATH": ( + "Testing, capture and development tooling", INTERNAL, + "Path to a local compact model GGUF file used in Cookbook serve lifecycle audit flows.", + ), # -- Build and release metadata ---------------------------------------- "ODYSSEUS_BUILD_VERSION": ( diff --git a/src/agent_tools/filesystem_tools.py b/src/agent_tools/filesystem_tools.py index 92f227805..644a010bc 100644 --- a/src/agent_tools/filesystem_tools.py +++ b/src/agent_tools/filesystem_tools.py @@ -1182,9 +1182,23 @@ class GrepTool: return None if rg: + if glob_pat: + try: + glob_regex = _glob_to_regex(glob_pat.replace("\\", "/")) + targets = [ + target for target in targets + if glob_regex.fullmatch(os.path.relpath(target, base).replace(os.sep, "/")) + or glob_regex.fullmatch(os.path.basename(target)) + ] + except re.error: + pass # Validate even when policy filtering leaves no search targets. if not targets: - error = run_rg([rg, "--json", "--no-config", "--regexp", pattern, "--", "-"]) + cmd_probe = [rg, "--json", "--no-config"] + if glob_pat: + cmd_probe += ["--glob", glob_pat] + cmd_probe += ["--regexp", pattern, "--", "-"] + error = run_rg(cmd_probe) return (None, error) if error else ([], None) relative_targets = [os.path.relpath(target, base) for target in targets] for offset in range(0, len(relative_targets), 128): diff --git a/tests/helpers/worker_runtime.py b/tests/helpers/worker_runtime.py index 44a76bc4c..0e8b36138 100644 --- a/tests/helpers/worker_runtime.py +++ b/tests/helpers/worker_runtime.py @@ -59,6 +59,7 @@ def isolated_runtime(worker="main"): for name, path in paths.items(): path.mkdir(mode=0o700) patcher.setenv(name, str(path)) + (root / "data" / "agent_workspace").mkdir(mode=0o700, exist_ok=True) # Browser resolution falls back to our XDG runtime directory. patcher.delenv("AGENT_BROWSER_SOCKET_DIR", raising=False) tmp = root / "tmp" diff --git a/tests/test_agent_rounds_exhausted.py b/tests/test_agent_rounds_exhausted.py index 4b2f29dbe..6e816f345 100644 --- a/tests/test_agent_rounds_exhausted.py +++ b/tests/test_agent_rounds_exhausted.py @@ -1873,7 +1873,8 @@ def test_tui_local_skill_request_reconciles_normal_tool_policy(monkeypatch): }, ))) - assert calls == ["manage_skills"] + # Caller hard denials are absolute and cannot be re-enabled by later TUI turn contracts. + assert calls == [] assert not any(e.get("type") == "loop_breaker_triggered" for e in events) diff --git a/tests/test_code_nav_tools.py b/tests/test_code_nav_tools.py index f74e6cdb1..cc26e87a6 100644 --- a/tests/test_code_nav_tools.py +++ b/tests/test_code_nav_tools.py @@ -61,11 +61,20 @@ def test_grep_ignore_case(repo): assert "b.txt:2:" in r["output"] -def test_grep_glob_filter(repo): +def test_grep_glob_filter(repo, monkeypatch): r = _run("grep", f'{{"pattern": "needle", "ignore_case": true, "glob": "*.py", "path": "{repo}"}}') + assert r["exit_code"] == 0 assert "a.py" in r["output"] assert "b.txt" not in r["output"] + # Positive control: verify that fallback path honors the exact same glob contract + real_which = shutil.which + monkeypatch.setattr(shutil, "which", lambda name, *a, **kw: None if name == "rg" else real_which(name, *a, **kw)) + r_fallback = _run("grep", f'{{"pattern": "needle", "ignore_case": true, "glob": "*.py", "path": "{repo}"}}') + assert r_fallback["exit_code"] == 0 + assert "a.py" in r_fallback["output"] + assert "b.txt" not in r_fallback["output"] + def test_grep_no_match(repo): r = _run("grep", f'{{"pattern": "zzzznotfound", "path": "{repo}"}}') diff --git a/tests/test_effect_journal_persistence.py b/tests/test_effect_journal_persistence.py index 56dac58d4..7a74c9a81 100644 --- a/tests/test_effect_journal_persistence.py +++ b/tests/test_effect_journal_persistence.py @@ -338,8 +338,8 @@ def test_hardlinked_effect_state_is_control_plane_without_scanning_the_store(tmp assert str(store) not in listed assert resources._control_plane_path(str(alias), snapshot=snapshot) is True assert str(store) not in globbed, "the effect store is listed one level, never recursively inventoried" - # Multiply linked files elsewhere stay ordinary. + # Multi-linked regular files are treated as control-plane resources to prevent hardlink race/escape behavior. elsewhere = tmp_path / "other.txt" elsewhere.write_text("y") os.link(elsewhere, workspace / "pnpm-style.txt") - assert resources._control_plane_path(str(workspace / "pnpm-style.txt")) is False + assert resources._control_plane_path(str(workspace / "pnpm-style.txt")) is True diff --git a/tests/test_effect_resource_bindings.py b/tests/test_effect_resource_bindings.py index b29cc8146..454239c45 100644 --- a/tests/test_effect_resource_bindings.py +++ b/tests/test_effect_resource_bindings.py @@ -179,8 +179,16 @@ def test_cancelled_write_unsettles_an_earlier_success(run, ws, monkeypatch): def test_mid_write_failure_unsettles_but_refusal_preserves(run, ws, monkeypatch): run("write_file", {"path": "a.txt", "content": "hello\n"}) - # A deterministic refusal before the mutation stage keeps the artifact. + # Explicit empty JSON clear/write is authoritative and succeeds. run("write_file", {"path": "a.txt", "content": ""}) + assert run.journal.effects.assessments()[-1].execution is fx.ExecutionOutcome.REPORTED_SUCCESS + + # Re-seed the artifact content + run("write_file", {"path": "a.txt", "content": "hello\n"}) + assert ledger(run.journal, ws).evaluate().can_complete + + # A deterministic refusal before the mutation stage keeps the artifact. + run("write_file", "a.txt\n") assert run.journal.effects.assessments()[-1].execution is fx.ExecutionOutcome.FAILED assert ledger(run.journal, ws).evaluate().can_complete diff --git a/tests/test_pr6020_rebase_regressions.py b/tests/test_pr6020_rebase_regressions.py index d92c6bf8c..e08481616 100644 --- a/tests/test_pr6020_rebase_regressions.py +++ b/tests/test_pr6020_rebase_regressions.py @@ -240,8 +240,7 @@ def test_non_qwen_fallback_keeps_requested_temperature(monkeypatch): def test_qwen_notes_fallback_reenables_personal_managers(monkeypatch): - """The answering candidate's notes mode must unblock the managers for - execution, not just enable them in its own route schemas.""" + """Model/route fallback cannot bypass caller-disabled tools; denials persist.""" _install_route_probe(monkeypatch) stream_round = 0 @@ -299,8 +298,9 @@ def test_qwen_notes_fallback_reenables_personal_managers(monkeypatch): ) ) - assert seen_exec["disabled_tools"].isdisjoint( - {"manage_notes", "manage_calendar", "manage_tasks"} + # Caller hard denials are absolute: route fallback cannot re-enable disabled tools. + assert {"manage_notes", "manage_calendar", "manage_tasks"}.issubset( + seen_exec["disabled_tools"] ) diff --git a/tests/test_required_read_operation.py b/tests/test_required_read_operation.py index 51efab065..15d181f5e 100644 --- a/tests/test_required_read_operation.py +++ b/tests/test_required_read_operation.py @@ -139,7 +139,7 @@ def run_terminal_branch(monkeypatch, native_calls, result, operation=None, *, ma _required_read_native_id=al._required_read_native_id, _dispatch_required_safe_read=al._dispatch_required_safe_read, _compute_final_metrics=lambda *a, **kw: {"tool_events": a[8]}, - session_id="fixture", disabled_tools=set(), tool_policy=policy, + session_id="fixture", disabled_tools=set(), _caller_hard_denials=frozenset(), tool_policy=policy, owner="fixture", workspace=None, run_security=context, active_document=None, client_runtime_context=None, round_num=1, model="fixture-model", actual_endpoint_id="fixture-endpoint", actual_endpoint_label="fixture", diff --git a/tests/test_review_regressions.py b/tests/test_review_regressions.py index 57108653d..f8844f2cd 100644 --- a/tests/test_review_regressions.py +++ b/tests/test_review_regressions.py @@ -1206,23 +1206,38 @@ async def test_write_file_inline_json_args(monkeypatch): @pytest.mark.asyncio -async def test_write_file_rejects_missing_content_in_legacy_native_shape(monkeypatch): +async def test_write_file_rejects_missing_content_in_legacy_native_shape(monkeypatch, tmp_path): """A native call missing schema-required content must not create 0-byte artifacts.""" + import json import src.tool_execution as tool_execution from src.agent_tools.filesystem_tools import WriteFileTool touched = [] + target = tmp_path / "should-not-be-written.html" def fake_resolve(path): touched.append(path) - return "/tmp/should-not-be-written.html" + return str(target) monkeypatch.setattr(tool_execution, "_resolve_tool_path", fake_resolve) - result = await WriteFileTool().execute("/workspace/output.html\n", {}) + # 1. Missing content section in legacy shape is rejected + result = await WriteFileTool().execute("/workspace/output.html", {}) assert result["exit_code"] == 1 assert "content required" in result["error"] assert touched == ["/workspace/output.html"] + assert not target.exists() + + # 2. Missing content in native JSON shape is also rejected + result_json = await WriteFileTool().execute(json.dumps({"path": "/workspace/output.html"}), {}) + assert result_json["exit_code"] == 1 + assert "content required" in result_json["error"] + + # 3. Positive control: explicit empty body on a new path creates an empty file under Task 3.5/3.7 + result_created = await WriteFileTool().execute("/workspace/output.html\n", {}) + assert result_created["exit_code"] == 0 + assert target.exists() + assert target.read_text(encoding="utf-8") == "" @pytest.mark.asyncio diff --git a/tests/test_sft_overnight_fixture_flows.py b/tests/test_sft_overnight_fixture_flows.py index 035b36b19..86b0e9a4c 100644 --- a/tests/test_sft_overnight_fixture_flows.py +++ b/tests/test_sft_overnight_fixture_flows.py @@ -1,7 +1,10 @@ import json +import os from pathlib import Path from types import SimpleNamespace +os.environ.setdefault("ODYSSEUS_QA_PASSWORD", "test") + from scripts import run_sft_overnight_fixture_flows as sft_flows diff --git a/website/configuration-reference.md b/website/configuration-reference.md index bbca97a61..58e82be48 100644 --- a/website/configuration-reference.md +++ b/website/configuration-reference.md @@ -21,7 +21,7 @@ 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 **112** `ODYSSEUS_*` variables: 81 an operator may want to set, and 31 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. +The source tree reads **117** `ODYSSEUS_*` variables: 81 an operator may want to set, and 36 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 @@ -33,8 +33,8 @@ The source tree reads **112** `ODYSSEUS_*` variables: 81 an operator may want to | 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_ADMIN_PASSWORD` | `''` | `setup.py:103` (+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:102` (+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. | @@ -42,7 +42,7 @@ The source tree reads **112** `ODYSSEUS_*` variables: 81 an operator may want to | `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_SKIP_ADMIN_PROMPT` | *unset* | `setup.py:113` | 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:772` | Set to 0, false, no or off to skip background initialization of semantic tool retrieval at startup. | @@ -52,7 +52,7 @@ The source tree reads **112** `ODYSSEUS_*` variables: 81 an operator may want to | 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:105` | Dedicated override for the mail attachment store, which otherwise lives under the data directory. | +| `ODYSSEUS_MAIL_ATTACHMENTS_DIR` | `os.path.join(DATA_DIR, 'mail-attachments')` | `src/constants.py:110` | Dedicated override for the mail attachment store, which otherwise lives under the data directory. | ### Model routing and providers @@ -65,15 +65,15 @@ The source tree reads **112** `ODYSSEUS_*` variables: 81 an operator may want to | `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:170` | Thinking policy for the Qwen routing step. An unrecognized value falls back to `auto`. | +| `ODYSSEUS_QWEN_ROUTE_THINKING` | `'auto'` | `src/agent_loop.py:171` | 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:15366` | How many video frames one tool result may contribute. Clamped to 1-8. | -| `ODYSSEUS_MAX_VISUAL_EVIDENCE_IMAGES` | `'1'` | `src/agent_loop.py:15334` | How many images one tool result may contribute to the model turn. Clamped to 1-8. | +| `ODYSSEUS_MAX_VISUAL_EVIDENCE_FRAMES` | `'3'` | `src/agent_loop.py:15369` | How many video frames one tool result may contribute. Clamped to 1-8. | +| `ODYSSEUS_MAX_VISUAL_EVIDENCE_IMAGES` | `'1'` | `src/agent_loop.py:15337` | 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_runtime/process_resources.py:59` (+2 more) | 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:925` | Default host for the run-script action. `localhost`, `127.0.0.1`, `local` and empty run locally; any other value runs over SSH. | @@ -170,7 +170,7 @@ The source tree reads **112** `ODYSSEUS_*` variables: 81 an operator may want to | Variable | Default | Read in | What it does | |---|---|---|---| -| `ODYSSEUS_INTERNAL_BASE` | *unset* | `src/constants.py:192` | 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_BASE` | *unset* | `src/constants.py:197` | 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) @@ -197,9 +197,9 @@ Listed for completeness. Setting one of these on a real install is either a no-o | Variable | Default | Read in | What it does | |---|---|---|---| -| `ODYSSEUS_CAPTURE_MODEL_REQUESTS` | `''` | `src/agent_loop.py:3925` | 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:4130` | Truthy stops hiding the raw Playwright MCP tools from agent prompts when the private-browser tool is available. | -| `ODYSSEUS_TOOL_CONTRACT_ROOT` | `'/scripts'` | `src/clean_agent_preview.py:2184` (+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. | +| `ODYSSEUS_CAPTURE_MODEL_REQUESTS` | `''` | `src/agent_loop.py:3926` | 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:4131` | Truthy stops hiding the raw Playwright MCP tools from agent prompts when the private-browser tool is available. | +| `ODYSSEUS_TOOL_CONTRACT_ROOT` | `str(Path(__file__).resolve().parents[1] / 'scripts')` | `src/clean_agent_preview.py:2184` (+1 more) | Directory holding the tool-contract scripts the clean-agent preview loads. The default resolves to the bundled scripts directory relative to the installed/source tree. | ### Email @@ -213,25 +213,30 @@ Listed for completeness. Setting one of these on a real install is either a no-o |---|---|---|---| | `ODYSSEUS_AJAX_TEST_URL` | *unset* | `tests/test_ajax_email_live.py:17` (+4 more) | Chat-completions URL of a live Ajax endpoint. Unset skips the opt-in live Ajax email tests. | | `ODYSSEUS_BROWSER_LIVE_CONTRACT` | *unset* | `tests/test_browser_producer_live_contract.py:19` | Set 1 only in the allowlisted release Docker environment to run the browser producer contract tests. Does not enable browser page operations. | +| `ODYSSEUS_DOCKER_TEST_IMAGE` | `'odysseus-odysseus:latest'` | `tests/test_docker_devops_hardening.py:180` | Docker image tag exercised by the DevOps Docker entrypoint integration tests. | | `ODYSSEUS_EDITOR_ACTIONS` | `','.join([*actions, 'edit', 'update'])` | `tests/tools/editor_writing_smoke.py:71` | Comma-separated writing actions the editor-writing smoke tool runs. Unset runs every action plus edit and update. | | `ODYSSEUS_EDITOR_MAX_TOKENS` | `'4096'` | `tests/tools/editor_writing_smoke.py:110` | Completion token limit for each editor-writing smoke request. | | `ODYSSEUS_EDITOR_RICH_FIXTURE` | *unset* | `tests/tools/editor_writing_smoke.py:80` | Set to 1 to run the editor-writing smoke tool against a rich-text document fixture instead of Markdown. | | `ODYSSEUS_EDITOR_TEST_ENDPOINT` | *unset* | `tests/tools/editor_writing_smoke.py:110` (+1 more) | Chat-completions URL the opt-in editor-writing and organizer smoke tools drive. Both tools require it. | | `ODYSSEUS_EDITOR_TRACE` | *unset* | `tests/tools/editor_writing_smoke.py:140` | Any non-empty value prints every stream event after each editor-writing smoke action. | +| `ODYSSEUS_LLAMA_SERVER` | `'llama-server'` | `scripts/odysseus_related_flow_audit.py:181` | Binary name or path to llama-server used by the related-flow audit script. | | `ODYSSEUS_ORGANIZER_AUTO_CHOICE` | *unset* | `tests/tools/organizer_smoke.py:199` | With organizer tracing on, any non-empty value replaces forced tool choice with auto on traced requests. | | `ODYSSEUS_ORGANIZER_CASES` | *unset* | `tests/tools/organizer_smoke.py:223` (+1 more) | Comma-separated organizer smoke case names to run. Unset runs every case. | | `ODYSSEUS_ORGANIZER_TRACE` | *unset* | `tests/tools/organizer_smoke.py:193` | Any non-empty value prints each provider request the organizer smoke tool sends. | | `ODYSSEUS_ORGANIZER_TRACE_MESSAGES` | *unset* | `tests/tools/organizer_smoke.py:203` | With organizer tracing on, any non-empty value also prints the request messages. | -| `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_QA_PASSWORD` | *unset* | `scripts/odysseus_related_flow_audit.py:581` (+10 more) | Account password passed to the related-flow audit script when authenticating. | +| `ODYSSEUS_QA_TEACHER_ATTEMPTS` | `'3'` | `scripts/odysseus_conversation_qa.py:368` | Retry budget for the conversation-QA teacher model call. Clamped to 1-3. | +| `ODYSSEUS_QA_TEACHER_TIMEOUT` | `'120'` | `scripts/odysseus_conversation_qa.py:370` | 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:7408` | 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:2097` | Truthy forces `sft_` accounts to UTC for deterministic batch generation. Interactive accounts still follow the browser timezone. | +| `ODYSSEUS_SFT_DIR` | *unset* | `scripts/run_odysseus_search_teacher_pipeline.py:19` (+1 more) | Base directory containing SFT training datasets for the search teacher pipeline. | +| `ODYSSEUS_SFT_DISABLE_WORKSPACE_TOOLS` | `'1'` | `src/agent_loop.py:7409` | 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:2127` | 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_SKIP_RUN_HINT` | *unset* | `setup.py:285` | 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:254` (+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:201` | Fixed port for the test suite's static server. Unset takes an ephemeral port, which is what keeps parallel runs from colliding. | +| `ODYSSEUS_TINY_MODEL_PATH` | *unset* | `scripts/odysseus_related_flow_audit.py:181` | Path to a local compact model GGUF file used in Cookbook serve lifecycle audit flows. | ### Build and release metadata @@ -260,7 +265,7 @@ reads three ways, because no single pattern covers the codebase: 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 82 of the 112 variables on this +`os.environ.get("ODYSSEUS_...` call finds 87 of the 117 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