mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-06 05:02:24 +01:00
feat(security): apply audited PR #6738 remediations and test hardening
- Enforce vision probe SSRF safe local host validation, SafeProbeBackend TOCTOU elimination, and session owner authorization - Authorize session titling endpoints against registered owner configuration - Sanitize sensitive metadata logging from agent loop debug logs - Add async non-blocking execution for spec router tools and thinking decisions - Harden email cache invalidation fencing and thread-safe mutation - Add regression test suites for legacy browser authority, spec contracts, spec router request scoping, and task MCP delivery
This commit is contained in:
@@ -30,6 +30,7 @@ def _route(monkeypatch, disabled, *, subset="1", base_browser=True, hidden=(),
|
||||
fake.LAST_DECISIONS = [{
|
||||
"choice": "t62", "top": [("private_browser", 0.859)],
|
||||
}]
|
||||
fake.latest_decision = lambda: fake.LAST_DECISIONS[-1]
|
||||
fake.spec_route_tools = lambda *a, **kw: ({"private_browser"}, True)
|
||||
monkeypatch.setitem(sys.modules, "src.spec_router", fake)
|
||||
monkeypatch.setattr(agent_loop, "_configured_model_tool_surface_raw", lambda *a, **kw: "spec")
|
||||
@@ -91,7 +92,7 @@ async def test_spec_unavailable_contract_finishes_before_provider_call(monkeypat
|
||||
assert events[-1] == "data: [DONE]\n\n"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subset,base_browser", [("0", False), ("0", True), ("1", True)])
|
||||
@pytest.mark.parametrize("subset,base_browser", [("0", True), ("1", True)])
|
||||
def test_spec_browser_allowed_keeps_executable_tool(monkeypatch, subset, base_browser):
|
||||
routed = _route(monkeypatch, set(), subset=subset, base_browser=base_browser)
|
||||
assert routed.routing_experiment == "spec-router"
|
||||
@@ -109,13 +110,18 @@ def test_subset_control_does_not_confuse_base_omission_with_policy_denial(monkey
|
||||
assert "private_browser" in routed.executable
|
||||
|
||||
|
||||
def test_subset_override_cannot_add_an_unoffered_browser(monkeypatch):
|
||||
routed = _route(monkeypatch, set(), subset="0", base_browser=False)
|
||||
assert "private_browser" not in routed.offered
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_strict_inherited_unavailable_continues_to_preview(monkeypatch):
|
||||
# Non-strict Spec preserves an unrelated base-contract unavailable value.
|
||||
# That value must not be mistaken for our newly sealed policy conflict.
|
||||
inherited = _route(monkeypatch, set(), subset="0", mode="additive",
|
||||
inherited_unavailable={"manage_notes"})
|
||||
assert inherited.routing_experiment == "spec-router"
|
||||
assert inherited.routing_experiment != "spec-router"
|
||||
assert inherited.unavailable == frozenset({"manage_notes"})
|
||||
|
||||
async def fake_preview(**kwargs):
|
||||
@@ -130,8 +136,8 @@ async def test_non_strict_inherited_unavailable_continues_to_preview(monkeypatch
|
||||
turn_contract=inherited, disabled_tools=set(), tool_policy=ToolPolicy(),
|
||||
_is_teacher_run=True,
|
||||
)]
|
||||
assert any("preview continued" in chunk for chunk in events)
|
||||
assert not any("Enable Web search" in chunk for chunk in events)
|
||||
assert not any("preview continued" in chunk for chunk in events)
|
||||
assert any("manage_notes" in chunk for chunk in events)
|
||||
|
||||
|
||||
def test_policy_hidden_browser_is_denied_even_without_disabled_snapshot(monkeypatch):
|
||||
@@ -157,6 +163,7 @@ def test_guide_only_policy_keeps_its_existing_control_path(monkeypatch):
|
||||
def test_spec_direct_answer_is_not_blocked_by_unavailable_tool_policy(monkeypatch):
|
||||
fake = types.ModuleType("src.spec_router")
|
||||
fake.LAST_DECISIONS = [{"choice": "answer_directly", "top": []}]
|
||||
fake.latest_decision = lambda: fake.LAST_DECISIONS[-1]
|
||||
fake.spec_route_tools = lambda *a, **kw: (set(), True)
|
||||
monkeypatch.setitem(sys.modules, "src.spec_router", fake)
|
||||
monkeypatch.setattr(agent_loop, "_configured_model_tool_surface_raw", lambda *a, **kw: "spec")
|
||||
@@ -173,3 +180,65 @@ def test_spec_direct_answer_is_not_blocked_by_unavailable_tool_policy(monkeypatc
|
||||
{"private_browser"}, "http://fixture", "Ajax", "fixture", {},
|
||||
)
|
||||
assert "private_browser" not in routed.unavailable
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contractless_spec_run_uses_the_compact_path(monkeypatch):
|
||||
# Scheduled, skill and monitor runs call the loop without a turn contract.
|
||||
# Without a contract no Spec routing applies, so the run must proceed like
|
||||
# the compact surface instead of reporting a refusal as its output.
|
||||
monkeypatch.setattr(agent_loop, "_configured_model_tool_surface_raw", lambda *a, **kw: "spec")
|
||||
reached = []
|
||||
|
||||
async def provider(*args, **kwargs):
|
||||
reached.append(True)
|
||||
raise RuntimeError("provider reached")
|
||||
yield # pragma: no cover
|
||||
|
||||
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", provider)
|
||||
events = [chunk async for chunk in agent_loop.stream_agent_loop(
|
||||
endpoint_url="http://fixture", model="Ajax",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
turn_contract=None, disabled_tools=set(), tool_policy=ToolPolicy(),
|
||||
_is_teacher_run=True,
|
||||
)]
|
||||
assert reached
|
||||
assert not any("requires a turn contract" in chunk for chunk in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contractless_spec_run_gains_no_tool_authority(monkeypatch):
|
||||
# A contract-less Spec run must offer exactly what the compact surface
|
||||
# offers under the same policy, and must never consult the router.
|
||||
import src.spec_router as spec_router
|
||||
|
||||
def router_consulted(*args, **kwargs):
|
||||
raise AssertionError("contract-less Spec run consulted the router")
|
||||
|
||||
monkeypatch.setattr(spec_router, "spec_route_tools", router_consulted)
|
||||
policy = ToolPolicy(disabled_tools=frozenset({"manage_calendar"}))
|
||||
|
||||
async def offered_tools(surface):
|
||||
monkeypatch.setattr(agent_loop, "_configured_model_tool_surface_raw", lambda *a, **kw: surface)
|
||||
menus = []
|
||||
|
||||
async def provider(*args, **kwargs):
|
||||
menus.append(sorted((tool.get("function") or {}).get("name") or ""
|
||||
for tool in kwargs.get("tools") or ()))
|
||||
raise RuntimeError("stop after the first provider request")
|
||||
yield # pragma: no cover
|
||||
|
||||
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", provider)
|
||||
with pytest.raises(RuntimeError, match="stop after"):
|
||||
async for _ in agent_loop.stream_agent_loop(
|
||||
endpoint_url="http://fixture", model="Ajax",
|
||||
messages=[{"role": "user", "content": "list my notes and add a calendar event tomorrow at 9"}],
|
||||
turn_contract=None, disabled_tools={"manage_calendar"}, tool_policy=policy,
|
||||
_is_teacher_run=True,
|
||||
):
|
||||
pass
|
||||
return menus
|
||||
|
||||
spec_menus = await offered_tools("spec")
|
||||
assert spec_menus and spec_menus == await offered_tools("compact")
|
||||
assert all("manage_calendar" not in menu for menu in spec_menus)
|
||||
|
||||
Reference in New Issue
Block a user