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:
Alexandre Teixeira
2026-10-05 15:59:59 +01:00
1395 changed files with 360455 additions and 105938 deletions
+93 -7
View File
@@ -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
)