merge(runtime): reconcile containment with frozen lab

This commit is contained in:
Alexandre Teixeira
2026-10-01 20:45:45 +01:00
41 changed files with 2219 additions and 51 deletions
+27
View File
@@ -2,6 +2,33 @@
from src.agent_runtime.journal import mark_dispatch, record_action
def server_authorized_executor(executor):
"""Give standalone dispatcher fixtures their explicit server grants.
These existing suites exercise handlers, policy, confinement and approvals.
Their fixture grants cover the declared native tool registry, independently
of the proposed block. Request-authority denial tests use the raw dispatcher.
"""
from functools import wraps
from inspect import signature
from src.agent_runtime.authority import OperationGrant, RequestAuthority
from src.tool_policy import known_tool_names
from src.turn_contract import canonical_tool
call_signature = signature(executor)
@wraps(executor)
async def execute(*args, **kwargs):
bound = call_signature.bind(*args, **kwargs)
parameters = bound.arguments
kwargs.setdefault("request_authority", RequestAuthority(
"standalone-test-request", str(parameters.get("owner") or "").strip().casefold(),
str(parameters.get("session_id") or ""), str(parameters.get("workspace") or ""),
tuple(OperationGrant(name) for name in sorted(
{canonical_tool(n) for n in known_tool_names()} | {"list_dir", "find_files"})),
))
return await executor(*args, **kwargs)
return execute
def authoritative_executor(function):
@record_action
async def execute(block, *args, **kwargs):
+80
View File
@@ -1,5 +1,7 @@
"""Windows execution contract for the agent Bash tool."""
import asyncio
import pytest
from types import SimpleNamespace
@@ -38,6 +40,84 @@ async def test_windows_bash_uses_git_bash_with_structural_cwd(monkeypatch):
assert captured["kwargs"]["cwd"] == workspace
@pytest.mark.asyncio
async def test_windows_bash_captures_output_instead_of_inheriting_server_handles(monkeypatch):
captured = {}
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
monkeypatch.setattr(
subprocess_tools, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe"
)
async def fake_exec(*_argv, **kwargs):
captured.update(kwargs)
return object()
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
await subprocess_tools._create_bash_subprocess("pwd", cwd=r"C:\Work")
assert captured["stdout"] == asyncio.subprocess.PIPE
assert captured["stderr"] == asyncio.subprocess.PIPE
assert captured["stdin"] == asyncio.subprocess.DEVNULL
@pytest.mark.asyncio
async def test_windows_bash_applies_the_subprocess_env(monkeypatch):
captured = {}
env = {"PATH": r"C:\Odysseus\venv\Scripts", "HOME": r"C:\Odysseus\data"}
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
monkeypatch.setattr(
subprocess_tools, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe"
)
async def fake_exec(*_argv, **kwargs):
captured.update(kwargs)
return object()
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
await subprocess_tools._create_bash_subprocess("pwd", cwd=r"C:\Work", env=env)
assert captured["env"] == env
@pytest.mark.asyncio
async def test_windows_bash_tool_passes_ctx_env_through_to_the_child(monkeypatch):
captured = {}
env = {"PATH": r"C:\Odysseus\venv\Scripts", "VIRTUAL_ENV": r"C:\Odysseus\venv"}
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
monkeypatch.setattr(
subprocess_tools, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe"
)
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: r"D:\Workspaces\Project")
async def fake_exec(*argv, **kwargs):
captured["argv"] = argv
captured["kwargs"] = kwargs
return SimpleNamespace(pid=4242)
async def fake_stream(_process, **_kwargs):
return "ok", "", 0, False
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream)
result = await subprocess_tools.BashTool().execute(
"pwd",
{"subproc_env": env, "session_id": "chat-1"},
)
assert result["output"] == "ok"
assert result["exit_code"] == 0
assert "containment" in result
assert captured["kwargs"]["env"] == env
assert captured["kwargs"]["stdout"] == asyncio.subprocess.PIPE
assert captured["kwargs"]["stderr"] == asyncio.subprocess.PIPE
@pytest.mark.asyncio
async def test_windows_bash_without_git_bash_fails_clearly(monkeypatch):
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
@@ -333,6 +333,7 @@ def test_external_tool_images_are_threaded_as_multimodal_evidence():
def test_declared_external_call_reaches_scoped_bridge(monkeypatch):
from src.agent_runtime.authority import OperationGrant, RequestAuthority
bridge_calls = []
round_no = 0
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default)
@@ -367,6 +368,8 @@ def test_declared_external_call_reaches_scoped_bridge(monkeypatch):
[{"role": "user", "content": "Perform the declared operation."}],
max_rounds=2,
owner="pewds",
request_authority=RequestAuthority("declared-fixture", "pewds", "", "",
(OperationGrant("inspect_state"),)),
relevant_tools={"inspect_state"},
forced_tools={"inspect_state"},
fallbacks=[],
@@ -389,6 +392,7 @@ def test_declared_external_call_reaches_scoped_bridge(monkeypatch):
def test_known_native_tool_reaches_scoped_bridge_without_redeclared_schema(monkeypatch):
from src.agent_runtime.authority import create_request_authority
bridge_calls = []
round_no = 0
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default)
@@ -435,6 +439,7 @@ def test_known_native_tool_reaches_scoped_bridge_without_redeclared_schema(monke
[{"role": "user", "content": "Search email for Project Alpha."}],
max_rounds=2,
owner="public-user",
request_authority=create_request_authority("Search email for Project Alpha.", owner="public-user"),
relevant_tools={"search_emails"},
forced_tools={"search_emails"},
fallbacks=[],
+3
View File
@@ -11,6 +11,9 @@ from src.agent_tools import ToolBlock, TOOL_TAGS # noqa: E402 (import first to
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from src.tool_index import ALWAYS_AVAILABLE, BUILTIN_TOOL_DESCRIPTIONS
from src.tool_security import is_public_blocked_tool
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
def _run(content):
+2 -1
View File
@@ -14,6 +14,7 @@ from routes.chat_routes import _agent_turn_cwd
import routes.chat_routes as chat_routes
from src import tool_execution as _te
from src.agent_loop import _is_explicit_local_network_request
from tests.runtime_evidence_helpers import server_authorized_executor
# Hold module-object references (not just from-imported names): other test
# modules re-import src.tool_execution via sys.modules pops, so string-target
@@ -28,7 +29,7 @@ _ROUTED_BRIDGE_TOOLS = _te._ROUTED_BRIDGE_TOOLS
async def _execute_tool_block_for_unit_tests(*args, **kwargs):
"""Use the explicit non-security-context test mode for dispatch tests."""
kwargs.setdefault("security_context", _te.NO_TOOL_SECURITY_CONTEXT)
return await _te.execute_tool_block(*args, **kwargs)
return await server_authorized_executor(_te.execute_tool_block)(*args, **kwargs)
execute_tool_block = _execute_tool_block_for_unit_tests
+8
View File
@@ -4,6 +4,14 @@ import os
import tempfile
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 src import tool_security
from src.tool_security import (
+4
View File
@@ -1,6 +1,7 @@
import asyncio
import logging
import src.tool_execution as tool_execution
from tests.runtime_evidence_helpers import server_authorized_executor
from src.tool_execution import (
AgentExecutionBridge,
@@ -11,6 +12,9 @@ from src.tool_execution import (
)
execute_tool_block = server_authorized_executor(execute_tool_block)
class Block:
tool_type = "host_shell"
+8
View File
@@ -7,6 +7,14 @@ 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
+4 -2
View File
@@ -125,6 +125,8 @@ async def test_generation_dispatch_uses_owner_aware_backend(monkeypatch, exit_co
from types import SimpleNamespace
from src import ai_interaction, tool_execution
from src.agent_runtime.journal import ActionJournal, bind_journal
from tests.runtime_evidence_helpers import server_authorized_executor
execute = server_authorized_executor(tool_execution.execute_tool_block)
calls = []
async def generate(content, **kwargs):
calls.append((content, kwargs))
@@ -140,9 +142,9 @@ async def test_generation_dispatch_uses_owner_aware_backend(monkeypatch, exit_co
block = SimpleNamespace(tool_type='generate_image', content='{"prompt":"A city"}')
journal = ActionJournal()
with bind_journal(journal):
_, denied = await tool_execution.execute_tool_block(block, owner='pewds', session_id='fixture',
_, denied = await execute(block, owner='pewds', session_id='fixture',
disabled_tools={'generate_image'}, security_context=tool_execution.NO_TOOL_SECURITY_CONTEXT)
_, result = await tool_execution.execute_tool_block(block, owner='pewds', session_id='fixture',
_, result = await execute(block, owner='pewds', session_id='fixture',
security_context=tool_execution.NO_TOOL_SECURITY_CONTEXT)
assert denied['exit_code'] != 0
assert journal.actions[0].execution_id is None
+584
View File
@@ -0,0 +1,584 @@
"""Request grants are independent of tool offerings and model proposals."""
import asyncio
from contextlib import nullcontext
from dataclasses import replace
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.agent_runtime.authority import (
MISSING_AUTHORITY, ExactOperation, OperationGrant, RequestAuthority,
active_request_authority, bind_request_authority, create_request_authority,
restore_background_authority, restore_task_authority, save_background_authority,
seal_task_authority, task_operation, with_request_authority,
is_internal_tool_request, require_user_approval_request,
request_authority_for_http,
)
from src.tool_policy import ToolPolicy
from src.tool_types import ToolBlock
def authority(*tools, owner="alice", session_id="s", workspace=""):
return RequestAuthority("request-test", owner, session_id, workspace,
tuple(OperationGrant(tool) for tool in tools))
@pytest.mark.asyncio
@pytest.mark.parametrize("offering", ["schema", "bridge", "dynamic"])
async def test_availability_and_model_selection_do_not_grant_execution(monkeypatch, offering):
from src import tool_execution as execution
from src.turn_contract import bind_turn_contract, resolve_turn_contract
implementation = AsyncMock(return_value=("bash", {"exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
offered_handler = AsyncMock()
if offering == "dynamic":
import src.agent_tools
monkeypatch.setitem(src.agent_tools.TOOL_HANDLERS, "bash", offered_handler)
bridge_context = execution.bind_execution_bridge(execution.AgentExecutionBridge(
route_tool=offered_handler, supported_tools=frozenset({"bash"}))) if offering == "bridge" else nullcontext()
contract = resolve_turn_contract(capabilities={"shell_files"}, policy=ToolPolicy(),
schemas=[{"function": {"name": "bash"}}])
with bind_turn_contract(contract), bridge_context:
description, result = await execution.execute_tool_block(
ToolBlock("bash", "echo 'authorized by model'"), owner="alice", session_id="s",
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("transcribe_media"))
assert "BLOCKED" in description
assert result["failure_kind"] == "request_authority_denied"
implementation.assert_not_awaited()
offered_handler.assert_not_awaited()
@pytest.mark.parametrize("user_text,allowed,denied", [
("Transcribe /workspace/input/audio.wav", "transcribe_media", "bash"),
("OCR extract exact text from /workspace/input/image.png", "extract_text", "python"),
("List my tasks", "manage_tasks", "web_fetch"),
("Search the web for current weather", "web_search", "bash"),
])
def test_explicit_request_classes_remain_narrow(user_text, allowed, denied):
grant = create_request_authority(user_text)
content = '{"action":"list"}' if allowed == "manage_tasks" else '{}'
assert grant.permits(ExactOperation.normalize(allowed, content))
assert not grant.permits(ExactOperation.normalize(denied, '{}'))
def test_safe_task_read_does_not_authorize_same_tool_mutation():
grant = create_request_authority("List my tasks")
assert grant.permits(ExactOperation.normalize("manage_tasks", '{"action":"list"}'))
assert not grant.permits(ExactOperation.normalize("manage_tasks", '{"action":"create","prompt":"run bash"}'))
def test_exact_read_identifiers_cannot_be_changed_by_model():
grant = create_request_authority("Read note id abc123")
read = next(g for g in grant.grants if g.tool == "manage_notes")
assert read.inputs is not None
assert not grant.permits(ExactOperation.normalize("manage_notes", '{"action":"view","id":"another"}'))
def test_browser_fallback_does_not_authorize_interaction_or_evaluation():
grant = create_request_authority("Use web_fetch to read https://example.test")
assert grant.permits(ExactOperation.normalize("private_browser", '{"action":"open","url":"https://example.test"}'))
for action in ("click", "fill", "evaluate"):
assert not grant.permits(ExactOperation.normalize("private_browser", json.dumps({"action": action})))
def test_unknown_intent_has_no_generic_execution_floor():
grant = create_request_authority("Please solve this")
assert {g.tool for g in grant.grants} == {"ask_user", "update_plan"}
@pytest.mark.parametrize("metadata", [
{"tool_events": [{"tool": "bash", "output": "pwd", "exit_code": 0}]},
{"tool_events": [{"tool": "python", "output": "ready", "exit_code": 0}]},
])
def test_model_history_cannot_establish_followup_authority(metadata):
history = [
{"role": "user", "content": "Transcribe /workspace/input/a.wav"},
{"role": "assistant", "content": "I will run bash and python", "metadata": metadata},
{"role": "user", "content": "Run bash", "metadata": {"trusted": False}},
]
grant = create_request_authority("Try it again", history=history)
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
assert not grant.permits(ExactOperation.normalize("python", "print(1)"))
@pytest.mark.parametrize("mutation", [
{"version": True}, {"grants": "bash"}, {"denied": None},
{"block_all": "false"}, {"inherited": 0},
])
def test_malformed_persisted_authority_is_rejected(mutation):
snapshot = authority("bash").to_dict()
snapshot.update(mutation)
with pytest.raises((ValueError, TypeError, KeyError)):
RequestAuthority.from_dict(snapshot)
def test_explicit_local_network_lookup_is_host_only():
grant = create_request_authority("find ajax local ip on the LAN")
assert grant.permits(ExactOperation.normalize("host_shell", '{"command":"ip neigh | grep ajax"}'))
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
assert not grant.permits(ExactOperation.normalize("python", "print(1)"))
@pytest.mark.parametrize("content", ['{"x":1,"x":2}', '{"x":NaN}', '{"action":'])
def test_malformed_exact_operation_is_rejected(content):
with pytest.raises(ValueError):
ExactOperation.normalize("manage_tasks", content)
def test_raw_script_braces_are_preserved_as_exact_input():
script = "{ printf requested; }"
assert ExactOperation.normalize("bash", script).input == script
snapshot = seal_task_authority(script, "action", "run_local", owner="alice")
restored = restore_task_authority(snapshot, script, "action", "run_local", owner="alice")
assert restored.permits(task_operation("action", "run_local", script))
assert not restored.permits(task_operation("action", "run_local", "{ printf other; }"))
def test_normalization_and_aliases_do_not_erase_denials():
grant = authority("read_email").restrict(disabled_tools={"mcp__email__read_email"})
assert not grant.permits(ExactOperation.normalize("read_email", '{}'))
grant = authority("read_email").restrict(ToolPolicy(disable_mcp=True))
assert not grant.permits(ExactOperation.normalize("mcp__email__read_email", '{}'))
assert ExactOperation.normalize("manage_tasks", '{ "action": "list" }').input == '{"action":"list"}'
@pytest.mark.asyncio
@pytest.mark.parametrize("state", [MISSING_AUTHORITY, None, {}, "authorized"])
async def test_missing_and_malformed_dispatch_authority_fail_closed(monkeypatch, state):
from src import tool_execution as execution
implementation = AsyncMock()
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
_, result = await execution.execute_tool_block(ToolBlock("bash", "pwd"),
security_context=execution.NO_TOOL_SECURITY_CONTEXT, request_authority=state)
assert result["blocked"] is True
implementation.assert_not_awaited()
@pytest.mark.asyncio
async def test_dispatch_checks_grants_and_current_disabled_policy(monkeypatch):
from src import tool_execution as execution
implementation = AsyncMock(return_value=("bash", {"exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
for disabled in (set(), {"bash"}):
_, result = await execution.execute_tool_block(ToolBlock("bash", "pwd"),
owner="alice", session_id="s", disabled_tools=disabled,
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("bash"))
assert result["exit_code"] == (1 if disabled else 0)
assert implementation.await_count == 1
@pytest.mark.asyncio
async def test_nested_stream_intersects_and_restores_parent_on_close():
seen = []
@with_request_authority
async def child(messages, request_authority=MISSING_AUTHORITY, owner="alice", session_id="s"):
seen.append(active_request_authority())
yield "child"
parent = authority("transcribe_media")
with bind_request_authority(parent):
stream = child([{"role": "user", "content": "Run bash"}],
request_authority=authority("bash", "transcribe_media"))
assert await anext(stream) == "child"
assert not seen[0].permits(ExactOperation.normalize("bash", "pwd"))
assert seen[0].permits(ExactOperation.normalize("transcribe_media", '{}'))
await stream.aclose()
assert active_request_authority() is parent
assert active_request_authority() is None
@pytest.mark.asyncio
async def test_retries_and_provider_changes_do_not_recreate_authority():
seen = []
@with_request_authority
async def run(messages, request_authority=MISSING_AUTHORITY, owner="alice", session_id="s"):
for proposed in ("transcribe_media", "bash", "python"):
seen.append(active_request_authority())
yield active_request_authority().permits(ExactOperation.normalize(proposed, '{}'))
assert [x async for x in run([{"role": "user", "content": "Transcribe /workspace/input/a.wav"}])] == [True, False, False]
assert all(value is seen[0] for value in seen)
def test_task_snapshot_caps_model_payload_and_rejects_changed_or_missing_state():
parent = authority("manage_tasks")
with bind_request_authority(parent):
snapshot = seal_task_authority("Run bash in the workspace", "llm", None, owner="alice")
restored = restore_task_authority(snapshot, "Run bash in the workspace", "llm", None, owner="alice")
assert not restored.permits(ExactOperation.normalize("bash", "pwd"))
for state in (None, "{}", snapshot):
changed = restore_task_authority(state, "a different prompt", "llm", None, owner="alice")
assert changed.grants == ()
def test_direct_task_ingress_seals_only_exact_builtin_action():
snapshot = seal_task_authority("printf requested", "action", "run_local", owner="alice")
restored = restore_task_authority(snapshot, "printf requested", "action", "run_local", owner="alice")
assert restored.permits(task_operation("action", "run_local", "printf requested"))
assert not restored.permits(ExactOperation.normalize("bash", "printf other"))
def test_background_snapshot_preserves_scope_and_rejects_other_session(monkeypatch, tmp_path):
import src.constants
monkeypatch.setattr(src.constants, "BG_JOBS_DIR", str(tmp_path))
grant = authority("transcribe_media").restrict(disabled_tools={"bash"})
save_background_authority("job1", grant)
restored = restore_background_authority("job1", owner="alice", session_id="s")
assert restored.request_id == grant.request_id
assert restored.denied == frozenset({"bash"})
assert not restored.permits(ExactOperation.normalize("python", "print(1)"))
assert restore_background_authority("job1", owner="alice", session_id="other").grants == ()
@pytest.mark.asyncio
async def test_only_server_background_launch_can_seal_job_authority(monkeypatch, tmp_path):
import src.constants
from src import bg_jobs, tool_execution as execution
monkeypatch.setattr(src.constants, "BG_JOBS_DIR", str(tmp_path))
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
monkeypatch.setattr(bg_jobs, "launch", lambda *a, **k: {"id": "server-job"})
await execution.execute_tool_block(ToolBlock("bash", "#!bg\nprintf trusted"),
owner="alice", session_id="s", security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("bash"))
restored = restore_background_authority("server-job", owner="alice", session_id="s")
assert restored.request_id == "request-test"
assert restored.permits(ExactOperation.normalize("bash", "printf trusted"))
handler = AsyncMock(return_value=("transcribe_media", {"bg_job_id": "forged-job", "exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
await execution.execute_tool_block(ToolBlock("transcribe_media", '{}'),
owner="alice", session_id="s", security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("transcribe_media"))
assert not (tmp_path / "forged-job.authority.json").exists()
@pytest.mark.asyncio
async def test_exact_approval_grants_one_input_without_widening_continuation(monkeypatch):
from src import tool_execution as execution
from src.tool_approvals import ToolApprovalStore
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
store = ToolApprovalStore()
original = authority("transcribe_media")
pending = store.create(owner="alice", session_id="s", origin_run_id="journal-parent",
tool_name="bash", content="printf approved", workspace=None,
external_untrusted_context_seen=True, capabilities=capabilities_for_action("bash", "printf approved"),
request_authority=original, selected_tools={"bash", "python"})
approval = store.consume(pending.approval_id, owner="alice", session_id="s", decision="approve_task")
implementation = AsyncMock(return_value=("bash", {"exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
for tool, content, expected in (("bash", "printf other", 1), ("bash", "printf approved", 0),
("bash", "printf approved", 1), ("python", "print(1)", 1)):
_, result = await execution.execute_tool_block(ToolBlock(tool, content), owner="alice", session_id="s",
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True),
request_authority=original, exact_approval=approval)
assert result["exit_code"] == expected
assert implementation.await_count == 1
assert "request_authority" not in pending.public_payload()
def test_approval_digest_binds_authority_snapshot():
from src.tool_approvals import ExactToolApproval, ToolApprovalStore
from src.tool_capabilities import capabilities_for_action
pending = ToolApprovalStore().create(owner="alice", session_id="s", origin_run_id="journal",
tool_name="bash", content="pwd", workspace=None, external_untrusted_context_seen=True,
capabilities=capabilities_for_action("bash", "pwd"), request_authority=authority("transcribe_media"))
forged = ExactToolApproval(replace(pending, request_authority=authority("bash", "python")))
assert not forged.matches(owner="alice", session_id="s", workspace=None, tool_name="bash", content="pwd")
@pytest.mark.asyncio
async def test_nested_approval_cannot_cross_parent_class_ceiling(monkeypatch):
from src import tool_execution as execution
from src.tool_approvals import ToolApprovalStore
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
store = ToolApprovalStore()
pending = store.create(owner="alice", session_id="s", origin_run_id="parent",
tool_name="bash", content="pwd", workspace=None, external_untrusted_context_seen=True,
capabilities=capabilities_for_action("bash", "pwd"))
approval = store.consume(pending.approval_id, owner="alice", session_id="s", decision="approve")
implementation = AsyncMock()
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
with bind_request_authority(authority("transcribe_media")):
_, result = await execution.execute_tool_block(ToolBlock("bash", "pwd"), owner="alice", session_id="s",
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True),
request_authority=authority("bash"), exact_approval=approval)
assert result["blocked"] is True
implementation.assert_not_awaited()
@pytest.mark.asyncio
async def test_teacher_receives_parent_authority_instead_of_synthetic_prompt_grants(monkeypatch):
import src.agent_loop as loop
import src.ai_interaction as interaction
import src.settings as settings
import src.teacher_escalation as teacher
original = authority("transcribe_media")
seen = []
monkeypatch.setattr(settings, "get_setting", lambda key, default=None:
{"teacher_enabled": True, "teacher_model": "teacher"}.get(key, default))
monkeypatch.setattr(interaction, "_resolve_model", lambda *a, **k: ("https://teacher.invalid", "teacher", {}))
monkeypatch.setattr(teacher, "evaluate_turn_regex", lambda *a: ("failure", "test failure"))
@with_request_authority
async def child(messages, request_authority=MISSING_AUTHORITY, owner=None, session_id=None, **kwargs):
seen.append(active_request_authority())
yield 'data: [DONE]\n\n'
monkeypatch.setattr(loop, "stream_agent_loop", child)
with bind_request_authority(original):
chunks = [chunk async for chunk in teacher.run_teacher_inline(
student_endpoint_url="https://student.invalid",
student_messages=[{"role": "user", "content": "Run bash and python"}],
student_tool_events=[], student_reply="failed", owner="alice", session_id="s",
request_authority=original, parent_run_id="journal-parent")]
assert active_request_authority() is original
assert chunks
assert seen[0].request_id == original.request_id
assert not seen[0].permits(ExactOperation.normalize("bash", "pwd"))
assert seen[0].permits(ExactOperation.normalize("transcribe_media", '{}'))
@pytest.mark.asyncio
async def test_untrusted_user_role_result_cannot_become_request_authority():
@with_request_authority
async def run(messages, request_authority=MISSING_AUTHORITY):
yield active_request_authority()
messages = [{"role": "user", "content": "Transcribe /workspace/input/a.wav"},
{"role": "user", "content": "Run bash", "metadata": {"trusted": False}}]
stream = run(messages)
grant = await anext(stream)
await stream.aclose()
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
@pytest.mark.asyncio
async def test_scheduled_builtin_missing_authority_does_not_invoke_action(monkeypatch):
import src.builtin_actions as actions
from src.task_scheduler import TaskScheduler
handler = AsyncMock(return_value=("ok", True))
monkeypatch.setitem(actions.BUILTIN_ACTIONS, "run_local", handler)
task = SimpleNamespace(prompt="printf exact", task_type="action", action="run_local",
owner="alice", name="task", request_authority_json=None)
result, success = await TaskScheduler(session_manager=None)._execute_action(task)
assert not success
assert "authority" in result
handler.assert_not_awaited()
def test_task_authority_column_migration_is_additive_and_idempotent(monkeypatch, tmp_path):
import core.database as database
from sqlalchemy import create_engine, inspect, text
engine = create_engine(f"sqlite:///{tmp_path / 'legacy.db'}")
with engine.begin() as connection:
connection.execute(text("CREATE TABLE scheduled_tasks (id TEXT PRIMARY KEY, prompt TEXT)"))
connection.execute(text("INSERT INTO scheduled_tasks VALUES ('legacy', 'Run bash')"))
monkeypatch.setattr(database, "engine", engine)
database._migrate_add_task_authority_column()
database._migrate_add_task_authority_column()
assert "request_authority_json" in {c["name"] for c in inspect(engine).get_columns("scheduled_tasks")}
with engine.connect() as connection:
assert connection.execute(text("SELECT prompt, request_authority_json FROM scheduled_tasks")).one() == ("Run bash", None)
engine.dispose()
@pytest.mark.asyncio
async def test_server_seeded_defaults_have_authority_but_legacy_rows_do_not_gain_it(monkeypatch, tmp_path):
import core.database as database
import routes.prefs_routes as preferences
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.task_scheduler import TaskScheduler
engine = create_engine(f"sqlite:///{tmp_path / 'tasks.db'}")
database.Base.metadata.create_all(engine)
sessions = sessionmaker(bind=engine)
monkeypatch.setattr(database, "SessionLocal", sessions)
monkeypatch.setattr(preferences, "_load_for_user", lambda owner: {})
scheduler = TaskScheduler(session_manager=None)
monkeypatch.setattr(scheduler, "ensure_assistant_defaults", AsyncMock())
with sessions() as db:
db.add(database.ScheduledTask(id="legacy", owner="alice", name="Legacy housekeeping",
task_type="action", action="tidy_sessions"))
db.commit()
await scheduler.ensure_defaults("alice")
with sessions() as db:
tasks = db.query(database.ScheduledTask).all()
assert len(tasks) > 1
for task in tasks:
grant = restore_task_authority(task.request_authority_json, task.prompt,
task.task_type, task.action, owner=task.owner)
assert grant.permits(task_operation(task.task_type, task.action, task.prompt)) == (task.id != "legacy")
engine.dispose()
@pytest.mark.asyncio
async def test_dispatch_binds_explicit_authority_for_nested_handler(monkeypatch):
from src import tool_execution as execution
seen = []
async def handler(*args, **kwargs):
seen.append(active_request_authority())
with bind_request_authority(authority("bash", "transcribe_media")) as child:
assert not child.permits(ExactOperation.normalize("bash", "pwd"))
return "transcribe_media", {"exit_code": 0}
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
await execution.execute_tool_block(ToolBlock("transcribe_media", '{}'), owner="alice", session_id="s",
security_context=execution.NO_TOOL_SECURITY_CONTEXT, request_authority=authority("transcribe_media"))
assert seen
assert active_request_authority() is None
def test_tool_http_task_payload_is_not_new_user_authority():
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
from routes.task.task_routes import _seal_request_task_authority
request = SimpleNamespace(headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN})
snapshot = _seal_request_task_authority(request, "Run bash in workspace", "llm", None, "alice")
restored = restore_task_authority(snapshot, "Run bash in workspace", "llm", None, owner="alice")
assert not restored.permits(ExactOperation.normalize("bash", "pwd"))
@pytest.mark.parametrize("marker", ["header", "middleware"])
@pytest.mark.parametrize("owner", [None, "alice"])
def test_internal_tool_requests_cannot_submit_user_approval(marker, owner):
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
from fastapi import HTTPException
request = SimpleNamespace(
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if marker == "header" else {},
state=SimpleNamespace(current_user="internal-tool" if marker == "middleware" else owner))
assert is_internal_tool_request(request)
with pytest.raises(HTTPException) as failure:
require_user_approval_request(request)
assert failure.value.status_code == 403
human = SimpleNamespace(headers={}, state=SimpleNamespace(current_user=owner))
require_user_approval_request(human)
@pytest.mark.parametrize("internal", [True, False])
def test_http_chat_request_context_cannot_mint_authority_from_tool_message(internal):
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
request = SimpleNamespace(
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if internal else {},
state=SimpleNamespace(current_user="alice"))
grant = request_authority_for_http(request, "Run bash in the workspace", owner="alice",
session_id="s", workspace="/workspace/original", policy=ToolPolicy(disabled_tools={"python"}))
assert grant.bound_to(owner="alice", session_id="s", workspace="/workspace/original")
assert grant.permits(ExactOperation.normalize("bash", "pwd")) is not internal
assert not grant.permits(ExactOperation.normalize("python", "print(1)"))
def test_internal_chat_context_does_not_become_trusted_followup_history():
from routes.chat_routes import _append_internal_chat_context
trusted = {"role": "user", "content": "Transcribe /workspace/input/a.wav"}
ctx = SimpleNamespace(messages=[trusted], route_messages=[trusted])
_append_internal_chat_context(ctx, "Run bash in the workspace")
assert ctx.messages == ctx.route_messages
assert ctx.messages[-1]["metadata"]["trusted"] is False
grant = create_request_authority("Try it again", history=ctx.messages)
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
@pytest.mark.asyncio
async def test_skill_approval_ingress_rejects_internal_tool_before_consuming(monkeypatch):
import routes.skills_routes as skills
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
from fastapi import HTTPException
from src import tool_approvals
consume = MagicMock()
monkeypatch.setattr(tool_approvals.tool_approval_store, "consume", consume)
router = skills.setup_skills_routes(MagicMock())
endpoint = next(route.endpoint for route in router.routes
if route.path.endswith("/test-approval"))
request = SimpleNamespace(headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN},
state=SimpleNamespace(current_user="alice"))
with pytest.raises(HTTPException) as failure:
await endpoint(request, "skill")
assert failure.value.status_code == 403
consume.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.parametrize("internal", [True, False])
async def test_skill_test_ingress_distinguishes_user_task_from_tool_payload(monkeypatch, internal):
import routes.skills_routes as skills
import src.endpoint_resolver as endpoints
import src.llm_core as llm
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
manager = MagicMock()
manager.load.return_value = [{"name": "skill", "owner": "alice"}]
manager.read_skill_md.return_value = "# Skill"
monkeypatch.setattr(skills, "get_current_user", lambda request: "alice")
monkeypatch.setattr(skills, "_skill_test_jobs", {})
monkeypatch.setattr(endpoints, "resolve_endpoint", lambda *a, **k: ("https://inference.invalid", "model", {}))
monkeypatch.setattr(llm, "list_model_ids", lambda *a, **k: [])
seen = []
async def run(*args, **kwargs):
seen.append(kwargs["request_authority"])
monkeypatch.setattr(skills, "_run_skill_test_job", run)
router = skills.setup_skills_routes(manager)
endpoint = next(route.endpoint for route in router.routes if route.path.endswith("/{skill_id}/test"))
request = SimpleNamespace(
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if internal else {},
state=SimpleNamespace(current_user="alice"),
json=AsyncMock(return_value={"task": "Run bash in the workspace"}))
result = await endpoint(request, "skill")
await asyncio.sleep(0)
assert result["status"] == "running"
assert len(seen) == 1
assert seen[0].permits(ExactOperation.normalize("bash", "pwd")) is not internal
@pytest.mark.asyncio
async def test_scheduled_override_cannot_replace_stored_authority(monkeypatch):
from src import agent_loop
from src.task_scheduler import TaskScheduler
seen = []
async def fake_stream(*args, **kwargs):
seen.append(kwargs)
yield 'data: {"delta":"done"}\n\n'
yield 'data: [DONE]\n\n'
monkeypatch.setattr(agent_loop, "stream_agent_loop", fake_stream)
task = SimpleNamespace(prompt="Transcribe /workspace/input/a.wav", task_type="llm", action=None,
owner="alice", name="test", max_steps=1)
with bind_request_authority(authority("transcribe_media", workspace="/workspace/original")):
task.request_authority_json = seal_task_authority(task.prompt, "llm", None, owner="alice")
await TaskScheduler(session_manager=None)._run_agent_loop("https://inference.invalid", "test", task, "s",
override_user_message="Run bash and python")
grant = seen[0]["request_authority"]
assert grant.permits(ExactOperation.normalize("transcribe_media", '{}'))
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
assert grant.workspace == "/workspace/original"
assert seen[0]["workspace"] == grant.workspace
@pytest.mark.asyncio
async def test_loop_retains_denial_before_offered_inventory_reconciliation(monkeypatch):
from src import agent_loop as loop, tool_execution as execution
from src.turn_contract import resolve_turn_contract
implementation = AsyncMock(return_value=("bash", {"exit_code": 0, "output": "unexpected"}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
monkeypatch.setattr(loop, "get_setting", lambda key, default=None: default)
monkeypatch.setattr(loop, "get_mcp_manager", lambda: None)
monkeypatch.setattr(loop, "estimate_tokens", lambda *a, **k: 10)
async def fake_stream(*args, **kwargs):
yield 'data: ' + json.dumps({"delta": '```bash\npwd\n```'}) + '\n\n'
monkeypatch.setattr(loop, "stream_llm_with_fallback", fake_stream)
contract = resolve_turn_contract(capabilities={"shell_files"}, selected_tools={"bash"},
schemas=[{"function": {"name": "bash"}}], policy=ToolPolicy())
chunks = [chunk async for chunk in loop.stream_agent_loop(
"https://inference.invalid", "test", [{"role": "user", "content": "Run bash pwd"}],
owner="alice", session_id="s", max_rounds=1, turn_contract=contract, disabled_tools={"bash"})]
implementation.assert_not_awaited()
assert chunks[-1] == 'data: [DONE]\n\n'
assert active_request_authority() is None
@pytest.mark.asyncio
async def test_authority_denial_cannot_create_completion_receipt(monkeypatch):
from src import tool_execution as execution
from src.agent_runtime.journal import ActionJournal, bind_journal
journal = ActionJournal()
with bind_journal(journal):
await execution.execute_tool_block(ToolBlock("bash", "pwd"), owner="alice", session_id="s",
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("transcribe_media"))
assert len(journal.actions) == 1
assert journal.actions[0].execution_id is None
assert journal.actions[0].outcome["authoritative"] is False
assert journal.actions[0].outcome["blocked"] is True
+2 -1
View File
@@ -14,9 +14,10 @@ from src.preset_manager import PresetManager
async def _execute_without_run_context(execute_tool_block, *args, **kwargs):
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT
from tests.runtime_evidence_helpers import server_authorized_executor
kwargs.setdefault("security_context", NO_TOOL_SECURITY_CONTEXT)
return await execute_tool_block(*args, **kwargs)
return await server_authorized_executor(execute_tool_block)(*args, **kwargs)
class _FakeColumn:
+8
View File
@@ -6,6 +6,14 @@ import json
import os
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 src.agent_evidence import CompletionRequirements, EvidenceLedger, EvidenceKind
from src.agent_runtime.completion import completion_answer, with_completion_gate
+295
View File
@@ -1,16 +1,21 @@
"""Tests for shell_routes.py helpers."""
import asyncio
import builtins
import errno
import importlib
import importlib.util
import json
import os
import shlex
import signal
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
from core.platform_compat import pid_alive
from routes.shell_routes import (
_find_line_break,
_host_docker_access_enabled,
@@ -60,6 +65,29 @@ def test_shell_routes_import_without_posix_pty_modules(monkeypatch):
assert module._find_line_break(b"ok\n") == (2, 1)
def test_shell_routes_import_without_sigkill(monkeypatch):
"""Native Windows has no signal.SIGKILL; app.py imports this module anyway.
The teardown escalation is resolved at import time, so naming SIGKILL
unconditionally would stop the whole app from starting on Windows rather
than only degrading PTY teardown there.
"""
monkeypatch.delattr(signal, "SIGKILL", raising=False)
module_path = Path(__file__).resolve().parents[1] / "routes" / "shell_routes.py"
spec = importlib.util.spec_from_file_location(
"_shell_routes_without_sigkill", module_path
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
finally:
sys.modules.pop(spec.name, None)
assert module.PTY_KILL_ESCALATION == (signal.SIGTERM,)
async def test_generate_pty_reports_explicit_unsupported_error(monkeypatch):
"""Clients can distinguish unsupported PTY mode from process failures."""
import routes.shell_routes as shell_routes
@@ -85,6 +113,273 @@ async def test_generate_pty_reports_explicit_unsupported_error(monkeypatch):
]
pty_session = pytest.mark.skipif(
not hasattr(os, "setsid"), reason="process sessions are POSIX-only"
)
async def _spawn_pty_style_session(script: str):
"""Spawn `script` the way _generate_pty does: its own session via setsid."""
return await asyncio.create_subprocess_shell(
script,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
preexec_fn=os.setsid,
)
def _stubborn_child(pid_file: Path, ignore: tuple[str, ...]) -> str:
"""Shell snippet that starts a child ignoring `ignore`, then waits for it.
Killing a PTY session leader makes the kernel send SIGHUP to the
terminal's foreground process group, so a plain `sleep` child looks
contained even when nothing ever signalled the group. A child that ignores
SIGHUP is what an admin actually runs into — a `nohup`ed job, a daemon,
anything meant to outlive its terminal.
The child publishes its own pid only after installing the handlers, and
the snippet blocks until it does, so a test can never signal it while it
is still starting up and read that as teardown having worked.
"""
ignores = "".join(
f"signal.signal(signal.{name}, signal.SIG_IGN); " for name in ignore
)
script = (
f"import os, signal, time; {ignores}"
f"open({str(pid_file)!r}, 'w').write(str(os.getpid())); "
"time.sleep(120)"
)
return (
f"{sys.executable} -c {shlex.quote(script)} & "
f"while [ ! -s {pid_file} ]; do sleep 0.02; done"
)
async def _never_disconnected() -> bool:
return False
def _reap_if_alive(pid: int) -> None:
"""Clean up a descendant the code under test was supposed to have killed."""
if pid_alive(pid):
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
async def _read_pid(path: Path, timeout: float = 5.0) -> int:
"""Wait for a child to publish its pid, then return it."""
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
if path.exists():
text = path.read_text().strip()
if text:
return int(text)
await asyncio.sleep(0.01)
raise AssertionError(f"child never wrote its pid to {path}")
@pty_session
async def test_terminate_pty_session_kills_descendants(tmp_path):
"""Tearing down a PTY command takes its children, not only the shell."""
import routes.shell_routes as shell_routes
pid_file = tmp_path / "child.pid"
proc = await _spawn_pty_style_session(
f"sleep 120 & echo $! > {pid_file}; sleep 120"
)
try:
child_pid = await _read_pid(pid_file)
assert pid_alive(child_pid)
assert await shell_routes._terminate_pty_session(proc) is True
assert proc.returncode is not None
assert not pid_alive(child_pid)
finally:
await shell_routes._terminate_pty_session(proc)
@pty_session
async def test_terminate_pty_session_escalates_past_ignored_sigterm(tmp_path):
"""A child that ignores SIGTERM is still gone when teardown returns."""
import routes.shell_routes as shell_routes
pid_file = tmp_path / "child.pid"
child = _stubborn_child(pid_file, ("SIGHUP", "SIGTERM"))
proc = await _spawn_pty_style_session(f"{child}; sleep 120")
try:
child_pid = await _read_pid(pid_file)
assert pid_alive(child_pid)
assert await shell_routes._terminate_pty_session(proc) is True
assert not pid_alive(child_pid)
finally:
await shell_routes._terminate_pty_session(proc)
@pty_session
async def test_generate_pty_timeout_kills_the_whole_session(tmp_path):
"""A timed-out PTY command leaves none of its children running."""
import routes.shell_routes as shell_routes
pid_file = tmp_path / "child.pid"
child = _stubborn_child(pid_file, ("SIGHUP",))
cmd = f"{child}; echo ready; sleep 120"
request = SimpleNamespace(is_disconnected=_never_disconnected)
events = [
json.loads(chunk.removeprefix("data: ").strip())
async for chunk in shell_routes._generate_pty(cmd, 1, request)
]
child_pid = await _read_pid(pid_file)
try:
assert events[-1] == {"exit_code": -1}
assert events[-2]["data"].startswith("Command timed out after 1s")
assert not pid_alive(child_pid)
finally:
_reap_if_alive(child_pid)
@pty_session
async def test_generate_pty_disconnect_kills_the_whole_session(tmp_path):
"""Abandoning the stream kills the command's children too."""
import routes.shell_routes as shell_routes
pid_file = tmp_path / "child.pid"
child = _stubborn_child(pid_file, ("SIGHUP",))
cmd = f"{child}; echo ready; sleep 120"
polls = []
async def disconnect_after_first_poll() -> bool:
polls.append(None)
return len(polls) > 1
request = SimpleNamespace(is_disconnected=disconnect_after_first_poll)
async for _ in shell_routes._generate_pty(cmd, 0, request):
pass
child_pid = await _read_pid(pid_file)
try:
assert not pid_alive(child_pid)
finally:
_reap_if_alive(child_pid)
async def test_terminate_pty_session_reports_a_session_it_could_not_kill(
monkeypatch,
):
"""Teardown returns False rather than claiming a surviving session died."""
import routes.shell_routes as shell_routes
monkeypatch.setattr(shell_routes, "PTY_KILL_GRACE", 0.01)
monkeypatch.setattr(shell_routes, "_signal_session", lambda *_: True)
monkeypatch.setattr(shell_routes, "_session_alive", lambda *_: True)
monkeypatch.setattr(shell_routes, "_session_pgid", lambda _: 4242)
proc = SimpleNamespace(pid=4242, returncode=0, wait=None)
assert await shell_routes._terminate_pty_session(proc) is False
async def test_terminate_pty_session_escalates_before_giving_up(monkeypatch):
"""SIGTERM then SIGKILL — the group is never signalled only once."""
import routes.shell_routes as shell_routes
sent = []
monkeypatch.setattr(shell_routes, "PTY_KILL_GRACE", 0.01)
monkeypatch.setattr(shell_routes, "_session_pgid", lambda _: 4242)
monkeypatch.setattr(shell_routes, "_session_alive", lambda *_: True)
monkeypatch.setattr(
shell_routes,
"_signal_session",
lambda pgid, pid, sig: sent.append(sig) or True,
)
proc = SimpleNamespace(pid=4242, returncode=0, wait=None)
await shell_routes._terminate_pty_session(proc)
assert sent == [signal.SIGTERM, signal.SIGKILL]
@pty_session
async def test_generate_pty_timeout_says_so_when_the_session_survives(
monkeypatch,
):
"""A timed-out command no longer reports clean termination it didn't get."""
import routes.shell_routes as shell_routes
real_terminate = shell_routes._terminate_pty_session
async def terminate_but_report_failure(proc):
await real_terminate(proc)
return False
monkeypatch.setattr(
shell_routes, "_terminate_pty_session", terminate_but_report_failure
)
request = SimpleNamespace(is_disconnected=_never_disconnected)
events = [
json.loads(chunk.removeprefix("data: ").strip())
async for chunk in shell_routes._generate_pty("echo ready; sleep 30", 1, request)
]
assert events[-1] == {"exit_code": -1}
timed_out = events[-2]
assert timed_out["stream"] == "stderr"
assert timed_out["data"] == (
"Command timed out after 1s" + shell_routes.PTY_KILL_FAILED_HINT
)
def test_session_alive_treats_a_refused_probe_as_alive(monkeypatch):
"""EPERM says the group exists but we may not signal it, not that it died.
Only ESRCH proves a process group is gone. Collapsing every OSError into
"gone" is the one error that makes teardown report a surviving session as
contained.
"""
import routes.shell_routes as shell_routes
def refuse(_pgid, _sig):
raise PermissionError(errno.EPERM, "Operation not permitted")
monkeypatch.setattr(shell_routes.os, "killpg", refuse)
assert shell_routes._session_alive(4242, 4242) is True
def gone(_pgid, _sig):
raise ProcessLookupError(errno.ESRCH, "No such process")
monkeypatch.setattr(shell_routes.os, "killpg", gone)
assert shell_routes._session_alive(4242, 4242) is False
async def test_terminate_pty_session_reports_a_group_it_may_not_signal(monkeypatch):
"""A session we cannot signal at all is reported as not contained.
Both the signal and the liveness probe are refused, so teardown has done
nothing and must say so rather than infer death from its own failure.
"""
import routes.shell_routes as shell_routes
def refuse(*_args):
raise PermissionError(errno.EPERM, "Operation not permitted")
monkeypatch.setattr(shell_routes, "PTY_KILL_GRACE", 0.01)
monkeypatch.setattr(shell_routes, "_session_pgid", lambda _: 4242)
monkeypatch.setattr(shell_routes.os, "killpg", refuse)
monkeypatch.setattr(shell_routes.os, "kill", refuse)
proc = SimpleNamespace(pid=4242, returncode=0, wait=None)
assert await shell_routes._terminate_pty_session(proc) is False
class TestFindLineBreak:
"""Test line-break detection in byte buffers."""
+1 -1
View File
@@ -88,7 +88,7 @@ def builtin_action_info(monkeypatch):
def _req(user):
return SimpleNamespace(state=SimpleNamespace(current_user=user))
return SimpleNamespace(state=SimpleNamespace(current_user=user), headers={})
def _endpoint(method, path):
+4 -1
View File
@@ -25,6 +25,7 @@ def _setup_db(tmp_path, monkeypatch):
next_run = Column(DateTime)
last_run = Column(DateTime)
prompt = Column(Text, default='')
request_authority_json = Column(Text)
trigger_type = Column(String, default='schedule')
schedule = Column(String, default='daily')
scheduled_time = Column(String, default='08:00')
@@ -187,9 +188,11 @@ def test_running_task_cancel_keeps_event_loop_responsive_during_database_lock(tm
from src.task_scheduler import TaskScheduler
from src.builtin_actions import BUILTIN_ACTIONS
monkeypatch.setenv('BACKGROUND_TASK_FOREGROUND_GATE', 'false')
from src.agent_runtime.authority import seal_task_authority
with session_local() as db:
db.add(ScheduledTask(id='running-task', owner='alice', name='Fixture action',
task_type='action', action='fixture_wait', status='active'))
task_type='action', action='fixture_wait', status='active',
request_authority_json=seal_task_authority('', 'action', 'fixture_wait', owner='alice')))
db.commit()
started = asyncio.Event()
async def external_action(**kwargs):
+15 -1
View File
@@ -4,6 +4,14 @@ import time
from collections import namedtuple
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 src.tool_approvals import ToolApprovalStore, document_content_digest
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
@@ -325,6 +333,8 @@ def test_approved_document_version_guard_rejects_changed_target():
@pytest.mark.asyncio
async def test_missing_sealed_document_does_not_fall_back_to_another(monkeypatch):
import sys
from types import ModuleType
import src.agent_tools.document_tools as document_tools
class FakeDb:
@@ -334,7 +344,11 @@ async def test_missing_sealed_document_does_not_fall_back_to_another(monkeypatch
def rollback(self):
pass
monkeypatch.setattr("src.database.SessionLocal", lambda: FakeDb())
database = ModuleType("src.database")
database.SessionLocal = lambda: FakeDb()
database.Document = object
database.DocumentVersion = object
monkeypatch.setitem(sys.modules, "src.database", database)
monkeypatch.setattr(
document_tools,
"_get_owned_document",
+8
View File
@@ -18,6 +18,14 @@ from types import SimpleNamespace
from unittest.mock import patch
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))
def _make_block(tool_type, content):
+3
View File
@@ -17,6 +17,9 @@ from src.tool_policy import (
web_search_enabled_for_turn,
)
from src.turn_contract import requested_capabilities
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
def _collect(gen):
+2
View File
@@ -1806,6 +1806,8 @@ def test_candidate_schema_changes_cannot_mutate_contract():
async def test_dispatcher_enforces_bound_contract_without_external_mutations(monkeypatch):
from src import tool_execution, tool_implementations
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
handler = AsyncMock(return_value={"events": [], "exit_code": 0})
monkeypatch.setattr(tool_implementations, "do_manage_calendar", handler)
+8
View File
@@ -4,6 +4,14 @@ import json
from unittest.mock import AsyncMock, Mock
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 src.tool_policy import ToolPolicy
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
+3
View File
@@ -11,6 +11,9 @@ from src.agent_tools import ToolBlock, TOOL_TAGS # import first to avoid circul
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from src.tool_index import ALWAYS_AVAILABLE, BUILTIN_TOOL_DESCRIPTIONS
from src.tool_security import is_public_blocked_tool
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
def _run(content):
+3
View File
@@ -10,6 +10,9 @@ from src.tool_policy import WEB_TOOL_NAMES, ToolPolicy
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
from src.tool_types import ToolBlock
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
from src.turn_contract import FAMILY_TOOLS
+4
View File
@@ -17,6 +17,7 @@ import tempfile
from types import SimpleNamespace
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
from src.tool_execution import (
NO_TOOL_SECURITY_CONTEXT,
@@ -31,6 +32,9 @@ from src.tool_execution import (
)
_execute_tool_block = server_authorized_executor(_execute_tool_block)
async def execute_tool_block(*args, **kwargs):
kwargs.setdefault("security_context", NO_TOOL_SECURITY_CONTEXT)
return await _execute_tool_block(*args, **kwargs)