From e9f704b21151ad244070fe56dd37d01d7d87c6b7 Mon Sep 17 00:00:00 2001 From: Saiyam Jain Date: Sat, 7 Nov 2026 03:50:59 +0530 Subject: [PATCH 1/3] fix(computer): guard Unix-only pwd import in site_proxy --- .../env/envs/computer/utils/site_proxy.py | 34 +++++++++++++------ tst/unit/env/envs/test_computer_site_proxy.py | 12 +++++-- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/agent_env/env/envs/computer/utils/site_proxy.py b/src/agent_env/env/envs/computer/utils/site_proxy.py index 0c14c9cf0..ba1955778 100644 --- a/src/agent_env/env/envs/computer/utils/site_proxy.py +++ b/src/agent_env/env/envs/computer/utils/site_proxy.py @@ -21,13 +21,16 @@ import asyncio import json import os -import pwd import shutil import socket import ssl import struct import subprocess import sys +try: + import pwd +except ImportError: + pwd = None import time import urllib.error import urllib.request @@ -609,6 +612,8 @@ async def serve(config: Config) -> None: def serve_user(session_user: Optional[str]) -> Optional[str]: """The user ``serve`` drops to: ``SERVE_USER`` where the base has it, else the session user.""" + if pwd is None: + return session_user try: pwd.getpwnam(SERVE_USER) except KeyError: @@ -617,12 +622,15 @@ def serve_user(session_user: Optional[str]) -> Optional[str]: def drop_privileges(user: Optional[str]) -> None: - if user is None or os.geteuid() != 0: + if user is None or pwd is None or not hasattr(os, "geteuid") or os.geteuid() != 0: return entry = pwd.getpwnam(user) - os.setgroups([]) - os.setgid(entry.pw_gid) - os.setuid(entry.pw_uid) + if hasattr(os, "setgroups"): + os.setgroups([]) + if hasattr(os, "setgid"): + os.setgid(entry.pw_gid) + if hasattr(os, "setuid"): + os.setuid(entry.pw_uid) @dataclass(frozen=True) @@ -734,9 +742,12 @@ def trust_system(paths: Paths, ca_pem: str) -> Optional[str]: def trust_nss(user: Optional[str], ca: str) -> list[str]: """Add the CA to the session user's NSS databases, Chromium's (and a snap Chromium's when there is one), as that user, for a Chromium too old to read its ``CACertificates`` policy; none without ``certutil``.""" - if user is None or not shutil.which("certutil"): + if user is None or pwd is None or not shutil.which("certutil"): + return [] + try: + home = Path(pwd.getpwnam(user).pw_dir) + except KeyError: return [] - home = Path(pwd.getpwnam(user).pw_dir) folders = [home / ".pki" / "nssdb"] if (home / "snap" / "chromium").is_dir(): folders.append(home / "snap" / "chromium" / "current" / ".pki" / "nssdb") @@ -754,7 +765,7 @@ def trust_nss(user: Optional[str], ca: str) -> list[str]: def as_user(user: Optional[str], argv: list[str]) -> list[str]: - if user is None or os.geteuid() != 0: + if user is None or not hasattr(os, "geteuid") or os.geteuid() != 0: return argv return ["runuser", "-u", user, "--", *argv] @@ -763,8 +774,9 @@ def install(config_path: str, paths: Paths = Paths()) -> dict: """As root: write the sites' certificates, names and trust, start ``serve`` on what's written, and check each name loads as the session user; what was trusted and each check's result. The config's folder, which arrives root's alone, is opened to the session user (for the CA) only once its keys and the config are private.""" - if os.geteuid() != 0 and paths.root == Path("/"): - raise SystemExit(f"the site proxy installs as root, not uid {os.geteuid()}") + uid = getattr(os, "geteuid", lambda: -1)() + if uid != 0 and paths.root == Path("/"): + raise SystemExit(f"the site proxy installs as root, not uid {uid}") given = json.loads(Path(config_path).read_text()) folder = Path(config_path).parent user = given.get("user") @@ -841,7 +853,7 @@ def start(serve_path: str, log: Optional[str]) -> None: def open_log(log: str) -> int: Path(log).parent.mkdir(parents=True, exist_ok=True) - return os.open(log, os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_NOFOLLOW, 0o600) + return os.open(log, os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600) def keep(serve_path: str) -> None: diff --git a/tst/unit/env/envs/test_computer_site_proxy.py b/tst/unit/env/envs/test_computer_site_proxy.py index ebb500398..049a90f87 100644 --- a/tst/unit/env/envs/test_computer_site_proxy.py +++ b/tst/unit/env/envs/test_computer_site_proxy.py @@ -678,19 +678,27 @@ def test_a_wildcard_name_starts_dns_even_when_resolv_conf_names_no_other_nameser def test_the_proxy_serves_as_nobody_where_the_base_has_it_else_as_the_session_user(tmp_path, monkeypatch): - monkeypatch.setattr(site_proxy.pwd, "getpwnam", lambda name: SimpleNamespace(pw_name=name)) + fake_pwd = SimpleNamespace(getpwnam=lambda name: SimpleNamespace(pw_name=name)) + monkeypatch.setattr(site_proxy, "pwd", fake_pwd) assert site_proxy.serve_user("agent") == "nobody" def missing(name): raise KeyError(name) - monkeypatch.setattr(site_proxy.pwd, "getpwnam", missing) + monkeypatch.setattr(fake_pwd, "getpwnam", missing) assert site_proxy.serve_user("agent") == "agent" serve_path = tmp_path / "serve.json" serve_path.write_text(json.dumps({"user": "agent", "serve_user": "nobody", "sites": []})) assert (Config.load(str(serve_path)).user, Config.load(str(serve_path)).serve_user) == ("agent", "nobody") +def test_the_proxy_falls_back_when_pwd_is_unavailable(monkeypatch): + monkeypatch.setattr(site_proxy, "pwd", None) + assert site_proxy.serve_user("agent") == "agent" + assert site_proxy.trust_nss("agent", "/fake/ca.pem") == [] + site_proxy.drop_privileges("agent") + + def test_keep_starts_serve_again_each_time_it_ends(monkeypatch): runs = [] From 17562671bc93b7c1e2b155ff96d4de255c798e1f Mon Sep 17 00:00:00 2001 From: Saiyam Jain Date: Sat, 7 Nov 2026 03:56:44 +0530 Subject: [PATCH 2/3] test(computer): add fresh import test for site_proxy without pwd --- tst/unit/env/envs/test_computer_site_proxy.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tst/unit/env/envs/test_computer_site_proxy.py b/tst/unit/env/envs/test_computer_site_proxy.py index 049a90f87..0f9d02727 100644 --- a/tst/unit/env/envs/test_computer_site_proxy.py +++ b/tst/unit/env/envs/test_computer_site_proxy.py @@ -8,12 +8,14 @@ import base64 import contextlib import email.message +import importlib import io import json import os import socket import ssl import struct +import sys import urllib.error from datetime import timedelta from pathlib import Path @@ -699,6 +701,16 @@ def test_the_proxy_falls_back_when_pwd_is_unavailable(monkeypatch): site_proxy.drop_privileges("agent") +def test_the_proxy_imports_when_pwd_is_unavailable(monkeypatch): + monkeypatch.setitem(sys.modules, "pwd", None) + reloaded = importlib.reload(site_proxy) + try: + assert reloaded.pwd is None + finally: + monkeypatch.undo() + importlib.reload(site_proxy) + + def test_keep_starts_serve_again_each_time_it_ends(monkeypatch): runs = [] From cfc5ff218df5158721976338938532a2cd34246c Mon Sep 17 00:00:00 2001 From: Saiyam Jain Date: Sat, 7 Nov 2026 07:47:11 +0530 Subject: [PATCH 3/3] fix(computer): revert runtime privilege guards and test fresh site_proxy import in subprocess --- .../env/envs/computer/utils/site_proxy.py | 31 +++++++------------ .../store/object_store/local/store.py | 4 ++- tst/unit/env/envs/test_computer_site_proxy.py | 31 +++++++------------ 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/agent_env/env/envs/computer/utils/site_proxy.py b/src/agent_env/env/envs/computer/utils/site_proxy.py index ba1955778..86ba64334 100644 --- a/src/agent_env/env/envs/computer/utils/site_proxy.py +++ b/src/agent_env/env/envs/computer/utils/site_proxy.py @@ -612,25 +612,20 @@ async def serve(config: Config) -> None: def serve_user(session_user: Optional[str]) -> Optional[str]: """The user ``serve`` drops to: ``SERVE_USER`` where the base has it, else the session user.""" - if pwd is None: - return session_user try: pwd.getpwnam(SERVE_USER) - except KeyError: + except (KeyError, AttributeError): return session_user return SERVE_USER def drop_privileges(user: Optional[str]) -> None: - if user is None or pwd is None or not hasattr(os, "geteuid") or os.geteuid() != 0: + if user is None or os.geteuid() != 0: return entry = pwd.getpwnam(user) - if hasattr(os, "setgroups"): - os.setgroups([]) - if hasattr(os, "setgid"): - os.setgid(entry.pw_gid) - if hasattr(os, "setuid"): - os.setuid(entry.pw_uid) + os.setgroups([]) + os.setgid(entry.pw_gid) + os.setuid(entry.pw_uid) @dataclass(frozen=True) @@ -742,12 +737,9 @@ def trust_system(paths: Paths, ca_pem: str) -> Optional[str]: def trust_nss(user: Optional[str], ca: str) -> list[str]: """Add the CA to the session user's NSS databases, Chromium's (and a snap Chromium's when there is one), as that user, for a Chromium too old to read its ``CACertificates`` policy; none without ``certutil``.""" - if user is None or pwd is None or not shutil.which("certutil"): - return [] - try: - home = Path(pwd.getpwnam(user).pw_dir) - except KeyError: + if user is None or not shutil.which("certutil"): return [] + home = Path(pwd.getpwnam(user).pw_dir) folders = [home / ".pki" / "nssdb"] if (home / "snap" / "chromium").is_dir(): folders.append(home / "snap" / "chromium" / "current" / ".pki" / "nssdb") @@ -765,7 +757,7 @@ def trust_nss(user: Optional[str], ca: str) -> list[str]: def as_user(user: Optional[str], argv: list[str]) -> list[str]: - if user is None or not hasattr(os, "geteuid") or os.geteuid() != 0: + if user is None or os.geteuid() != 0: return argv return ["runuser", "-u", user, "--", *argv] @@ -774,9 +766,8 @@ def install(config_path: str, paths: Paths = Paths()) -> dict: """As root: write the sites' certificates, names and trust, start ``serve`` on what's written, and check each name loads as the session user; what was trusted and each check's result. The config's folder, which arrives root's alone, is opened to the session user (for the CA) only once its keys and the config are private.""" - uid = getattr(os, "geteuid", lambda: -1)() - if uid != 0 and paths.root == Path("/"): - raise SystemExit(f"the site proxy installs as root, not uid {uid}") + if os.geteuid() != 0 and paths.root == Path("/"): + raise SystemExit(f"the site proxy installs as root, not uid {os.geteuid()}") given = json.loads(Path(config_path).read_text()) folder = Path(config_path).parent user = given.get("user") @@ -853,7 +844,7 @@ def start(serve_path: str, log: Optional[str]) -> None: def open_log(log: str) -> int: Path(log).parent.mkdir(parents=True, exist_ok=True) - return os.open(log, os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600) + return os.open(log, os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_NOFOLLOW, 0o600) def keep(serve_path: str) -> None: diff --git a/src/agent_env/store/object_store/local/store.py b/src/agent_env/store/object_store/local/store.py index 81ce7a89c..e324894f5 100644 --- a/src/agent_env/store/object_store/local/store.py +++ b/src/agent_env/store/object_store/local/store.py @@ -23,8 +23,10 @@ from agent_env.config.paths import state_root from agent_env.store.base import ObjectAlreadyExistsError, ObjectNotFoundError -if os.name != "nt": +try: import fcntl +except ImportError: + fcntl = None from agent_env.store.local_state import ensure_state_dir from agent_env.store.object_store.local.grant_server import DEFAULT_ADVERTISE_HOST, DEFAULT_ALSO_SERVED, grant_server from agent_env.store.object_store.local.tls import check_local_host diff --git a/tst/unit/env/envs/test_computer_site_proxy.py b/tst/unit/env/envs/test_computer_site_proxy.py index 0f9d02727..842861de4 100644 --- a/tst/unit/env/envs/test_computer_site_proxy.py +++ b/tst/unit/env/envs/test_computer_site_proxy.py @@ -8,13 +8,13 @@ import base64 import contextlib import email.message -import importlib import io import json import os import socket import ssl import struct +import subprocess import sys import urllib.error from datetime import timedelta @@ -680,35 +680,28 @@ def test_a_wildcard_name_starts_dns_even_when_resolv_conf_names_no_other_nameser def test_the_proxy_serves_as_nobody_where_the_base_has_it_else_as_the_session_user(tmp_path, monkeypatch): - fake_pwd = SimpleNamespace(getpwnam=lambda name: SimpleNamespace(pw_name=name)) - monkeypatch.setattr(site_proxy, "pwd", fake_pwd) + monkeypatch.setattr(site_proxy.pwd, "getpwnam", lambda name: SimpleNamespace(pw_name=name)) assert site_proxy.serve_user("agent") == "nobody" def missing(name): raise KeyError(name) - monkeypatch.setattr(fake_pwd, "getpwnam", missing) + monkeypatch.setattr(site_proxy.pwd, "getpwnam", missing) assert site_proxy.serve_user("agent") == "agent" serve_path = tmp_path / "serve.json" serve_path.write_text(json.dumps({"user": "agent", "serve_user": "nobody", "sites": []})) assert (Config.load(str(serve_path)).user, Config.load(str(serve_path)).serve_user) == ("agent", "nobody") -def test_the_proxy_falls_back_when_pwd_is_unavailable(monkeypatch): - monkeypatch.setattr(site_proxy, "pwd", None) - assert site_proxy.serve_user("agent") == "agent" - assert site_proxy.trust_nss("agent", "/fake/ca.pem") == [] - site_proxy.drop_privileges("agent") - - -def test_the_proxy_imports_when_pwd_is_unavailable(monkeypatch): - monkeypatch.setitem(sys.modules, "pwd", None) - reloaded = importlib.reload(site_proxy) - try: - assert reloaded.pwd is None - finally: - monkeypatch.undo() - importlib.reload(site_proxy) +def test_the_proxy_imports_when_pwd_is_unavailable(): + subprocess.run( + [ + sys.executable, + "-c", + "import sys; sys.modules['pwd'] = None; from agent_env.env.envs.computer.utils import site_proxy; assert site_proxy.pwd is None", + ], + check=True, + ) def test_keep_starts_serve_again_each_time_it_ends(monkeypatch):