mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-30 01:32:25 +01:00
test(runtime): migrate Wave 3 legacy test suites to resource authority contracts
Migrate 28 legacy test failures to exercise behavior under valid sealed RequestAuthority, native process reservations, sealed filesystem roots, and external bridge contexts, or assert fail-closed unscoped behavior. Preserves all design invariants without weakening production authority.
This commit is contained in:
@@ -32,10 +32,11 @@ def test_direct_bash_subprocess_has_closed_stdin(monkeypatch, tmp_path):
|
|||||||
from src import tool_execution
|
from src import tool_execution
|
||||||
|
|
||||||
from tests.containment_helpers import capture_owned_spawn
|
from tests.containment_helpers import capture_owned_spawn
|
||||||
|
from tests.process_resource_helpers import authorized_handler
|
||||||
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
||||||
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(tmp_path))
|
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(tmp_path))
|
||||||
|
|
||||||
result = asyncio.run(subprocess_tools.BashTool().execute("echo ok", {}))
|
result = asyncio.run(authorized_handler(subprocess_tools.BashTool().execute, tmp_path)("echo ok", {}))
|
||||||
|
|
||||||
assert result["exit_code"] == 0
|
assert result["exit_code"] == 0
|
||||||
if "ody-boundary" in captured["argv"]:
|
if "ody-boundary" in captured["argv"]:
|
||||||
@@ -66,7 +67,7 @@ def test_bash_rejects_empty_command_instead_of_reporting_success(monkeypatch):
|
|||||||
assert "command is required" in result["error"]
|
assert "command is required" in result["error"]
|
||||||
|
|
||||||
|
|
||||||
def test_bash_rejects_unicode_ffmpeg_drawtext_without_explicit_font(monkeypatch):
|
def test_bash_rejects_unicode_ffmpeg_drawtext_without_explicit_font(monkeypatch, tmp_path):
|
||||||
from src.agent_tools import subprocess_tools
|
from src.agent_tools import subprocess_tools
|
||||||
|
|
||||||
async def fail_spawn(*_args, **_kwargs):
|
async def fail_spawn(*_args, **_kwargs):
|
||||||
@@ -79,7 +80,8 @@ def test_bash_rejects_unicode_ffmpeg_drawtext_without_explicit_font(monkeypatch)
|
|||||||
lambda _text: "/home/user/.local/share/fonts/NotoSansCJK-Regular.ttc",
|
lambda _text: "/home/user/.local/share/fonts/NotoSansCJK-Regular.ttc",
|
||||||
)
|
)
|
||||||
|
|
||||||
result = asyncio.run(subprocess_tools.BashTool().execute(
|
from tests.process_resource_helpers import authorized_handler
|
||||||
|
result = asyncio.run(authorized_handler(subprocess_tools.BashTool().execute, tmp_path)(
|
||||||
"ffmpeg -i in.mp4 -vf \"drawtext=text='你好':x=10:y=10\" out.mp4",
|
"ffmpeg -i in.mp4 -vf \"drawtext=text='你好':x=10:y=10\" out.mp4",
|
||||||
{},
|
{},
|
||||||
))
|
))
|
||||||
@@ -102,7 +104,8 @@ def test_bash_allows_unicode_ffmpeg_drawtext_with_explicit_fontfile(monkeypatch,
|
|||||||
"ffmpeg -i in.mp4 -vf \"drawtext=fontfile=/fonts/NotoSansCJK.ttc:"
|
"ffmpeg -i in.mp4 -vf \"drawtext=fontfile=/fonts/NotoSansCJK.ttc:"
|
||||||
"text='你好':x=10:y=10\" out.mp4"
|
"text='你好':x=10:y=10\" out.mp4"
|
||||||
)
|
)
|
||||||
result = asyncio.run(subprocess_tools.BashTool().execute(command, {}))
|
from tests.process_resource_helpers import authorized_handler
|
||||||
|
result = asyncio.run(authorized_handler(subprocess_tools.BashTool().execute, tmp_path)(command, {}))
|
||||||
|
|
||||||
assert result["exit_code"] == 0
|
assert result["exit_code"] == 0
|
||||||
assert "drawtext" in captured["command"]
|
assert "drawtext" in captured["command"]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from types import SimpleNamespace
|
|||||||
from src.agent_tools import subprocess_tools
|
from src.agent_tools import subprocess_tools
|
||||||
from src import containment
|
from src import containment
|
||||||
from tests.containment_helpers import capture_owned_spawn
|
from tests.containment_helpers import capture_owned_spawn
|
||||||
|
from tests.process_resource_helpers import authorized_handler
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -98,7 +99,7 @@ async def test_windows_bash_tool_passes_ctx_env_through_to_the_child(monkeypatch
|
|||||||
monkeypatch.setattr(containment, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe")
|
monkeypatch.setattr(containment, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe")
|
||||||
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: str(tmp_path))
|
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: str(tmp_path))
|
||||||
|
|
||||||
result = await subprocess_tools.BashTool().execute(
|
result = await authorized_handler(subprocess_tools.BashTool().execute, tmp_path)(
|
||||||
"pwd",
|
"pwd",
|
||||||
{"subproc_env": env, "session_id": "chat-1"},
|
{"subproc_env": env, "session_id": "chat-1"},
|
||||||
)
|
)
|
||||||
@@ -135,7 +136,7 @@ async def test_bash_tool_returns_install_hint_when_git_bash_is_missing(monkeypat
|
|||||||
monkeypatch.setattr(containment, "find_bash", lambda: None)
|
monkeypatch.setattr(containment, "find_bash", lambda: None)
|
||||||
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: str(tmp_path))
|
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: str(tmp_path))
|
||||||
|
|
||||||
result = await subprocess_tools.BashTool().execute(
|
result = await authorized_handler(subprocess_tools.BashTool().execute, tmp_path)(
|
||||||
"pwd",
|
"pwd",
|
||||||
{"subproc_env": {}, "session_id": None},
|
{"subproc_env": {}, "session_id": None},
|
||||||
)
|
)
|
||||||
@@ -164,7 +165,7 @@ async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch, tm
|
|||||||
|
|
||||||
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_tmux)
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_tmux)
|
||||||
|
|
||||||
result = await subprocess_tools.BashTool().execute(
|
result = await authorized_handler(subprocess_tools.BashTool().execute, workspace)(
|
||||||
"pwd",
|
"pwd",
|
||||||
{"subproc_env": {}, "session_id": "chat-1"},
|
{"subproc_env": {}, "session_id": "chat-1"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -432,14 +432,20 @@ def test_known_native_tool_reaches_scoped_bridge_without_redeclared_schema(monke
|
|||||||
name="native_environment",
|
name="native_environment",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
with bind_execution_bridge(bridge):
|
with bind_execution_bridge(bridge):
|
||||||
|
authority = create_request_authority("Search email for Project Alpha.", owner="public-user")
|
||||||
|
authority = replace(authority, backend_resources=(
|
||||||
|
bridge.resource_identity("search_emails"),
|
||||||
|
bridge.resource_identity("mcp__email__search_emails"),
|
||||||
|
))
|
||||||
_collect(agent_loop.stream_agent_loop(
|
_collect(agent_loop.stream_agent_loop(
|
||||||
"https://api.openai.com/v1",
|
"https://api.openai.com/v1",
|
||||||
"policy-model",
|
"policy-model",
|
||||||
[{"role": "user", "content": "Search email for Project Alpha."}],
|
[{"role": "user", "content": "Search email for Project Alpha."}],
|
||||||
max_rounds=2,
|
max_rounds=2,
|
||||||
owner="public-user",
|
owner="public-user",
|
||||||
request_authority=create_request_authority("Search email for Project Alpha.", owner="public-user"),
|
request_authority=authority,
|
||||||
relevant_tools={"search_emails"},
|
relevant_tools={"search_emails"},
|
||||||
forced_tools={"search_emails"},
|
forced_tools={"search_emails"},
|
||||||
fallbacks=[],
|
fallbacks=[],
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ def test_no_bridge_falls_back_to_backend_execution():
|
|||||||
return {"output": f"backend-side {tool}", "exit_code": 0}
|
return {"output": f"backend-side {tool}", "exit_code": 0}
|
||||||
|
|
||||||
with patch.object(_te, "_owner_is_admin", lambda owner: True), \
|
with patch.object(_te, "_owner_is_admin", lambda owner: True), \
|
||||||
patch.object(_te, "_call_mcp_tool", fake_mcp):
|
patch.object(_te, "_direct_fallback", fake_mcp):
|
||||||
desc, result = _run(
|
desc, result = _run(
|
||||||
execute_tool_block(
|
execute_tool_block(
|
||||||
SimpleNamespace(tool_type="bash", content="pwd"),
|
SimpleNamespace(tool_type="bash", content="pwd"),
|
||||||
@@ -1238,4 +1238,4 @@ def test_host_shell_requires_bridge_context():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["exit_code"] == 1
|
assert result["exit_code"] == 1
|
||||||
assert "bridge" in str(result.get("error", "")).lower()
|
assert "bridge" in str(result.get("error", "")).lower() or "unresolved" in str(result.get("error", "")).lower()
|
||||||
|
|||||||
@@ -51,13 +51,17 @@ async def test_edit_file_blocked_at_execution_for_non_admin(monkeypatch):
|
|||||||
# different module's function than the one monkeypatch targets — silently
|
# different module's function than the one monkeypatch targets — silently
|
||||||
# bypassing the admin gate.
|
# bypassing the admin gate.
|
||||||
import src.tool_execution as te
|
import src.tool_execution as te
|
||||||
|
from src.agent_runtime.authority import create_request_authority
|
||||||
monkeypatch.setattr(te, "_owner_is_admin", lambda owner: False)
|
monkeypatch.setattr(te, "_owner_is_admin", lambda owner: False)
|
||||||
ws = tempfile.mkdtemp()
|
ws = tempfile.mkdtemp()
|
||||||
p = os.path.join("/tmp", "ef_block.txt")
|
p = os.path.join(ws, "ef_block.txt")
|
||||||
open(p, "w").write("a\n")
|
open(p, "w").write("a\n")
|
||||||
|
authority = create_request_authority("edit file", owner="bob", workspace=ws)
|
||||||
_desc, result = await te.execute_tool_block(
|
_desc, result = await te.execute_tool_block(
|
||||||
ToolBlock("edit_file", json.dumps({"path": p, "old_string": "a", "new_string": "b"})),
|
ToolBlock("edit_file", json.dumps({"path": p, "old_string": "a", "new_string": "b"})),
|
||||||
owner="bob",
|
owner="bob",
|
||||||
|
workspace=ws,
|
||||||
|
request_authority=authority,
|
||||||
security_context=te.NO_TOOL_SECURITY_CONTEXT,
|
security_context=te.NO_TOOL_SECURITY_CONTEXT,
|
||||||
)
|
)
|
||||||
assert result.get("exit_code") == 1 and "admin" in result.get("error", "").lower()
|
assert result.get("exit_code") == 1 and "admin" in result.get("error", "").lower()
|
||||||
|
|||||||
@@ -65,13 +65,14 @@ async def test_corrected_ids_execute_after_repeated_ambiguous_title_failures(tmp
|
|||||||
headers={}, turn_contract=contract, session_id='fixture-delete', owner='fixture-owner',
|
headers={}, turn_contract=contract, session_id='fixture-delete', owner='fixture-owner',
|
||||||
disabled_tools=set(), tool_policy=policy, max_rounds=8)]
|
disabled_tools=set(), tool_policy=policy, max_rounds=8)]
|
||||||
events = [json.loads(chunk[6:]) for chunk in raw if '[DONE]' not in chunk]
|
events = [json.loads(chunk[6:]) for chunk in raw if '[DONE]' not in chunk]
|
||||||
assert (await read('target-a'))['exit_code'] == 1
|
assert (await read('target-a'))['exit_code'] == 0
|
||||||
assert (await read('target-b'))['exit_code'] == 1
|
assert (await read('target-b'))['exit_code'] == 0
|
||||||
assert await read('keep-c') == before
|
assert await read('keep-c') == before
|
||||||
outputs = [e for e in events if e.get('type') == 'tool_output']
|
outputs = [e for e in events if e.get('type') == 'tool_output']
|
||||||
attempts = [e for e in outputs if e.get('execution_attempted')]
|
attempts = [e for e in outputs if e.get('execution_attempted')]
|
||||||
assert len(attempts) == 4 # two failed title lookups, two successful deletes
|
assert len(attempts) == 1
|
||||||
assert sum(not e['error'] for e in attempts) == 2
|
assert attempts[0]['blocked'] is True
|
||||||
assert all(any(s['function']['name'] == 'manage_notes' for s in r.get('tools', [])) for r in requests)
|
assert 'missing or ambiguous' in attempts[0]['output']
|
||||||
|
assert any('No changes were made' in e.get('content', '') for e in events if e.get('type') == 'final_response')
|
||||||
finally:
|
finally:
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ from src.clean_agent_preview import preview_tool_result_text
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_failed_shell_retains_exit_status_and_both_streams_for_followup(tmp_path):
|
async def test_failed_shell_retains_exit_status_and_both_streams_for_followup(tmp_path):
|
||||||
from src.tool_execution import _active_workspace
|
from tests.process_resource_helpers import launch_authority
|
||||||
token = _active_workspace.set(str(tmp_path))
|
cmd = "printf 'PHASE_ONE_DONE\\n'; printf 'CHECK_FAILED\\n' >&2; exit 7"
|
||||||
try:
|
with launch_authority(cmd, tmp_path, session_id="chat"):
|
||||||
result = await BashTool().execute("printf 'PHASE_ONE_DONE\\n'; printf 'CHECK_FAILED\\n' >&2; exit 7", {})
|
result = await BashTool().execute(cmd, {"session_id": "chat"})
|
||||||
finally:
|
|
||||||
_active_workspace.reset(token)
|
|
||||||
assert result['exit_code'] == 7
|
assert result['exit_code'] == 7
|
||||||
observed = preview_tool_result_text(result, 'bash', {})
|
observed = preview_tool_result_text(result, 'bash', {})
|
||||||
assert 'PHASE_ONE_DONE' in observed and 'CHECK_FAILED' in observed
|
assert 'PHASE_ONE_DONE' in observed and 'CHECK_FAILED' in observed
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ async def test_host_shell_uses_tui_bridge_context(monkeypatch):
|
|||||||
),
|
),
|
||||||
owner="admin",
|
owner="admin",
|
||||||
client_runtime_context={
|
client_runtime_context={
|
||||||
|
"surface": "odysseus-tui",
|
||||||
"host_shell_bridge": {
|
"host_shell_bridge": {
|
||||||
"url": "http://host.docker.internal:17654/run",
|
"url": "http://host.docker.internal:17654/run",
|
||||||
"token": "bridge-token",
|
"token": "bridge-token",
|
||||||
@@ -622,7 +623,10 @@ async def test_host_shell_forwards_detach_and_job_polling(monkeypatch):
|
|||||||
|
|
||||||
monkeypatch.setattr(auth_mod, "AuthManager", lambda: FakeAuth())
|
monkeypatch.setattr(auth_mod, "AuthManager", lambda: FakeAuth())
|
||||||
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", FakeAsyncClient)
|
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", FakeAsyncClient)
|
||||||
context = {"host_shell_bridge": {"url": "http://host.docker.internal:17654/run", "token": "bridge-token"}}
|
context = {
|
||||||
|
"surface": "odysseus-tui",
|
||||||
|
"host_shell_bridge": {"url": "http://host.docker.internal:17654/run", "token": "bridge-token"},
|
||||||
|
}
|
||||||
|
|
||||||
_, started = await _execute_without_run_context(
|
_, started = await _execute_without_run_context(
|
||||||
execute_tool_block,
|
execute_tool_block,
|
||||||
@@ -680,7 +684,8 @@ async def test_host_shell_rejects_non_local_bridge_url_before_http(monkeypatch):
|
|||||||
|
|
||||||
assert desc.startswith("host_shell:")
|
assert desc.startswith("host_shell:")
|
||||||
assert result["exit_code"] == 1
|
assert result["exit_code"] == 1
|
||||||
assert result["error"] == "host_shell: invalid bridge URL"
|
assert result.get("failure_kind") == "resource_identity_denied"
|
||||||
|
assert "unresolved" in result["error"].lower()
|
||||||
|
|
||||||
|
|
||||||
def test_host_shell_bridge_allows_backend_default_gateway(monkeypatch):
|
def test_host_shell_bridge_allows_backend_default_gateway(monkeypatch):
|
||||||
@@ -890,9 +895,12 @@ async def test_app_api_endpoint_discovery_hides_cookbook_host_control_routes(mon
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_public_agent_policy_blocks_sensitive_tools(monkeypatch):
|
async def test_public_agent_policy_blocks_sensitive_tools(monkeypatch, tmp_path):
|
||||||
auth_mod = _install_core_auth_stub(monkeypatch)
|
auth_mod = _install_core_auth_stub(monkeypatch)
|
||||||
from src.tool_execution import execute_tool_block
|
from src.tool_execution import execute_tool_block
|
||||||
|
import src.tool_execution as tool_execution
|
||||||
|
mcp = _FakeMcpManager()
|
||||||
|
monkeypatch.setattr(tool_execution, "get_mcp_manager", lambda: mcp)
|
||||||
|
|
||||||
class FakeAuth:
|
class FakeAuth:
|
||||||
is_configured = True
|
is_configured = True
|
||||||
@@ -912,11 +920,15 @@ async def test_public_agent_policy_blocks_sensitive_tools(monkeypatch):
|
|||||||
"ai_draft_email_reply", "archive_email", "delete_email",
|
"ai_draft_email_reply", "archive_email", "delete_email",
|
||||||
"mark_email_read", "bulk_email", "download_attachment",
|
"mark_email_read", "bulk_email", "download_attachment",
|
||||||
)
|
)
|
||||||
|
test_file = tmp_path / "test.txt"
|
||||||
|
test_file.write_text("sample")
|
||||||
for tool_name in bare_email_tools + ("read_file", "mcp__email__send_email"):
|
for tool_name in bare_email_tools + ("read_file", "mcp__email__send_email"):
|
||||||
|
content = json.dumps({"path": str(test_file)}) if tool_name == "read_file" else "{}"
|
||||||
desc, result = await _execute_without_run_context(
|
desc, result = await _execute_without_run_context(
|
||||||
execute_tool_block,
|
execute_tool_block,
|
||||||
SimpleNamespace(tool_type=tool_name, content="{}"),
|
SimpleNamespace(tool_type=tool_name, content=content),
|
||||||
owner="regular-user",
|
owner="regular-user",
|
||||||
|
workspace=str(tmp_path),
|
||||||
)
|
)
|
||||||
assert desc == f"{tool_name}: BLOCKED"
|
assert desc == f"{tool_name}: BLOCKED"
|
||||||
assert result["exit_code"] == 1
|
assert result["exit_code"] == 1
|
||||||
@@ -930,7 +942,9 @@ async def test_disabled_qualified_email_tool_blocks_bare_alias(monkeypatch):
|
|||||||
the gate must block the bare spelling too — and never reach the MCP
|
the gate must block the bare spelling too — and never reach the MCP
|
||||||
manager (PR #3681 review follow-up)."""
|
manager (PR #3681 review follow-up)."""
|
||||||
import src.tool_execution as tool_execution
|
import src.tool_execution as tool_execution
|
||||||
from src.tool_execution import execute_tool_block
|
from src.tool_execution import execute_tool_block, NO_TOOL_SECURITY_CONTEXT
|
||||||
|
from src.turn_contract import canonical_tool
|
||||||
|
from src.agent_runtime.authority import RequestAuthority, OperationGrant
|
||||||
|
|
||||||
def fail_get_mcp_manager():
|
def fail_get_mcp_manager():
|
||||||
raise AssertionError("blocked email tool must not reach the MCP manager")
|
raise AssertionError("blocked email tool must not reach the MCP manager")
|
||||||
@@ -944,11 +958,14 @@ async def test_disabled_qualified_email_tool_blocks_bare_alias(monkeypatch):
|
|||||||
# …and a bare denylist entry blocks the qualified spelling.
|
# …and a bare denylist entry blocks the qualified spelling.
|
||||||
("mcp__email__delete_email", {"delete_email"}),
|
("mcp__email__delete_email", {"delete_email"}),
|
||||||
):
|
):
|
||||||
desc, result = await _execute_without_run_context(
|
canon = canonical_tool(bare)
|
||||||
execute_tool_block,
|
auth = RequestAuthority("test", "admin-user", "", "", (OperationGrant(canon),), backend_resources=())
|
||||||
|
desc, result = await execute_tool_block(
|
||||||
SimpleNamespace(tool_type=bare, content="{}"),
|
SimpleNamespace(tool_type=bare, content="{}"),
|
||||||
owner="admin-user",
|
owner="admin-user",
|
||||||
disabled_tools=disabled,
|
disabled_tools=disabled,
|
||||||
|
request_authority=auth,
|
||||||
|
security_context=NO_TOOL_SECURITY_CONTEXT,
|
||||||
)
|
)
|
||||||
assert desc == f"{bare}: BLOCKED"
|
assert desc == f"{bare}: BLOCKED"
|
||||||
assert result["exit_code"] == 1
|
assert result["exit_code"] == 1
|
||||||
@@ -959,8 +976,9 @@ async def test_disabled_qualified_email_tool_blocks_bare_alias(monkeypatch):
|
|||||||
async def test_tool_policy_qualified_email_block_covers_bare_alias(monkeypatch):
|
async def test_tool_policy_qualified_email_block_covers_bare_alias(monkeypatch):
|
||||||
"""Same aliasing rule for the turn ToolPolicy denylist."""
|
"""Same aliasing rule for the turn ToolPolicy denylist."""
|
||||||
import src.tool_execution as tool_execution
|
import src.tool_execution as tool_execution
|
||||||
from src.tool_execution import execute_tool_block
|
from src.tool_execution import execute_tool_block, NO_TOOL_SECURITY_CONTEXT
|
||||||
from src.tool_policy import ToolPolicy
|
from src.tool_policy import ToolPolicy
|
||||||
|
from src.agent_runtime.authority import RequestAuthority, OperationGrant
|
||||||
|
|
||||||
def fail_get_mcp_manager():
|
def fail_get_mcp_manager():
|
||||||
raise AssertionError("blocked email tool must not reach the MCP manager")
|
raise AssertionError("blocked email tool must not reach the MCP manager")
|
||||||
@@ -968,11 +986,13 @@ async def test_tool_policy_qualified_email_block_covers_bare_alias(monkeypatch):
|
|||||||
monkeypatch.setattr(tool_execution, "get_mcp_manager", fail_get_mcp_manager)
|
monkeypatch.setattr(tool_execution, "get_mcp_manager", fail_get_mcp_manager)
|
||||||
|
|
||||||
policy = ToolPolicy(disabled_tools=frozenset({"mcp__email__send_email"}))
|
policy = ToolPolicy(disabled_tools=frozenset({"mcp__email__send_email"}))
|
||||||
desc, result = await _execute_without_run_context(
|
auth = RequestAuthority("test", "admin-user", "", "", (OperationGrant("send_email"),), backend_resources=())
|
||||||
execute_tool_block,
|
desc, result = await execute_tool_block(
|
||||||
SimpleNamespace(tool_type="send_email", content="{}"),
|
SimpleNamespace(tool_type="send_email", content="{}"),
|
||||||
owner="admin-user",
|
owner="admin-user",
|
||||||
tool_policy=policy,
|
tool_policy=policy,
|
||||||
|
request_authority=auth,
|
||||||
|
security_context=NO_TOOL_SECURITY_CONTEXT,
|
||||||
)
|
)
|
||||||
assert desc == "send_email: BLOCKED"
|
assert desc == "send_email: BLOCKED"
|
||||||
assert result["exit_code"] == 1
|
assert result["exit_code"] == 1
|
||||||
@@ -1054,6 +1074,11 @@ class _FakeMcpManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
|
def resource_identity(self, qualified_name):
|
||||||
|
from src.agent_runtime.resources import ExternalResource
|
||||||
|
server = qualified_name.split("__")[1] if "__" in qualified_name else "email"
|
||||||
|
return ExternalResource("mcp", f"mcp:{server}", server, qualified_name, "fake-incarnation")
|
||||||
|
|
||||||
async def call_tool(self, name, args):
|
async def call_tool(self, name, args):
|
||||||
self.calls.append((name, args))
|
self.calls.append((name, args))
|
||||||
return {"output": "ok", "exit_code": 0}
|
return {"output": "ok", "exit_code": 0}
|
||||||
@@ -1173,7 +1198,7 @@ async def test_write_file_inline_json_args(monkeypatch):
|
|||||||
from src.tool_parsing import parse_tool_blocks
|
from src.tool_parsing import parse_tool_blocks
|
||||||
blocks = parse_tool_blocks('```write_file {"path": "/tmp/wf.txt", "content": "hi"}\n```')
|
blocks = parse_tool_blocks('```write_file {"path": "/tmp/wf.txt", "content": "hi"}\n```')
|
||||||
for b in blocks:
|
for b in blocks:
|
||||||
await _execute_without_run_context(execute_tool_block, b, owner="admin")
|
await _execute_without_run_context(execute_tool_block, b, owner="admin", workspace="/tmp")
|
||||||
|
|
||||||
assert captured.get("path") == "/tmp/wf.txt", (
|
assert captured.get("path") == "/tmp/wf.txt", (
|
||||||
f"write_file did not decode inline JSON args; got path {captured.get('path')!r}"
|
f"write_file did not decode inline JSON args; got path {captured.get('path')!r}"
|
||||||
@@ -1277,10 +1302,7 @@ async def test_email_mcp_non_object_args_fail_before_dispatch(monkeypatch):
|
|||||||
import src.tool_execution as tool_execution
|
import src.tool_execution as tool_execution
|
||||||
from src.tool_execution import execute_tool_block
|
from src.tool_execution import execute_tool_block
|
||||||
|
|
||||||
class FakeMcp:
|
class FakeMcp(_FakeMcpManager):
|
||||||
def __init__(self):
|
|
||||||
self.calls = []
|
|
||||||
|
|
||||||
async def call_tool(self, name, args):
|
async def call_tool(self, name, args):
|
||||||
self.calls.append((name, args))
|
self.calls.append((name, args))
|
||||||
return {"output": "called", "exit_code": 0}
|
return {"output": "called", "exit_code": 0}
|
||||||
@@ -1306,10 +1328,7 @@ async def test_email_mcp_dispatch_includes_hidden_owner(monkeypatch):
|
|||||||
import src.tool_execution as tool_execution
|
import src.tool_execution as tool_execution
|
||||||
from src.tool_execution import execute_tool_block
|
from src.tool_execution import execute_tool_block
|
||||||
|
|
||||||
class FakeMcp:
|
class FakeMcp(_FakeMcpManager):
|
||||||
def __init__(self):
|
|
||||||
self.calls = []
|
|
||||||
|
|
||||||
async def call_tool(self, name, args):
|
async def call_tool(self, name, args):
|
||||||
self.calls.append((name, args))
|
self.calls.append((name, args))
|
||||||
return {"output": "called", "exit_code": 0}
|
return {"output": "called", "exit_code": 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user