diff --git a/src/agents/sandbox/sandboxes/unix_local.py b/src/agents/sandbox/sandboxes/unix_local.py index b20c6edb78..d69d3b0096 100644 --- a/src/agents/sandbox/sandboxes/unix_local.py +++ b/src/agents/sandbox/sandboxes/unix_local.py @@ -988,6 +988,8 @@ def _literal(path: Path | str) -> str: [ "(version 1)", "(allow default)", + # Launch Services can start applications outside this process's sandbox. + "(deny lsopen)", deny_rules, *allow_rules, ] diff --git a/tests/sandbox/test_runtime.py b/tests/sandbox/test_runtime.py index d0bd145a4b..ced237680e 100644 --- a/tests/sandbox/test_runtime.py +++ b/tests/sandbox/test_runtime.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import ctypes import io import json import logging @@ -5122,6 +5123,38 @@ async def test_unix_local_exec_confines_commands_to_workspace_root() -> None: shutil.rmtree(workspace_root, ignore_errors=True) +@pytest.mark.asyncio +@pytest.mark.requires_native_macos_sandbox +@pytest.mark.skipif( + sys.platform != "darwin" or shutil.which("sandbox-exec") is None, + reason="sandbox-exec is only available on macOS when installed", +) +async def test_unix_local_exec_denies_launch_services(tmp_path: Path) -> None: + # Query the OS decision without depending on a GUI session or launching a host app. + sandbox_check = ctypes.CDLL("/usr/lib/libsandbox.1.dylib").sandbox_check + sandbox_check.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] + sandbox_check.restype = ctypes.c_int + assert sandbox_check(os.getpid(), b"lsopen", 0) == 0 + + check_launch_services = """ +import ctypes +import os + +sandbox_check = ctypes.CDLL("/usr/lib/libsandbox.1.dylib").sandbox_check +sandbox_check.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] +sandbox_check.restype = ctypes.c_int +print(sandbox_check(os.getpid(), b"lsopen", 0)) +""" + client = UnixLocalSandboxClient(inherit_host_environment=False) + async with await client.create(manifest=Manifest(root=str(tmp_path / "workspace"))) as session: + result = await session.exec( + sys.executable, "-I", "-c", check_launch_services, shell=False, timeout=10 + ) + + assert result.ok(), result.stderr.decode("utf-8", errors="replace") + assert int(result.stdout.strip()) > 0 + + @pytest.mark.asyncio async def test_unix_local_exec_rejects_when_confinement_is_unavailable( monkeypatch: pytest.MonkeyPatch, @@ -5265,6 +5298,7 @@ def _fake_which(name: str, path: str | None = None) -> str | None: profile = command[2] assert command[:2] == ["/usr/bin/sandbox-exec", "-p"] + assert "(deny lsopen)" in profile assert '(allow file-read-data file-read-metadata (subpath "/opt/homebrew"))' in profile assert '(allow file-read-data file-read-metadata (subpath "/usr/local"))' in profile assert (