mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 23:42:21 +02:00
fix(runtime): fold native execution into shared containment (ODY-152)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""Captured spawns exercise the production runner without signalling fake PIDs."""
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src import containment
|
||||
|
||||
|
||||
def capture_owned_spawn(monkeypatch, tmp_path):
|
||||
captured = {}
|
||||
monkeypatch.setattr(containment, "CONTAINMENT_MODE", containment.MODE_REPORT_ONLY)
|
||||
monkeypatch.setattr(containment, "_store_path", lambda: tmp_path / "grants.json")
|
||||
monkeypatch.setattr(containment, "_pgid_of", lambda pid: pid)
|
||||
|
||||
async def fake_exec(*argv, **kwargs):
|
||||
captured.update(argv=argv, kwargs=kwargs, command=argv[-1])
|
||||
stdout = asyncio.StreamReader()
|
||||
stdout.feed_data(b"ok")
|
||||
stdout.feed_eof()
|
||||
stderr = asyncio.StreamReader()
|
||||
stderr.feed_eof()
|
||||
async def wait():
|
||||
return 0
|
||||
return SimpleNamespace(pid=99999999, stdout=stdout, stderr=stderr,
|
||||
returncode=0, wait=wait)
|
||||
|
||||
async def release(*args, **kwargs):
|
||||
return containment.ReleaseOutcome(dead=True, escalated=False)
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_exec)
|
||||
monkeypatch.setattr(containment, "_release_awaited", release)
|
||||
return captured
|
||||
@@ -188,25 +188,14 @@ def test_direct_bash_subprocess_has_closed_stdin(monkeypatch, tmp_path):
|
||||
from src.agent_tools import subprocess_tools
|
||||
from src import tool_execution
|
||||
|
||||
captured = {}
|
||||
sentinel = SimpleNamespace(pid=12345)
|
||||
|
||||
async def fake_create(command, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return sentinel
|
||||
|
||||
async def fake_stream(proc, **_kwargs):
|
||||
assert proc is sentinel
|
||||
return "ok", "", 0, False
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_shell", fake_create)
|
||||
monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream)
|
||||
from tests.containment_helpers import capture_owned_spawn
|
||||
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(tmp_path))
|
||||
|
||||
result = asyncio.run(subprocess_tools.BashTool().execute("echo ok", {}))
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert captured["stdin"] is asyncio.subprocess.DEVNULL
|
||||
assert captured["kwargs"]["stdin"] is asyncio.subprocess.DEVNULL
|
||||
assert not (tmp_path / ".tmp").exists()
|
||||
|
||||
|
||||
@@ -258,19 +247,8 @@ def test_bash_allows_unicode_ffmpeg_drawtext_with_explicit_fontfile(monkeypatch,
|
||||
from src.agent_tools import subprocess_tools
|
||||
from src import tool_execution
|
||||
|
||||
captured = {}
|
||||
sentinel = SimpleNamespace(pid=12345)
|
||||
|
||||
async def fake_create(command, **kwargs):
|
||||
captured["command"] = command
|
||||
return sentinel
|
||||
|
||||
async def fake_stream(proc, **_kwargs):
|
||||
assert proc is sentinel
|
||||
return "ok", "", 0, False
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_shell", fake_create)
|
||||
monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream)
|
||||
from tests.containment_helpers import capture_owned_spawn
|
||||
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(tmp_path))
|
||||
|
||||
command = (
|
||||
|
||||
@@ -6,6 +6,8 @@ import pytest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.agent_tools import subprocess_tools
|
||||
from src import containment
|
||||
from tests.containment_helpers import capture_owned_spawn
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -84,26 +86,17 @@ async def test_windows_bash_applies_the_subprocess_env(monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_windows_bash_tool_passes_ctx_env_through_to_the_child(monkeypatch):
|
||||
captured = {}
|
||||
async def test_windows_bash_tool_passes_ctx_env_through_to_the_child(monkeypatch, tmp_path):
|
||||
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
||||
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)
|
||||
monkeypatch.setattr(containment, "IS_WINDOWS", True)
|
||||
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))
|
||||
|
||||
result = await subprocess_tools.BashTool().execute(
|
||||
"pwd",
|
||||
@@ -134,9 +127,13 @@ async def test_windows_bash_without_git_bash_fails_clearly(monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_tool_returns_install_hint_when_git_bash_is_missing(monkeypatch):
|
||||
async def test_bash_tool_returns_install_hint_when_git_bash_is_missing(monkeypatch, tmp_path):
|
||||
capture_owned_spawn(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
||||
monkeypatch.setattr(subprocess_tools, "find_bash", lambda: None)
|
||||
monkeypatch.setattr(containment, "IS_WINDOWS", True)
|
||||
monkeypatch.setattr(containment, "find_bash", lambda: None)
|
||||
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: str(tmp_path))
|
||||
|
||||
result = await subprocess_tools.BashTool().execute(
|
||||
"pwd",
|
||||
@@ -148,11 +145,13 @@ async def test_bash_tool_returns_install_hint_when_git_bash_is_missing(monkeypat
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch):
|
||||
captured = {}
|
||||
workspace = r"D:\Workspaces\Project with spaces"
|
||||
async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch, tmp_path):
|
||||
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
||||
workspace = str(tmp_path)
|
||||
|
||||
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
||||
monkeypatch.setattr(containment, "IS_WINDOWS", True)
|
||||
monkeypatch.setattr(containment, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe")
|
||||
monkeypatch.setattr(
|
||||
subprocess_tools.shutil,
|
||||
"which",
|
||||
@@ -163,17 +162,7 @@ async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch):
|
||||
async def fail_tmux(*_args, **_kwargs):
|
||||
pytest.fail("native Windows must not enter the POSIX tmux path")
|
||||
|
||||
async def fake_create(command, **kwargs):
|
||||
captured["command"] = command
|
||||
captured["kwargs"] = kwargs
|
||||
return SimpleNamespace(pid=12345)
|
||||
|
||||
async def fake_stream(_process, **_kwargs):
|
||||
return "ok", "", 0, False
|
||||
|
||||
monkeypatch.setattr(subprocess_tools, "_run_tmux_bash", fail_tmux)
|
||||
monkeypatch.setattr(subprocess_tools, "_create_bash_subprocess", fake_create)
|
||||
monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream)
|
||||
|
||||
result = await subprocess_tools.BashTool().execute(
|
||||
"pwd",
|
||||
@@ -185,7 +174,7 @@ async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch):
|
||||
# Every bash result now carries the execution boundary it actually got.
|
||||
# Asserting dict equality here would make that field impossible to add
|
||||
# without touching a test about tmux, so the shape is asserted instead.
|
||||
assert result["containment"]["reported_dimensions"] == ["filesystem"]
|
||||
assert result["containment"]["network"] == "inherit"
|
||||
assert captured["command"] == "pwd"
|
||||
assert captured["kwargs"]["cwd"] == workspace
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Native execution must use the shared boundary and report actual teardown."""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from src import containment, tool_execution
|
||||
from src.agent_tools import subprocess_tools
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def native_boundary(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(tmp_path))
|
||||
monkeypatch.setattr(containment, "_store_path", lambda: tmp_path / "grants.json")
|
||||
monkeypatch.setattr(containment, "CONTAINMENT_MODE", containment.MODE_REPORT_ONLY)
|
||||
monkeypatch.setattr(containment, "MECHANISMS", tuple(
|
||||
m for m in containment.MECHANISMS if m.name == "process_group"
|
||||
))
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="real POSIX group teardown")
|
||||
async def test_native_bash_owns_and_releases_its_child(native_boundary):
|
||||
result = await subprocess_tools.BashTool().execute(
|
||||
"if read answer; then echo unexpected; else printf '%s' \"$ODY_TEST_ENV\"; fi",
|
||||
{"subproc_env": {"PATH": "/usr/bin:/bin", "ODY_TEST_ENV": "captured"}},
|
||||
)
|
||||
assert result["output"] == "captured"
|
||||
assert result["exit_code"] == 0
|
||||
assert result["teardown"]["dead"] is True
|
||||
assert result["containment"]["enforced"] == ["process_tree", "wall_clock"]
|
||||
assert result["containment"]["unenforced_required"] == ["filesystem"]
|
||||
assert result["containment"]["network"] == "inherit"
|
||||
assert containment.active_grants() == []
|
||||
|
||||
|
||||
async def test_native_bash_refuses_before_spawn_when_required_boundary_missing(monkeypatch):
|
||||
monkeypatch.setattr(containment, "CONTAINMENT_MODE", containment.MODE_ENFORCING)
|
||||
async def forbidden(*args, **kwargs):
|
||||
pytest.fail("refused command reached spawn")
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", forbidden)
|
||||
result = await subprocess_tools.BashTool().execute("echo hello", {})
|
||||
assert result["containment"]["executed"] is False
|
||||
assert result["containment"]["unenforced_required"] == ["filesystem"]
|
||||
|
||||
|
||||
async def test_failed_spawn_releases_unstarted_grant(native_boundary, monkeypatch):
|
||||
async def fail(*args, **kwargs):
|
||||
raise OSError("spawn failed")
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", fail)
|
||||
result = await subprocess_tools.BashTool().execute("echo hello", {})
|
||||
assert result["exit_code"] == 1
|
||||
assert containment.active_grants() == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="real POSIX process")
|
||||
async def test_long_line_is_drained_and_truncation_reported(native_boundary):
|
||||
spec = containment.ContainmentSpec(
|
||||
workspace=str(native_boundary), env=dict(os.environ), wall_clock_s=5,
|
||||
required=frozenset({containment.PROCESS_TREE, containment.WALL_CLOCK}),
|
||||
max_output_bytes=100,
|
||||
)
|
||||
result = await containment.run(containment.acquire(spec, owner="long-line"),
|
||||
[sys.executable, "-c", "print('x' * 200000)"], argv=True)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout == "x" * 100
|
||||
assert result.output_truncated is True
|
||||
assert result.release.dead is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="real POSIX process")
|
||||
async def test_output_exactly_at_cap_is_complete(native_boundary):
|
||||
spec = containment.ContainmentSpec(
|
||||
workspace=str(native_boundary), env=dict(os.environ), wall_clock_s=5,
|
||||
required=frozenset({containment.PROCESS_TREE, containment.WALL_CLOCK}), max_output_bytes=100,
|
||||
)
|
||||
result = await containment.run(containment.acquire(spec, owner="exact-cap"),
|
||||
[sys.executable, "-c", "import sys; sys.stdout.write('x' * 100)"], argv=True)
|
||||
assert len(result.stdout) == 100
|
||||
assert result.output_truncated is False
|
||||
|
||||
|
||||
def test_permission_denied_is_not_verified_death(monkeypatch):
|
||||
from core import platform_compat
|
||||
def denied(*args):
|
||||
raise PermissionError("EPERM")
|
||||
monkeypatch.setattr(platform_compat, "IS_WINDOWS", False)
|
||||
monkeypatch.setattr(os, "kill", denied)
|
||||
monkeypatch.setattr(os, "killpg", denied)
|
||||
monkeypatch.setattr(containment, "_own_pgid", lambda: 1)
|
||||
assert platform_compat.pid_alive(987654) is True
|
||||
assert containment._group_present(987654) is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="real POSIX process")
|
||||
async def test_blocked_stdin_is_inside_wall_clock(native_boundary):
|
||||
spec = containment.ContainmentSpec(
|
||||
workspace=str(native_boundary), env=dict(os.environ), wall_clock_s=1,
|
||||
required=frozenset({containment.PROCESS_TREE, containment.WALL_CLOCK}),
|
||||
)
|
||||
result = await asyncio.wait_for(containment.run(
|
||||
containment.acquire(spec, owner="blocked-stdin"), "sleep 60", stdin=b"x" * 2000000,
|
||||
), timeout=8)
|
||||
assert result.timed_out is True
|
||||
assert result.release.dead is True
|
||||
@@ -165,6 +165,7 @@ def test_a_gone_leader_with_a_live_group_is_reported_not_killed(
|
||||
def test_a_verified_grant_is_torn_down_normally(grant_store, monkeypatch):
|
||||
seed_grant()
|
||||
verdicts(monkeypatch, {4242: process_ownership.OWNED})
|
||||
monkeypatch.setattr(containment, "_pgid_of", lambda pid: 4242)
|
||||
signals = []
|
||||
monkeypatch.setattr(
|
||||
containment, "_signal_tree",
|
||||
@@ -179,6 +180,28 @@ def test_a_verified_grant_is_torn_down_normally(grant_store, monkeypatch):
|
||||
assert outcome.ownership == ""
|
||||
|
||||
|
||||
def test_verified_leader_does_not_authorize_a_different_group(grant_store, monkeypatch):
|
||||
seed_grant(pgid=9999)
|
||||
verdicts(monkeypatch, {4242: process_ownership.OWNED})
|
||||
monkeypatch.setattr(containment, "_pgid_of", lambda pid: 4242)
|
||||
monkeypatch.setattr(containment, "_signal_tree", lambda *args: pytest.fail("foreign group signalled"))
|
||||
outcome = containment.reap_record(grant_store()["grant-1"])
|
||||
assert outcome.dead is False
|
||||
assert outcome.ownership == process_ownership.UNVERIFIABLE
|
||||
|
||||
|
||||
def test_reaper_retains_a_group_after_its_leader_dies(grant_store, monkeypatch):
|
||||
from src import process_reaper
|
||||
seed_grant()
|
||||
verdicts(monkeypatch, {4242: process_ownership.GONE})
|
||||
monkeypatch.setattr(containment, "_group_present", lambda pgid: True)
|
||||
monkeypatch.setattr(containment, "_signal_tree", lambda *args: pytest.fail("unidentified group signalled"))
|
||||
report = process_reaper.reap_containment_grants()
|
||||
assert report["failed"] == 1
|
||||
assert report["already_gone"] == 0
|
||||
assert "grant-1" in grant_store()
|
||||
|
||||
|
||||
def test_an_in_process_grant_is_not_subjected_to_the_gate(monkeypatch, tmp_path):
|
||||
"""A grant carrying its own pid belongs to the caller holding it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user