From cdbcb44cc9f0c77a408291dfa645ed1f2aa8ae85 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:38:45 +0100 Subject: [PATCH] fix(runtime): handle synthetic requests without app scope and update env reference - Narrowly guard _request_privileges() in routes/chat_routes.py against synthetic requests lacking scope['app'] or auth manager state, safely returning empty privileges without granting agent privileges. - Add focused regression test in tests/test_context_resolution_route.py verifying that requests without app scope do not crash and cannot gain agent privileges or qualify for compact preview runtime. - Regenerate website/configuration-reference.md mechanically to align with current source line numbers. --- routes/chat_routes.py | 7 ++++++- tests/test_context_resolution_route.py | 22 ++++++++++++++++++++++ website/configuration-reference.md | 16 ++++++++-------- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/routes/chat_routes.py b/routes/chat_routes.py index ad126b17e..4ef072afa 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -148,7 +148,12 @@ def _turn_contract_enabled(*, exact_tool_approval, runtime_surface, def _request_privileges(request, user) -> Dict[str, Any]: """Per-user privileges from the app's auth manager; empty when unmanaged.""" - auth_manager = getattr(request.app.state, "auth_manager", None) + try: + app = getattr(request, "app", None) + except (AttributeError, KeyError): + app = None + state = getattr(app, "state", None) if app is not None else None + auth_manager = getattr(state, "auth_manager", None) if state is not None else None if not user or not auth_manager: return {} return auth_manager.get_privileges(user) or {} diff --git a/tests/test_context_resolution_route.py b/tests/test_context_resolution_route.py index 5ec173546..648f434a3 100644 --- a/tests/test_context_resolution_route.py +++ b/tests/test_context_resolution_route.py @@ -510,3 +510,25 @@ def test_compact_selection_rule_and_contract_stamp(): assert is_compact_preview_contract(SimpleNamespace(selection_mode=MODE)) assert not is_compact_preview_contract(SimpleNamespace(selection_mode="routed")) assert not is_compact_preview_contract(None) + + +def test_synthetic_request_without_app_scope_does_not_gain_agent_privilege(): + """A synthetic Request without scope['app'] must not crash or gain privileges.""" + from starlette.requests import Request + from routes.chat_routes import _request_privileges + from src.agent_runtime.runtime_selection import uses_compact_preview_runtime + + req = Request({"type": "http", "method": "POST", "path": "/api/chat_stream", "headers": []}) + privs = _request_privileges(req, "alice") + assert privs == {} + assert privs.get("can_use_agent") is None + + # Plain chat without app/auth state must not qualify for compact agent preview + selected = uses_compact_preview_runtime( + clean_route_requested=True, + turn_contract_enabled=True, + agent_mode=False, + agent_permitted=privs.get("can_use_agent", True), + image_generation=False, + ) + assert not selected diff --git a/website/configuration-reference.md b/website/configuration-reference.md index 160d9e7dd..63a5244be 100644 --- a/website/configuration-reference.md +++ b/website/configuration-reference.md @@ -65,15 +65,15 @@ The source tree reads **108** `ODYSSEUS_*` variables: 78 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:169` | Thinking policy for the Qwen routing step. An unrecognized value falls back to `auto`. | +| `ODYSSEUS_QWEN_ROUTE_THINKING` | `'auto'` | `src/agent_loop.py:170` | 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:15361` | How many video frames one tool result may contribute. Clamped to 1-8. | -| `ODYSSEUS_MAX_VISUAL_EVIDENCE_IMAGES` | `'1'` | `src/agent_loop.py:15329` | 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:15362` | How many video frames one tool result may contribute. Clamped to 1-8. | +| `ODYSSEUS_MAX_VISUAL_EVIDENCE_IMAGES` | `'1'` | `src/agent_loop.py:15330` | 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:931` | 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. | @@ -194,9 +194,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:3924` | 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:4129` | 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:2183` (+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: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. | ### Email @@ -221,8 +221,8 @@ Listed for completeness. Setting one of these on a real install is either a no-o | `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:7407` | 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:2080` | Truthy forces `sft_` accounts to UTC for deterministic batch generation. Interactive accounts still follow the browser timezone. | +| `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:2094` | 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. |