mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 16:02:20 +02:00
fix(runtime): enforce functional containment after closing execution bypasses (ODY-141)
This commit is contained in:
@@ -14,18 +14,26 @@ def capture_owned_spawn(monkeypatch, tmp_path):
|
||||
async def fake_exec(*argv, **kwargs):
|
||||
captured.update(argv=argv, kwargs=kwargs, command=argv[-1])
|
||||
stdout = asyncio.StreamReader()
|
||||
if "ody-boundary" in argv:
|
||||
stdout.feed_data((argv[argv.index("ody-boundary") + 1] + "\n").encode())
|
||||
stdout.feed_data(b"ok")
|
||||
stdout.feed_eof()
|
||||
stderr = asyncio.StreamReader()
|
||||
stderr.feed_eof()
|
||||
async def wait():
|
||||
return 0
|
||||
async def drain():
|
||||
return None
|
||||
writer = SimpleNamespace(write=lambda data: None, drain=drain,
|
||||
close=lambda: captured.update(stdin_closed=True))
|
||||
return SimpleNamespace(pid=99999999, stdout=stdout, stderr=stderr,
|
||||
returncode=0, wait=wait)
|
||||
stdin=writer, returncode=0, wait=wait)
|
||||
|
||||
async def release(*args, **kwargs):
|
||||
return containment.ReleaseOutcome(dead=True, escalated=False)
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_exec)
|
||||
if hasattr(containment.os, "pidfd_open"):
|
||||
monkeypatch.delattr(containment.os, "pidfd_open")
|
||||
monkeypatch.setattr(containment, "_release_awaited", release)
|
||||
return captured
|
||||
|
||||
@@ -38,7 +38,11 @@ def test_direct_bash_subprocess_has_closed_stdin(monkeypatch, tmp_path):
|
||||
result = asyncio.run(subprocess_tools.BashTool().execute("echo ok", {}))
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert captured["kwargs"]["stdin"] is asyncio.subprocess.DEVNULL
|
||||
if "ody-boundary" in captured["argv"]:
|
||||
assert captured["kwargs"]["stdin"] is asyncio.subprocess.PIPE
|
||||
assert captured["stdin_closed"] is True
|
||||
else:
|
||||
assert captured["kwargs"]["stdin"] is asyncio.subprocess.DEVNULL
|
||||
assert not (tmp_path / ".tmp").exists()
|
||||
|
||||
|
||||
|
||||
@@ -349,10 +349,8 @@ def test_the_two_modes_differ_only_in_whether_the_shortfall_refuses(monkeypatch,
|
||||
assert frozenset(reported.unenforced_required) == caught.value.missing
|
||||
|
||||
|
||||
def test_report_only_is_the_shipped_default():
|
||||
"""Pinned deliberately: merging this must not change behaviour on a host
|
||||
without bubblewrap. Flipping it is a one-line diff, reviewed as one."""
|
||||
assert containment.CONTAINMENT_MODE == containment.MODE_REPORT_ONLY
|
||||
def test_enforcement_is_the_shipped_default():
|
||||
assert containment.CONTAINMENT_MODE == containment.MODE_ENFORCING
|
||||
|
||||
|
||||
# ── Spec validation: caller bugs raise in both modes ────────────────────────
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
"""Final enforcement gate: real namespaces, no fallback, and owned cancellation."""
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from src import containment, tool_execution
|
||||
from src.agent_tools import subprocess_tools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(tmp_path, monkeypatch):
|
||||
path = tmp_path / "workspace"
|
||||
path.mkdir()
|
||||
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(path))
|
||||
monkeypatch.setattr(containment, "_store_path", lambda: tmp_path / "grants.json")
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def namespaces(workspace):
|
||||
if not containment._bwrap_available():
|
||||
pytest.skip("functional bubblewrap PID/mount namespaces unavailable")
|
||||
return workspace
|
||||
|
||||
|
||||
def test_installed_but_nonfunctional_bwrap_is_not_available(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(containment, "IS_WINDOWS", False)
|
||||
monkeypatch.setattr(containment.shutil, "which", lambda name: "/usr/bin/bwrap")
|
||||
def blocked(argv, **kwargs):
|
||||
calls.append((argv, kwargs))
|
||||
return subprocess.CompletedProcess(argv, 1, b"", b"Operation not permitted")
|
||||
monkeypatch.setattr(containment.subprocess, "run", blocked)
|
||||
assert containment._bwrap_available() is False
|
||||
assert calls[0][0][-1] == "/bin/true"
|
||||
assert "--unshare-pid" in calls[0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool,source", [
|
||||
(subprocess_tools.BashTool, "echo forbidden"),
|
||||
(subprocess_tools.PythonTool, "print(1 + 1)"),
|
||||
])
|
||||
async def test_shipped_mode_refuses_without_namespaces(tool, source, workspace, monkeypatch):
|
||||
assert containment.CONTAINMENT_MODE == containment.MODE_ENFORCING
|
||||
monkeypatch.setattr(containment, "MECHANISMS", tuple(
|
||||
item for item in containment.MECHANISMS if item.name != "bubblewrap"
|
||||
))
|
||||
async def forbidden(*args, **kwargs):
|
||||
pytest.fail("uncontained model command spawned")
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", forbidden)
|
||||
result = await tool().execute(source, {})
|
||||
assert "containment unavailable" in result["error"]
|
||||
assert result["containment"]["executed"] is False
|
||||
assert result["containment"]["contained"] is False
|
||||
assert {"filesystem", "process_tree"} <= set(result["containment"]["unenforced_required"])
|
||||
|
||||
|
||||
def test_process_groups_and_taskkill_do_not_claim_tree_containment(workspace):
|
||||
spec = containment.agent_spec(str(workspace), dict(os.environ), 5)
|
||||
assert containment.PROCESS_TREE not in containment._posix_group_provides(spec)
|
||||
assert containment.PROCESS_TREE not in containment._windows_provides(spec)
|
||||
|
||||
|
||||
async def test_fully_overclaimed_group_grant_cannot_spawn(workspace, monkeypatch):
|
||||
spec = containment.agent_spec(str(workspace), {}, 5)
|
||||
forged = containment.ContainmentGrant(
|
||||
id="forged-all", mechanism="process_group", workspace=str(workspace),
|
||||
enforced=containment.DEFAULT_REQUIRED, degraded=(), unenforced_required=(),
|
||||
owner="test", mode=containment.MODE_ENFORCING, spec=spec,
|
||||
)
|
||||
async def forbidden(*args, **kwargs):
|
||||
pytest.fail("overclaimed grant reached a host spawn")
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", forbidden)
|
||||
with pytest.raises(containment.ContainmentUnavailable):
|
||||
await containment.run(forged, "echo forbidden")
|
||||
|
||||
|
||||
def test_home_interpreter_is_bound_read_only(workspace, monkeypatch):
|
||||
monkeypatch.setattr(sys, "prefix", "/home/test/venv")
|
||||
spec = subprocess_tools._owned_spec(str(workspace), {}, 5)
|
||||
assert "/home/test/venv" in spec.readonly_extra
|
||||
argv = containment._bwrap_prefix(spec)
|
||||
index = argv.index("/home/test/venv")
|
||||
assert argv[index - 1] == "--ro-bind"
|
||||
assert "--unshare-pid" in argv
|
||||
assert "--dev-bind" not in argv
|
||||
assert "--unshare-net" not in argv
|
||||
|
||||
|
||||
async def test_actual_namespace_hides_host_pid_tree_and_sibling(namespaces):
|
||||
sibling = namespaces.parent / "host-secret.txt"
|
||||
sibling.write_text("original")
|
||||
host_namespace = os.readlink("/proc/self/ns/pid")
|
||||
result = await subprocess_tools.PythonTool().execute(
|
||||
"import os\n"
|
||||
"print(os.readlink('/proc/self/ns/pid'))\n"
|
||||
f"print(os.path.exists({str(sibling)!r}))\n"
|
||||
f"try:\n open({str(sibling)!r}, 'w').write('changed')\n"
|
||||
"except OSError:\n pass\n", {},
|
||||
)
|
||||
assert result["exit_code"] == 0, result
|
||||
lines = result["output"].splitlines()
|
||||
assert lines[0] != host_namespace
|
||||
assert lines[1] == "False"
|
||||
assert sibling.read_text() == "original"
|
||||
assert result["containment"]["contained"] is True
|
||||
assert result["teardown"]["dead"] is True
|
||||
|
||||
|
||||
async def test_default_namespace_preserves_loopback_sidecars(namespaces):
|
||||
async def reply(reader, writer):
|
||||
writer.write(b"sidecar\n")
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
server = await asyncio.start_server(reply, "127.0.0.1", 0)
|
||||
async with server:
|
||||
port = server.sockets[0].getsockname()[1]
|
||||
result = await subprocess_tools.PythonTool().execute(
|
||||
f"import socket\ns=socket.create_connection(('127.0.0.1', {port}), timeout=2)\n"
|
||||
"print(s.recv(100).decode().strip())\ns.close()", {},
|
||||
)
|
||||
assert result["output"] == "sidecar", result
|
||||
assert result["containment"]["network"] == "inherit"
|
||||
assert "network" not in result["containment"]["enforced"]
|
||||
|
||||
|
||||
async def test_namespace_handshake_closes_model_stdin(namespaces):
|
||||
result = await subprocess_tools.BashTool().execute(
|
||||
"if read value; then echo unexpected; else echo closed; fi", {},
|
||||
)
|
||||
assert result["output"] == "closed", result
|
||||
assert result["teardown"]["dead"] is True
|
||||
|
||||
|
||||
async def test_partial_initialization_reaps_before_model_code_starts(namespaces, monkeypatch):
|
||||
from src.agent_runtime import journal
|
||||
effect = namespaces / "must-not-exist"
|
||||
def fail(*args, **kwargs):
|
||||
raise RuntimeError("initialization failed")
|
||||
monkeypatch.setattr(journal, "mark_operation_started", fail)
|
||||
result = await subprocess_tools.PythonTool().execute(
|
||||
f"open({str(effect)!r}, 'w').write('effect')", {},
|
||||
)
|
||||
assert result["exit_code"] == 1
|
||||
assert result["containment"]["executed"] is False
|
||||
assert result["containment"]["contained"] is False
|
||||
assert not effect.exists()
|
||||
assert containment.active_grants() == []
|
||||
|
||||
|
||||
async def test_explicit_network_isolation_is_established_or_refused(namespaces):
|
||||
spec = replace(subprocess_tools._owned_spec(str(namespaces), dict(os.environ), 5),
|
||||
network=containment.NETWORK_NONE,
|
||||
required=containment.DEFAULT_REQUIRED | {containment.NETWORK})
|
||||
host_namespace = os.readlink("/proc/self/ns/net")
|
||||
grant = containment.acquire(spec, owner="network-hook")
|
||||
try:
|
||||
result = await containment.run(grant, [sys.executable, "-c",
|
||||
"import os; print(os.readlink('/proc/self/ns/net'))"], argv=True)
|
||||
except containment.ContainmentUnavailable:
|
||||
assert containment.active_grants() == []
|
||||
else:
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() != host_namespace
|
||||
assert containment.NETWORK in result.grant.enforced
|
||||
assert result.release.dead
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exit_parent", [False, True])
|
||||
async def test_setsid_daemon_cannot_survive_namespace_death(namespaces, exit_parent):
|
||||
heartbeat = namespaces / "heartbeat"
|
||||
code = (
|
||||
"import os,signal,time\n"
|
||||
"pid=os.fork()\n"
|
||||
"if pid:\n"
|
||||
+ (" time.sleep(.2); os._exit(0)\n" if exit_parent else " time.sleep(60); os._exit(0)\n")
|
||||
+ "os.setsid()\nsignal.signal(signal.SIGTERM, signal.SIG_IGN)\n"
|
||||
"while True:\n"
|
||||
f" open({str(heartbeat)!r}, 'w').write(str(time.monotonic_ns()))\n"
|
||||
" time.sleep(.01)\n"
|
||||
)
|
||||
spec = subprocess_tools._owned_spec(str(namespaces), dict(os.environ), 1)
|
||||
result = await containment.run(containment.acquire(spec, owner="setsid"),
|
||||
[sys.executable, "-c", code], argv=True)
|
||||
assert heartbeat.exists(), result.stderr
|
||||
assert result.timed_out is (not exit_parent)
|
||||
assert result.release.dead is True
|
||||
last = heartbeat.read_text()
|
||||
await asyncio.sleep(.15)
|
||||
assert heartbeat.read_text() == last
|
||||
assert containment.active_grants() == []
|
||||
|
||||
|
||||
async def test_failed_namespace_initialization_does_not_claim_containment(workspace, monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
monkeypatch.setattr(containment, "MECHANISMS", (containment.Mechanism(
|
||||
"bubblewrap", 30, lambda: True, lambda spec: containment.DEFAULT_REQUIRED,
|
||||
),))
|
||||
async def fail(*args, **kwargs):
|
||||
stdout, stderr = asyncio.StreamReader(), asyncio.StreamReader()
|
||||
stdout.feed_eof()
|
||||
stderr.feed_data(b"bwrap: bind failed\n")
|
||||
stderr.feed_eof()
|
||||
async def wait():
|
||||
return 1
|
||||
return SimpleNamespace(pid=99999999, stdout=stdout, stderr=stderr, returncode=1, wait=wait)
|
||||
async def release(grant, proc, **kwargs):
|
||||
outcome = containment.ReleaseOutcome(dead=True, escalated=False)
|
||||
containment._finish_release(grant, outcome)
|
||||
return outcome
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", fail)
|
||||
monkeypatch.setattr(containment, "_release_awaited", release)
|
||||
result = await subprocess_tools.PythonTool().execute("print('never')", {})
|
||||
assert "containment unavailable" in result["error"]
|
||||
assert result["containment"]["executed"] is False
|
||||
assert result["containment"]["enforced"] == []
|
||||
assert containment.active_grants() == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="real POSIX process")
|
||||
async def test_cancellation_during_spawn_recovers_and_reaps_handle(workspace, monkeypatch):
|
||||
monkeypatch.setattr(containment, "MECHANISMS", tuple(
|
||||
item for item in containment.MECHANISMS if item.name == "process_group"
|
||||
))
|
||||
real_spawn = asyncio.create_subprocess_exec
|
||||
spawned, return_handle = asyncio.Event(), asyncio.Event()
|
||||
children = []
|
||||
async def delayed_spawn(*args, **kwargs):
|
||||
proc = await real_spawn(*args, **kwargs)
|
||||
children.append(proc)
|
||||
spawned.set()
|
||||
await return_handle.wait()
|
||||
return proc
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", delayed_spawn)
|
||||
spec = containment.ContainmentSpec(str(workspace), dict(os.environ), 10,
|
||||
required={containment.WALL_CLOCK})
|
||||
task = asyncio.create_task(containment.run(containment.acquire(spec, owner="spawn-cancel"),
|
||||
[sys.executable, "-c", "import time; time.sleep(60)"], argv=True))
|
||||
await asyncio.wait_for(spawned.wait(), 3)
|
||||
task.cancel()
|
||||
await asyncio.sleep(.01)
|
||||
task.cancel()
|
||||
return_handle.set()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await asyncio.wait_for(task, 8)
|
||||
assert children[0].returncode is not None
|
||||
assert containment.active_grants() == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="real POSIX process")
|
||||
async def test_repeated_cancellation_cannot_interrupt_kill_escalation(workspace, monkeypatch):
|
||||
monkeypatch.setattr(containment, "MECHANISMS", tuple(
|
||||
item for item in containment.MECHANISMS if item.name == "process_group"
|
||||
))
|
||||
ready = workspace / "ready"
|
||||
spec = containment.ContainmentSpec(str(workspace), dict(os.environ), 10,
|
||||
required={containment.WALL_CLOCK})
|
||||
task = asyncio.create_task(containment.run(containment.acquire(spec, owner="cancel"),
|
||||
[sys.executable, "-c", "import signal,time; signal.signal(signal.SIGTERM,signal.SIG_IGN); "
|
||||
f"open({str(ready)!r},'w').write('ready'); time.sleep(60)"], argv=True))
|
||||
for _ in range(100):
|
||||
if ready.exists():
|
||||
break
|
||||
await asyncio.sleep(.02)
|
||||
assert ready.exists()
|
||||
task.cancel()
|
||||
await asyncio.sleep(.1)
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await asyncio.wait_for(task, 8)
|
||||
assert containment.active_grants() == []
|
||||
@@ -56,12 +56,12 @@ def workspace(tmp_path):
|
||||
|
||||
|
||||
def tree_spec(workspace, **kwargs):
|
||||
"""A spec requiring exactly what a process group can give."""
|
||||
"""Exercise group teardown without claiming prevention of session escape."""
|
||||
kwargs.setdefault("env", {"PATH": "/usr/bin:/bin:/usr/sbin:/sbin"})
|
||||
kwargs.setdefault("wall_clock_s", 1)
|
||||
return containment.ContainmentSpec(
|
||||
workspace=workspace,
|
||||
required=frozenset({containment.PROCESS_TREE, containment.WALL_CLOCK}),
|
||||
required=frozenset({containment.WALL_CLOCK}),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -312,7 +312,7 @@ async def test_a_process_count_limit_is_applied_to_the_child(workspace):
|
||||
env={"PATH": "/usr/bin:/bin"},
|
||||
wall_clock_s=10,
|
||||
required=frozenset({
|
||||
containment.PROCESS_TREE, containment.WALL_CLOCK, containment.PROCESS_COUNT,
|
||||
containment.WALL_CLOCK, containment.PROCESS_COUNT,
|
||||
}),
|
||||
max_processes=64,
|
||||
)
|
||||
@@ -341,7 +341,7 @@ async def test_a_memory_limit_is_claimed_only_where_it_can_be_applied(workspace)
|
||||
workspace=workspace,
|
||||
env={"PATH": "/usr/bin:/bin"},
|
||||
wall_clock_s=10,
|
||||
required=frozenset({containment.PROCESS_TREE, containment.WALL_CLOCK}),
|
||||
required=frozenset({containment.WALL_CLOCK}),
|
||||
max_memory_bytes=limit,
|
||||
)
|
||||
grant = containment.acquire(spec, owner="session-9")
|
||||
@@ -369,7 +369,7 @@ def test_an_unenforceable_required_limit_refuses_instead_of_crashing_the_spawn(w
|
||||
env={"PATH": "/usr/bin:/bin"},
|
||||
wall_clock_s=10,
|
||||
required=frozenset({
|
||||
containment.PROCESS_TREE, containment.WALL_CLOCK, containment.MEMORY,
|
||||
containment.WALL_CLOCK, containment.MEMORY,
|
||||
}),
|
||||
max_memory_bytes=2 * 1024 * 1024 * 1024,
|
||||
)
|
||||
|
||||
@@ -53,13 +53,16 @@ def _argv(workspace, **kwargs):
|
||||
import shutil as _shutil
|
||||
|
||||
original = _shutil.which
|
||||
original_available = containment._bwrap_available
|
||||
try:
|
||||
containment._bwrap_available = lambda: True
|
||||
_shutil.which = lambda name, *a, **kw: (
|
||||
"/usr/bin/bwrap" if name == "bwrap" else original(name, *a, **kw)
|
||||
)
|
||||
wrapped = subprocess_tools._wrap_workspace_namespace("true", workspace, **kwargs)
|
||||
finally:
|
||||
_shutil.which = original
|
||||
containment._bwrap_available = original_available
|
||||
assert wrapped is not None, "forced bwrap should produce a namespace argv"
|
||||
return shlex.split(wrapped)
|
||||
|
||||
@@ -174,6 +177,7 @@ def test_fallback_reports_that_filesystem_containment_did_not_hold(
|
||||
def test_the_fallback_mechanism_is_not_named_like_a_mechanism(workspace, monkeypatch):
|
||||
"""A string rewrite reported as "bubblewrap" or "none" is the same silence
|
||||
with extra steps. It gets its own name so a reader cannot mistake it."""
|
||||
monkeypatch.setattr(containment, "CONTAINMENT_MODE", containment.MODE_REPORT_ONLY)
|
||||
monkeypatch.setattr(
|
||||
subprocess_tools, "_wrap_workspace_namespace",
|
||||
lambda *args, **kwargs: None,
|
||||
|
||||
@@ -29,8 +29,8 @@ async def test_native_bash_owns_and_releases_its_child(native_boundary):
|
||||
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"]["enforced"] == ["wall_clock"]
|
||||
assert result["containment"]["unenforced_required"] == ["filesystem", "process_tree"]
|
||||
assert result["containment"]["network"] == "inherit"
|
||||
assert containment.active_grants() == []
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_native_bash_refuses_before_spawn_when_required_boundary_missing(m
|
||||
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"]
|
||||
assert result["containment"]["unenforced_required"] == ["filesystem", "process_tree"]
|
||||
|
||||
|
||||
async def test_failed_spawn_releases_unstarted_grant(native_boundary, monkeypatch):
|
||||
|
||||
@@ -217,11 +217,17 @@ def test_the_procfs_token_reads_a_comm_containing_spaces_and_parens(monkeypatch,
|
||||
# each value equals its own field number.
|
||||
fields = " ".join(str(index) for index in range(4, 54))
|
||||
(procfs / "77" / "stat").write_text(f"77 (my (weird) prog) S {fields}\n")
|
||||
boot_path = procfs / "sys/kernel/random/boot_id"
|
||||
boot_path.parent.mkdir(parents=True)
|
||||
boot_path.write_text("first-boot\n")
|
||||
monkeypatch.setattr(platform_compat, "PROC_ROOT", procfs)
|
||||
monkeypatch.setattr(po, "PROC_ROOT", procfs)
|
||||
monkeypatch.setattr(po, "IS_WINDOWS", False)
|
||||
|
||||
assert po.start_token(77) == "procfs:22"
|
||||
assert po.start_token(77) == "procfs:first-boot:22"
|
||||
token = po.start_token(77)
|
||||
boot_path.write_text("second-boot\n")
|
||||
assert po.start_token(77) != token
|
||||
|
||||
|
||||
# ── The process tree ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2198,8 +2198,9 @@ def test_python_loaded_code_sees_virtual_workspace_alias(monkeypatch, tmp_path):
|
||||
import venv
|
||||
from types import SimpleNamespace
|
||||
|
||||
if not shutil.which("bwrap"):
|
||||
return
|
||||
from src import containment
|
||||
if not containment._bwrap_available():
|
||||
pytest.skip("functional bubblewrap namespaces unavailable")
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -2238,6 +2239,7 @@ def test_workspace_namespace_mounts_only_a_nested_python_environment(monkeypatch
|
||||
from src.agent_tools import subprocess_tools
|
||||
|
||||
monkeypatch.setattr(subprocess_tools.shutil, "which", lambda name: "/usr/bin/bwrap")
|
||||
monkeypatch.setattr(subprocess_tools.containment, "_bwrap_available", lambda: True)
|
||||
environment = tmp_path / "nested" / "venv"
|
||||
environment.mkdir(parents=True)
|
||||
(environment / "pyvenv.cfg").write_text("home = /usr/bin\n")
|
||||
@@ -2259,6 +2261,7 @@ def test_workspace_namespace_rejects_broad_or_symlinked_python_prefixes(monkeypa
|
||||
from src.agent_tools import subprocess_tools
|
||||
|
||||
monkeypatch.setattr(subprocess_tools.shutil, "which", lambda name: "/usr/bin/bwrap")
|
||||
monkeypatch.setattr(subprocess_tools.containment, "_bwrap_available", lambda: True)
|
||||
linked_root = tmp_path / "linked-root"
|
||||
linked_root.symlink_to("/", target_is_directory=True)
|
||||
# Compared against the argv with no interpreter prefix at all: an unsafe
|
||||
@@ -2290,6 +2293,7 @@ def test_workspace_namespace_preserves_the_64_bit_dynamic_loader(monkeypatch):
|
||||
from src.agent_tools import subprocess_tools
|
||||
|
||||
monkeypatch.setattr(subprocess_tools.shutil, "which", lambda name: "/usr/bin/bwrap")
|
||||
monkeypatch.setattr(subprocess_tools.containment, "_bwrap_available", lambda: True)
|
||||
command = subprocess_tools._wrap_workspace_namespace("echo ok", "/tmp/workspace")
|
||||
assert command is not None
|
||||
args = shlex.split(command)
|
||||
|
||||
Reference in New Issue
Block a user