mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 16:02:20 +02:00
fix(security): keep the post-external-context approval gate on by default
Request authority admits whole tool families from the user's request, so a request to read email also admits send_email, delete_email and bulk_email, and agent processes inherit the host network. With the gate defaulting to off, an instruction injected through an email or a fetched page reaches those tools with no other check; dev refuses them today. Default the gate on, keep ODYSSEUS_TOOL_APPROVAL_GATE=0 as the opt-out, and pin the production default with a test that imports the module in a fresh interpreter. Four routing tests written for the opt-out posture now set it explicitly.
This commit is contained in:
@@ -1109,6 +1109,9 @@ def test_tui_local_workspace_turn_hides_backend_file_tools(monkeypatch):
|
||||
|
||||
|
||||
def test_native_host_shell_call_runs_through_bridge_and_threads_result(monkeypatch):
|
||||
# Routing test written for the opt-out posture; the gate default is pinned in
|
||||
# tests/test_tool_approval_gate_default.py.
|
||||
monkeypatch.setattr("src.tool_capabilities.TOOL_APPROVAL_GATE_ENABLED", False)
|
||||
monkeypatch.setattr(al, "get_setting", lambda key, default=None: default, raising=False)
|
||||
monkeypatch.setattr(al, "get_mcp_manager", lambda: None, raising=False)
|
||||
monkeypatch.setattr(al, "estimate_tokens", lambda *args, **kwargs: 10, raising=False)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Pin the production default of the post-external-context approval gate.
|
||||
|
||||
Every other gate test sets ``TOOL_APPROVAL_GATE_ENABLED`` explicitly, so none of
|
||||
them notices if the default flips. The flag is read once at import, so each
|
||||
case imports the module in a fresh interpreter with a controlled environment.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
|
||||
_PROBE = """
|
||||
import json
|
||||
from src.tool_capabilities import TOOL_APPROVAL_GATE_ENABLED, ToolRunSecurityContext
|
||||
context = ToolRunSecurityContext(external_untrusted_context_seen=True, external_sources=["read_email"])
|
||||
print(json.dumps({
|
||||
"enabled": TOOL_APPROVAL_GATE_ENABLED,
|
||||
"allowed": {tool: context.decision_for(tool, "{}").allowed
|
||||
for tool in ("bash", "send_email", "delete_email", "read_file")},
|
||||
}))
|
||||
"""
|
||||
|
||||
|
||||
def _probe(gate_value):
|
||||
env = {key: value for key, value in os.environ.items() if key != "ODYSSEUS_TOOL_APPROVAL_GATE"}
|
||||
if gate_value is not None:
|
||||
env["ODYSSEUS_TOOL_APPROVAL_GATE"] = gate_value
|
||||
out = subprocess.run(
|
||||
[sys.executable, "-c", _PROBE], cwd=REPO, env=env,
|
||||
capture_output=True, text=True, timeout=60, check=True,
|
||||
)
|
||||
return json.loads(out.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def test_gate_is_on_when_the_variable_is_unset():
|
||||
result = _probe(None)
|
||||
assert result["enabled"] is True
|
||||
assert result["allowed"] == {
|
||||
"bash": False, "send_email": False, "delete_email": False, "read_file": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["0", "false", "no", "off", " OFF "])
|
||||
def test_gate_can_be_turned_off_explicitly(value):
|
||||
result = _probe(value)
|
||||
assert result["enabled"] is False
|
||||
assert all(result["allowed"].values())
|
||||
@@ -1613,6 +1613,9 @@ def test_agent_loop_synthesizes_web_answer_after_tool_preamble(monkeypatch):
|
||||
|
||||
|
||||
def test_open_calendar_request_uses_ui_control_panel_not_event_dump(monkeypatch):
|
||||
# Routing test written for the opt-out posture; the gate default is pinned in
|
||||
# tests/test_tool_approval_gate_default.py.
|
||||
monkeypatch.setattr("src.tool_capabilities.TOOL_APPROVAL_GATE_ENABLED", False)
|
||||
_patch_loop_basics(monkeypatch)
|
||||
src = Path(__file__).resolve().parent.parent.joinpath("src", "agent_loop.py").read_text(encoding="utf-8")
|
||||
assert 'if isinstance(_ev, dict) and _ev.get("context_only"):' in src
|
||||
@@ -1679,6 +1682,9 @@ def test_open_calendar_request_uses_ui_control_panel_not_event_dump(monkeypatch)
|
||||
|
||||
|
||||
def test_calendar_create_response_includes_persistent_event_link(monkeypatch):
|
||||
# Routing test written for the opt-out posture; the gate default is pinned in
|
||||
# tests/test_tool_approval_gate_default.py.
|
||||
monkeypatch.setattr("src.tool_capabilities.TOOL_APPROVAL_GATE_ENABLED", False)
|
||||
_patch_loop_basics(monkeypatch)
|
||||
from src.user_time import clear_user_time_context, set_user_timezone
|
||||
|
||||
@@ -3505,6 +3511,9 @@ def test_notes_about_calendar_context_still_route_to_notes():
|
||||
|
||||
|
||||
def test_notes_panel_open_plus_create_keeps_both_tool_calls(monkeypatch):
|
||||
# Routing test written for the opt-out posture; the gate default is pinned in
|
||||
# tests/test_tool_approval_gate_default.py.
|
||||
monkeypatch.setattr("src.tool_capabilities.TOOL_APPROVAL_GATE_ENABLED", False)
|
||||
_patch_loop_basics(monkeypatch)
|
||||
seen_blocks = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user