mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 15:32:21 +02:00
Merge commit 'refs/phase3/pre-ajax/publication-tip' into integration/pre-ajax-release
# Conflicts: # routes/chat_routes.py # routes/session_routes.py # src/agent_loop.py # src/agent_tools/filesystem_tools.py # src/teacher_escalation.py # src/tool_capabilities.py # src/tool_execution.py # tests/test_mcp_add_server_args_validation.py # tests/test_token_cache_atomic_swap.py
This commit is contained in:
@@ -2,11 +2,23 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from tests.runtime_evidence_helpers import server_authorized_executor
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def standalone_dispatch_authority(monkeypatch):
|
||||
from src import tool_execution
|
||||
monkeypatch.setattr(tool_execution, "execute_tool_block",
|
||||
server_authorized_executor(tool_execution.execute_tool_block))
|
||||
|
||||
from tests.helpers.document_source import document_source
|
||||
from tests.helpers.js_modules import email_library_paths
|
||||
import src.tool_capabilities as tool_capabilities
|
||||
from src.tool_capabilities import (
|
||||
KNOWN_CAPABILITY_TOOLS,
|
||||
ResultIntegrity,
|
||||
@@ -22,6 +34,11 @@ from src.tool_capabilities import (
|
||||
ToolBlock = namedtuple("ToolBlock", ["tool_type", "content"])
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_approval_gate_for_legacy_gate_tests(monkeypatch):
|
||||
monkeypatch.setattr(tool_capabilities, "TOOL_APPROVAL_GATE_ENABLED", True)
|
||||
|
||||
|
||||
def _collect_agent_events(generator):
|
||||
async def _collect():
|
||||
return [chunk async for chunk in generator]
|
||||
@@ -106,6 +123,37 @@ def test_external_web_result_blocks_later_code_execution():
|
||||
assert "execute_code" in decision.reason
|
||||
|
||||
|
||||
def test_unattended_mode_is_not_an_approval_bypass():
|
||||
context = ToolRunSecurityContext(
|
||||
external_untrusted_context_seen=True,
|
||||
)
|
||||
|
||||
assert context.decision_for("manage_memory", '{"action":"list"}').allowed is False
|
||||
assert context.decision_for("bash", '{"command":"pwd"}').allowed is False
|
||||
|
||||
|
||||
def test_request_scoped_unattended_tools_do_not_authorize_personal_actions():
|
||||
context = ToolRunSecurityContext(
|
||||
external_untrusted_context_seen=True,
|
||||
unattended_tools=frozenset({"host_shell", "read_file", "edit_file"}),
|
||||
)
|
||||
|
||||
assert context.decision_for("host_shell", '{"command":"pwd"}').allowed is True
|
||||
assert context.decision_for("read_file", '{"path":"app.py"}').allowed is True
|
||||
assert context.decision_for("edit_file", '{"path":"app.py"}').allowed is True
|
||||
assert context.decision_for("manage_memory", '{"action":"add"}').allowed is False
|
||||
assert context.decision_for("manage_calendar", '{"action":"create"}').allowed is False
|
||||
|
||||
|
||||
def test_client_runtime_context_cannot_bypass_host_shell_approval():
|
||||
context = ToolRunSecurityContext(
|
||||
external_untrusted_context_seen=True,
|
||||
external_sources=["client runtime context"],
|
||||
)
|
||||
|
||||
assert context.decision_for("host_shell", '{"command":"pwd"}').allowed is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_name",
|
||||
[
|
||||
@@ -337,6 +385,22 @@ def test_external_context_keeps_explicit_low_impact_tools_available(tool_name):
|
||||
assert context.decision_for(tool_name).allowed is True
|
||||
|
||||
|
||||
def test_inspect_media_export_is_classified_as_workspace_write():
|
||||
read = capabilities_for_action(
|
||||
"inspect_media", '{"path":"/workspace/source.mp4","timestamp":"00:00:01"}'
|
||||
)
|
||||
export = capabilities_for_action(
|
||||
"inspect_media",
|
||||
'{"path":"/workspace/source.mp4","timestamp":"00:00:01",'
|
||||
'"output_path":"/workspace/still.png"}',
|
||||
)
|
||||
|
||||
assert read.effects == frozenset({ToolEffect.READ_WORKSPACE})
|
||||
assert export.effects == frozenset(
|
||||
{ToolEffect.READ_WORKSPACE, ToolEffect.WRITE_WORKSPACE}
|
||||
)
|
||||
|
||||
|
||||
def test_external_context_blocks_model_controlled_web_fetch_egress():
|
||||
context = ToolRunSecurityContext(external_untrusted_context_seen=True)
|
||||
|
||||
@@ -645,6 +709,18 @@ def test_multiplexed_non_destructive_actions_do_not_claim_destructive_effect(
|
||||
assert ToolEffect.DESTRUCTIVE not in capabilities.effects
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_name,action", [
|
||||
("manage_endpoints", "list"),
|
||||
("manage_mcp", "list"),
|
||||
("manage_mcp", "list_tools"),
|
||||
("manage_tokens", "list"),
|
||||
("manage_webhooks", "list"),
|
||||
])
|
||||
def test_admin_inventory_reads_are_classified_as_private_reads(tool_name, action):
|
||||
capabilities = capabilities_for_action(tool_name, {"action": action})
|
||||
assert capabilities.effects == frozenset({ToolEffect.READ_PRIVATE})
|
||||
|
||||
|
||||
def test_ambiguous_private_manager_action_fails_high():
|
||||
capabilities = capabilities_for_action("manage_notes", "not json")
|
||||
|
||||
@@ -941,6 +1017,8 @@ def test_initial_external_context_blocks_document_before_editor_side_effect(monk
|
||||
messages,
|
||||
max_rounds=1,
|
||||
relevant_tools={"create_document"},
|
||||
# Exercise the action gate, not the low-signal summary fast path.
|
||||
forced_tools={"create_document"},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -950,7 +1028,7 @@ def test_initial_external_context_blocks_document_before_editor_side_effect(monk
|
||||
event.get("type") == "ask_user"
|
||||
and event.get("data", {}).get("kind") == "tool_approval"
|
||||
for event in events
|
||||
)
|
||||
), events
|
||||
|
||||
|
||||
def test_native_argument_deltas_do_not_mutate_editor_before_gate(monkeypatch):
|
||||
@@ -1012,6 +1090,8 @@ def test_native_argument_deltas_do_not_mutate_editor_before_gate(monkeypatch):
|
||||
messages,
|
||||
max_rounds=1,
|
||||
relevant_tools={"create_document"},
|
||||
# Exercise native action approval, not the direct-chat passthrough.
|
||||
forced_tools={"create_document"},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1021,7 +1101,7 @@ def test_native_argument_deltas_do_not_mutate_editor_before_gate(monkeypatch):
|
||||
and event.get("tool") == "create_document"
|
||||
and event.get("ask_user", {}).get("kind") == "tool_approval"
|
||||
for event in events
|
||||
)
|
||||
), events
|
||||
|
||||
|
||||
def test_tainted_native_route_keeps_action_schema_for_exact_approval(monkeypatch):
|
||||
@@ -1330,7 +1410,7 @@ def test_teacher_takeover_inherits_delegated_and_tainted_run_authority(monkeypat
|
||||
"http://local.test/v1",
|
||||
"qwen-local-model",
|
||||
[
|
||||
{"role": "user", "content": "finish it"},
|
||||
{"role": "user", "content": "Explain how a compass works."},
|
||||
untrusted_context_message("stored context", "untrusted"),
|
||||
],
|
||||
session_id="session-1",
|
||||
@@ -1373,7 +1453,9 @@ def test_frontend_tool_approval_uses_opaque_id_and_fixed_decisions():
|
||||
assert "/test-approval`" in skills
|
||||
assert "approval_id: approval.approval_id" in skills
|
||||
assert "['approve', 'Allow once'" in skills
|
||||
assert index.count("app.js?v=20260815toolapproval4") == 2
|
||||
app_versions = re.findall(r"app\.js\?v=([A-Za-z0-9._-]+)", index)
|
||||
assert len(app_versions) == 2
|
||||
assert app_versions[0] == app_versions[1]
|
||||
assert "app.js?v=20260808startupshell1" not in index
|
||||
approval_module_sources = [
|
||||
(root / path).read_text()
|
||||
@@ -1383,19 +1465,23 @@ def test_frontend_tool_approval_uses_opaque_id_and_fixed_decisions():
|
||||
"static/js/chat.js",
|
||||
"static/js/chatRenderer.js",
|
||||
"static/js/chatStream.js",
|
||||
"static/js/document.js",
|
||||
"static/js/emailInbox.js",
|
||||
"static/js/emailLibrary.js",
|
||||
"static/js/settings.js",
|
||||
"static/js/slashCommands.js",
|
||||
)
|
||||
]
|
||||
# Both the document editor and email library are module sets.
|
||||
approval_module_sources.append(document_source())
|
||||
approval_module_sources.extend(
|
||||
p.read_text()
|
||||
for p in email_library_paths(include_wrapper=True)
|
||||
)
|
||||
assert all(
|
||||
"20260722emailfastindex1" not in source
|
||||
for source in approval_module_sources
|
||||
)
|
||||
assert all(
|
||||
"20260815approvalsave1" in source
|
||||
"20260815approvalsave1" not in source
|
||||
for source in approval_module_sources
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user