fix: execute local reins through the sandbox owner
Some checks failed
ci / validate (push) Has been cancelled
Some checks failed
ci / validate (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
parent
92392f75f1
commit
63a7f9f160
14 changed files with 463 additions and 81 deletions
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sandboxer.models import Reachability, SandboxState, SandboxStatus
|
||||
|
||||
from glas_harness.contract import (
|
||||
|
|
@ -14,6 +16,7 @@ from glas_harness.contract import (
|
|||
)
|
||||
from glas_harness.gateway import run_execution, run_task_through_rein
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from glas_harness.transport import transport_from_sandbox
|
||||
|
||||
|
||||
PROFILE = "harness.agent-dev-local@1.0.0"
|
||||
|
|
@ -127,6 +130,81 @@ def test_run_execution_creates_and_destroys_sandbox() -> None:
|
|||
assert result.tool_output == "sensitive direct output"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("request_id", ["exact-request", None])
|
||||
def test_owner_commands_preserve_create_identity_routes_and_timeout(request_id):
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
manager.execute.return_value = MagicMock(
|
||||
timed_out=False, output_truncated=False, exit_code=0, stdout="", stderr=""
|
||||
)
|
||||
catalog = _catalog_with_readiness()
|
||||
profile, _ = catalog.resolve(PROFILE)
|
||||
refs = ["proof-route-a", "proof-route-b"]
|
||||
catalog.profiles()[(profile.id, profile.version)] = profile.model_copy(update={
|
||||
"credential_route_refs": refs,
|
||||
"limits": profile.limits.model_copy(update={"timeout_seconds": 7}),
|
||||
})
|
||||
|
||||
class OwnerRein(_FakeRein):
|
||||
def start_session(self, profile, inputs, sandbox):
|
||||
assert "_owner_execute" not in sandbox.model_dump()
|
||||
transport = transport_from_sandbox(sandbox)
|
||||
transport.run(["printf", "%s", "non-secret"], timeout=30)
|
||||
path = transport.write_task_file({"title": "private prompt"})
|
||||
transport.remove_file(path)
|
||||
return super().start_session(profile, inputs, sandbox)
|
||||
|
||||
request = _request().model_copy(update={
|
||||
"request_id": request_id, "actor": "atm", "project": "exact-project"
|
||||
})
|
||||
result = run_execution(request, catalog=catalog, rein=OwnerRein(), manager=manager)
|
||||
assert result.ok
|
||||
create_consumer = manager.create.call_args.args[0].consumer
|
||||
assert create_consumer.actor.value == "atm"
|
||||
assert create_consumer.project == "exact-project"
|
||||
assert create_consumer.run_id == result.evidence.request_id
|
||||
assert create_consumer.session_id is None
|
||||
assert manager.execute.call_count == 3
|
||||
for call in manager.execute.call_args_list:
|
||||
sandbox_id, command = call.args
|
||||
assert sandbox_id == "sbx1"
|
||||
assert command.consumer == create_consumer
|
||||
assert command.credential_route_refs == refs
|
||||
assert command.timeout_seconds == 7
|
||||
writer = manager.execute.call_args_list[1].args[1]
|
||||
assert writer.stdin_text == '{"title": "private prompt"}'
|
||||
assert "private prompt" not in str(writer.command)
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", ["refused", "timeout", "truncated", "write"])
|
||||
def test_owner_session_failure_still_destroys_sandbox(failure):
|
||||
from glas_harness.reins.rein_aharness import ReinAharness
|
||||
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
good = MagicMock(timed_out=False, output_truncated=False, exit_code=0,
|
||||
stdout="head-before", stderr="")
|
||||
if failure == "refused":
|
||||
manager.execute.side_effect = PermissionError("owner denied execution")
|
||||
elif failure == "write":
|
||||
manager.execute.side_effect = [good, MagicMock(
|
||||
timed_out=False, output_truncated=False, exit_code=1,
|
||||
stdout="", stderr="task write refused",
|
||||
)]
|
||||
else:
|
||||
manager.execute.return_value = MagicMock(
|
||||
timed_out=failure == "timeout", output_truncated=failure == "truncated"
|
||||
)
|
||||
with patch("glas_harness.transport.subprocess.run") as host_run:
|
||||
result = run_execution(_request(), catalog=_catalog_with_readiness(),
|
||||
rein=ReinAharness(), manager=manager)
|
||||
assert not result.ok
|
||||
assert result.evidence.failure_stage == "session_start"
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
host_run.assert_not_called()
|
||||
|
||||
|
||||
def test_run_execution_normalizes_execution_failure_and_tears_down() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
host="localhost",
|
||||
reachability={"pid": str(os.getpid()), "workspace_dir": str(repo)},
|
||||
)
|
||||
sandbox._owner_execute = MagicMock(
|
||||
return_value=MagicMock(timed_out=False, output_truncated=False, exit_code=0,
|
||||
stdout="", stderr="")
|
||||
)
|
||||
with patch.object(ExecutionTransport, "git_head", return_value="abc123"):
|
||||
session = rein.start_session(
|
||||
profile=ProfileCatalog().resolve("harness.agent-dev-local@1.0.0")[0],
|
||||
|
|
@ -55,11 +59,11 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
assert session["target_repo"] == str(repo)
|
||||
assert session["head_before"] == "abc123"
|
||||
assert session["transport"].workspace == str(repo)
|
||||
task_spec = json.loads(open(session["task_file"]).read())
|
||||
task_spec = json.loads(sandbox._owner_execute.call_args.args[1])
|
||||
assert task_spec["title"] == "t"
|
||||
assert task_spec["target_repo"] == str(repo)
|
||||
rein.cleanup_session(session)
|
||||
assert not os.path.exists(session["task_file"])
|
||||
assert sandbox._owner_execute.call_args.args[0] == ["rm", "-f", "--", session["task_file"]]
|
||||
|
||||
|
||||
def test_start_session_requires_resolvable_target_repo() -> None:
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
host="localhost",
|
||||
reachability={"pid": str(os.getpid()), "workspace_dir": str(repo)},
|
||||
)
|
||||
sandbox._owner_execute = MagicMock(
|
||||
return_value=MagicMock(timed_out=False, output_truncated=False, exit_code=0,
|
||||
stdout="", stderr="")
|
||||
)
|
||||
with patch.object(ExecutionTransport, "git_head", return_value="abc123"):
|
||||
session = rein.start_session(
|
||||
profile=ProfileCatalog().resolve("harness.agent-dev-openweights-local@1.0.0")[0],
|
||||
|
|
@ -55,11 +59,11 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
assert session["target_repo"] == str(repo)
|
||||
assert session["head_before"] == "abc123"
|
||||
assert session["transport"].workspace == str(repo)
|
||||
task_spec = json.loads(open(session["task_file"]).read())
|
||||
task_spec = json.loads(sandbox._owner_execute.call_args.args[1])
|
||||
assert task_spec["title"] == "t"
|
||||
assert task_spec["target_repo"] == str(repo)
|
||||
rein.cleanup_session(session)
|
||||
assert not os.path.exists(session["task_file"])
|
||||
assert sandbox._owner_execute.call_args.args[0] == ["rm", "-f", "--", session["task_file"]]
|
||||
|
||||
|
||||
def test_start_session_requires_resolvable_target_repo() -> None:
|
||||
|
|
|
|||
|
|
@ -14,28 +14,18 @@ def test_local_namespace_transport_requires_pid_and_workspace(tmp_path: Path) ->
|
|||
host="localhost",
|
||||
reachability={"pid": "4321", "workspace_dir": str(tmp_path)},
|
||||
)
|
||||
sandbox._owner_execute = MagicMock(return_value=MagicMock(
|
||||
timed_out=False, output_truncated=False, exit_code=0, stdout="ok", stderr=""
|
||||
))
|
||||
transport = transport_from_sandbox(sandbox)
|
||||
|
||||
assert transport.kind == "local_namespace"
|
||||
assert transport.workspace == str(tmp_path)
|
||||
assert transport.command(["git", "status"]) == [
|
||||
"nsenter",
|
||||
"--target",
|
||||
"4321",
|
||||
"--mount",
|
||||
"--pid",
|
||||
"--net",
|
||||
"--uts",
|
||||
"--ipc",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
'cd "$1" && shift && exec "$@"',
|
||||
"sh",
|
||||
str(tmp_path),
|
||||
"git",
|
||||
"status",
|
||||
]
|
||||
with patch("glas_harness.transport.subprocess.run") as run:
|
||||
assert transport.run(["git", "status"], timeout=30).stdout == "ok"
|
||||
run.assert_not_called()
|
||||
sandbox._owner_execute.assert_called_once_with(["git", "status"], None, 30)
|
||||
with pytest.raises(TransportError, match="owner execution"):
|
||||
transport.command(["git", "status"])
|
||||
|
||||
|
||||
def test_remote_transport_wraps_command_without_local_shell() -> None:
|
||||
|
|
@ -100,20 +90,47 @@ def test_incomplete_or_ambiguous_reachability_fails_closed(reachability) -> None
|
|||
)
|
||||
|
||||
|
||||
def test_local_task_file_is_private_and_removable(tmp_path: Path) -> None:
|
||||
(tmp_path / ".git").mkdir()
|
||||
def test_local_task_file_uses_owner_stdin_and_cleanup(tmp_path: Path) -> None:
|
||||
owner = MagicMock(return_value=MagicMock(
|
||||
timed_out=False, output_truncated=False, exit_code=0, stdout="", stderr=""
|
||||
))
|
||||
transport = ExecutionTransport(
|
||||
kind="local_namespace", workspace=str(tmp_path), pid=123
|
||||
kind="local_namespace", workspace=str(tmp_path), pid=123, owner_execute=owner,
|
||||
timeout_seconds=7,
|
||||
)
|
||||
task_path = transport.write_task_file({"title": "bounded", "description": "safe"})
|
||||
path = Path(task_path)
|
||||
|
||||
assert path.parent == tmp_path / ".git"
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
assert json.loads(path.read_text())["title"] == "bounded"
|
||||
|
||||
task_path = transport.write_task_file({"title": "private prompt"})
|
||||
command, stdin, timeout = owner.call_args.args
|
||||
assert "umask 077; set -C; cat" in command[2]
|
||||
assert "private prompt" not in str(command)
|
||||
assert json.loads(stdin) == {"title": "private prompt"}
|
||||
assert timeout == 7
|
||||
assert Path(task_path).parent == tmp_path / ".git"
|
||||
assert not Path(task_path).exists() # No host write, even with a fake owner.
|
||||
transport.remove_file(task_path)
|
||||
assert not path.exists()
|
||||
owner.assert_called_with(["rm", "-f", "--", task_path], None, 7)
|
||||
|
||||
|
||||
def test_local_descriptor_without_owner_fails_closed():
|
||||
with pytest.raises(TransportError, match="owner execution binding"):
|
||||
transport_from_sandbox(SandboxHandle(
|
||||
sandbox_id="sbx", host="localhost",
|
||||
reachability={"pid": "123", "workspace_dir": "/tmp/ws"},
|
||||
))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("timed_out,truncated", [(True, False), (False, True)])
|
||||
def test_owner_timeout_and_truncation_fail_closed(timed_out, truncated):
|
||||
import subprocess
|
||||
owner = MagicMock(return_value=MagicMock(
|
||||
timed_out=timed_out, output_truncated=truncated
|
||||
))
|
||||
transport = ExecutionTransport(
|
||||
kind="local_namespace", workspace="/tmp/ws", owner_execute=owner,
|
||||
timeout_seconds=3,
|
||||
)
|
||||
with pytest.raises(subprocess.TimeoutExpired if timed_out else TransportError):
|
||||
transport.run(["true"], timeout=30)
|
||||
owner.assert_called_once_with(["true"], None, 3)
|
||||
|
||||
|
||||
def test_remote_task_file_uses_ssh_stdin_and_cleanup() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue