mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-12 18:02:20 +02:00
fix(runtime): resolve Wave 3-S audit findings (P2-1, P2-2, P2-3)
- P2-1: reject non-process PID values (None, 0, negative integers) in pid_alive without invoking underlying process probe. - P2-2: truthfully represent production external bridge executions as uncontained, external, non-authoritative grants carrying sanitized endpoint metadata. - P2-3: reject writable_extra overlay bindings over protected system roots and their descendants while preserving legitimate scratch destinations.
This commit is contained in:
@@ -505,3 +505,40 @@ async def test_an_external_bridge_grant_claims_nothing_and_cannot_be_run_locally
|
||||
with pytest.raises(ValueError, match="does not own"):
|
||||
await containment.run(grant, "echo hello")
|
||||
assert no_spawn == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protected_dest", [
|
||||
"/etc",
|
||||
"/etc/ssl",
|
||||
"/usr",
|
||||
"/usr/local",
|
||||
"/bin",
|
||||
"/bin/sh",
|
||||
"/sbin",
|
||||
"/lib",
|
||||
"/lib64",
|
||||
"/proc",
|
||||
"/proc/sys",
|
||||
"/dev",
|
||||
"/dev/shm",
|
||||
"/sys",
|
||||
"/root",
|
||||
"/root/.ssh",
|
||||
"/home",
|
||||
"/workspace",
|
||||
"/workspace/sub",
|
||||
])
|
||||
def test_writable_extra_rejects_protected_system_roots_and_descendants(monkeypatch, workspace, protected_dest):
|
||||
install(monkeypatch, mechanism("fake", 10, containment.DIMENSIONS))
|
||||
spec = spec_for(workspace, writable_extra=(protected_dest,))
|
||||
with pytest.raises(ValueError, match="reserved path"):
|
||||
containment.acquire(spec, owner="session-1")
|
||||
|
||||
|
||||
def test_writable_extra_accepts_legitimate_scratch_destinations(monkeypatch, workspace):
|
||||
install(monkeypatch, mechanism("fake", 10, containment.DIMENSIONS))
|
||||
spec = spec_for(workspace, writable_extra=("/var/scratch", "/tmp/custom_scratch", "/home/testuser/scratch"))
|
||||
grant = containment.acquire(spec, owner="session-1")
|
||||
assert "/var/scratch" in grant.spec.writable_extra
|
||||
assert "/tmp/custom_scratch" in grant.spec.writable_extra
|
||||
assert "/home/testuser/scratch" in grant.spec.writable_extra
|
||||
|
||||
@@ -376,3 +376,55 @@ def test_an_unenforceable_required_limit_refuses_instead_of_crashing_the_spawn(w
|
||||
with pytest.raises(containment.ContainmentUnavailable) as caught:
|
||||
containment.acquire(spec, owner="session-9")
|
||||
assert caught.value.missing == frozenset({containment.MEMORY})
|
||||
|
||||
|
||||
def test_pid_alive_rejects_non_process_pids(monkeypatch):
|
||||
calls = []
|
||||
real_kill = os.kill
|
||||
|
||||
def fake_kill(pid, sig):
|
||||
calls.append((pid, sig))
|
||||
return real_kill(pid, sig)
|
||||
|
||||
monkeypatch.setattr(os, "kill", fake_kill)
|
||||
|
||||
# Required: None, 0, and any negative integers must return False
|
||||
assert pid_alive(None) is False
|
||||
assert pid_alive(0) is False
|
||||
assert pid_alive(-1) is False
|
||||
assert pid_alive(-42) is False
|
||||
assert pid_alive(-9999) is False
|
||||
assert pid_alive("not_a_pid") is False
|
||||
|
||||
# The underlying process probe (os.kill) must NEVER be invoked for pid <= 0
|
||||
assert calls == []
|
||||
|
||||
# Normal positive-PID behavior remains covered
|
||||
my_pid = os.getpid()
|
||||
assert pid_alive(my_pid) is True
|
||||
assert (my_pid, 0) in calls
|
||||
|
||||
|
||||
def test_pid_alive_retains_conservative_liveness_on_eperm(monkeypatch):
|
||||
def fake_kill(pid, sig):
|
||||
raise PermissionError(1, "Operation not permitted")
|
||||
|
||||
monkeypatch.setattr(os, "kill", fake_kill)
|
||||
|
||||
# Positive PID with EPERM is conservatively considered alive
|
||||
assert pid_alive(1) is True
|
||||
assert pid_alive(99999) is True
|
||||
|
||||
# But non-process values still immediately return False without calling probe
|
||||
assert pid_alive(None) is False
|
||||
assert pid_alive(0) is False
|
||||
assert pid_alive(-1) is False
|
||||
assert pid_alive(-100) is False
|
||||
|
||||
|
||||
def test_pid_alive_reports_false_on_process_lookup_error(monkeypatch):
|
||||
def fake_kill(pid, sig):
|
||||
raise ProcessLookupError(3, "No such process")
|
||||
|
||||
monkeypatch.setattr(os, "kill", fake_kill)
|
||||
assert pid_alive(99999999) is False
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Tests verifying truthful representation of production external bridge execution.
|
||||
|
||||
P2-2 invariant: external bridge execution != local containment.
|
||||
"""
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src import containment
|
||||
from src.agent_tools import subprocess_tools
|
||||
from src import tool_execution as _te
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, data, status_code=200):
|
||||
self._data = data
|
||||
self.status_code = status_code
|
||||
self.text = "error detail" if status_code >= 400 else ""
|
||||
|
||||
def json(self):
|
||||
return self._data
|
||||
|
||||
|
||||
class _FakeAsyncClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return None
|
||||
|
||||
async def post(self, url, *args, **kwargs):
|
||||
return _FakeResponse({"stdout": "remote stdout", "stderr": "", "exit_code": 0})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_store(tmp_path, monkeypatch):
|
||||
store = tmp_path / "containment_grants.json"
|
||||
monkeypatch.setattr(containment, "_store_path", lambda: store)
|
||||
return store
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_shell_creates_uncontained_external_record(monkeypatch):
|
||||
"""1. Production bridge execution creates an external/uncontained record.
|
||||
2. It cannot be interpreted as contained."""
|
||||
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", _FakeAsyncClient)
|
||||
|
||||
tool = subprocess_tools.HostShellTool()
|
||||
ctx = {
|
||||
"client_runtime_context": {
|
||||
"host_shell_bridge": {
|
||||
"url": "http://127.0.0.1:17654/run",
|
||||
"token": "secret-bridge-token",
|
||||
}
|
||||
},
|
||||
"session_id": "test-session-host-shell",
|
||||
}
|
||||
result = await tool.execute('{"command": "echo host"}', ctx)
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert result["output"] == "remote stdout"
|
||||
assert "containment" in result
|
||||
c = result["containment"]
|
||||
|
||||
# Invariant: external bridge execution != local containment
|
||||
assert c["external"] is True
|
||||
assert c["contained"] is False
|
||||
assert c["mechanism"] == "external_bridge"
|
||||
assert c["enforced"] == []
|
||||
assert c["executed"] is True
|
||||
|
||||
# Server-owned metadata identifies endpoint without secrets
|
||||
assert c.get("endpoint") == "http://127.0.0.1:17654/run"
|
||||
assert "secret-bridge-token" not in str(c)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_shell_failure_does_not_become_containment_or_effect_evidence(monkeypatch):
|
||||
"""5. Bridge failure does not become successful containment/effect evidence."""
|
||||
class _FailingClient(_FakeAsyncClient):
|
||||
async def post(self, url, *args, **kwargs):
|
||||
return _FakeResponse({"error": "bridge exploded"}, status_code=500)
|
||||
|
||||
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", _FailingClient)
|
||||
|
||||
tool = subprocess_tools.HostShellTool()
|
||||
ctx = {
|
||||
"client_runtime_context": {
|
||||
"host_shell_bridge": {
|
||||
"url": "http://127.0.0.1:17654/run",
|
||||
"token": "secret-bridge-token",
|
||||
}
|
||||
},
|
||||
"session_id": "test-session-host-shell-fail",
|
||||
}
|
||||
result = await tool.execute('{"command": "echo fail"}', ctx)
|
||||
|
||||
assert result["exit_code"] == 1
|
||||
assert "bridge returned HTTP 500" in result["error"]
|
||||
assert "containment" in result
|
||||
c = result["containment"]
|
||||
|
||||
assert c["external"] is True
|
||||
assert c["contained"] is False
|
||||
assert c["enforced"] == []
|
||||
# Failure means not executed
|
||||
assert c["executed"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_routed_bash_and_python_via_bridge_creates_uncontained_external_record():
|
||||
"""Prove _route_tool_via_bridge generates truthful external records for bash & python."""
|
||||
bridge_ctx = {
|
||||
"surface": "odysseus-tui",
|
||||
"host_shell_bridge": {
|
||||
"url": "http://127.0.0.1:17654/run",
|
||||
"token": "bridge-token",
|
||||
},
|
||||
}
|
||||
|
||||
async def fake_bridge_post(bridge, path, payload, **kwargs):
|
||||
return {"stdout": "bridge out", "stderr": "", "exit_code": 0}
|
||||
|
||||
from tests.runtime_evidence_helpers import server_authorized_executor
|
||||
|
||||
with patch.object(_te, "_bridge_post", fake_bridge_post), \
|
||||
patch.object(_te, "_owner_is_admin", lambda owner: True):
|
||||
# Routed bash
|
||||
desc, result = await server_authorized_executor(_te.execute_tool_block)(
|
||||
SimpleNamespace(tool_type="bash", content="ls -la"),
|
||||
session_id="session-routed-bash",
|
||||
client_runtime_context=bridge_ctx,
|
||||
security_context=_te.NO_TOOL_SECURITY_CONTEXT,
|
||||
)
|
||||
assert result["exit_code"] == 0
|
||||
assert "containment" in result
|
||||
cb = result["containment"]
|
||||
assert cb["external"] is True
|
||||
assert cb["contained"] is False
|
||||
assert cb["mechanism"] == "external_bridge"
|
||||
assert cb["enforced"] == []
|
||||
assert cb["executed"] is True
|
||||
assert cb.get("endpoint") == "http://127.0.0.1:17654/run"
|
||||
|
||||
# Routed python
|
||||
desc_py, result_py = await server_authorized_executor(_te.execute_tool_block)(
|
||||
SimpleNamespace(tool_type="python", content="print('hi')"),
|
||||
session_id="session-routed-py",
|
||||
client_runtime_context=bridge_ctx,
|
||||
security_context=_te.NO_TOOL_SECURITY_CONTEXT,
|
||||
)
|
||||
assert result_py["exit_code"] == 0
|
||||
assert "containment" in result_py
|
||||
cp = result_py["containment"]
|
||||
assert cp["external"] is True
|
||||
assert cp["contained"] is False
|
||||
assert cp["mechanism"] == "external_bridge"
|
||||
assert cp["enforced"] == []
|
||||
assert cp["executed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_external_record_does_not_grant_authority(tmp_path):
|
||||
"""3. The record does not grant execution authority."""
|
||||
spec = containment.agent_spec(str(tmp_path), {}, 5)
|
||||
grant = containment.declare_external_bridge(
|
||||
spec, owner="auth-test-session", endpoint="http://127.0.0.1:17654/run"
|
||||
)
|
||||
|
||||
assert grant.external is True
|
||||
assert grant.contained is False
|
||||
|
||||
# Attempting to use this grant to run local command must be rejected
|
||||
with pytest.raises(ValueError, match="backend does not own"):
|
||||
await containment.run(grant, "id")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_native_local_bash_python_behavior_unchanged(tmp_path, monkeypatch):
|
||||
"""4. Native local Bash/Python behavior is unchanged."""
|
||||
tool_bash = subprocess_tools.BashTool()
|
||||
ctx = {
|
||||
"session_id": "native-session",
|
||||
}
|
||||
result = await tool_bash.execute("echo 'native run'", ctx)
|
||||
assert result["exit_code"] == 0
|
||||
assert "native run" in result["output"]
|
||||
assert "containment" in result
|
||||
c = result["containment"]
|
||||
assert c["external"] is False
|
||||
assert c["mechanism"] in ("bubblewrap", "process_group")
|
||||
assert c["executed"] is True
|
||||
Reference in New Issue
Block a user