From 7d2f93a97f3a842d1089bf9064daf4ca88ba0037 Mon Sep 17 00:00:00 2001 From: MestreY0d4-Uninter <241404605+MestreY0d4-Uninter@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:25:02 +0000 Subject: [PATCH] fix: set HOME for Copilot ACP subprocesses Pass an explicit HOME into Copilot ACP child processes so delegated ACP runs do not fail when the ambient environment is missing HOME. Prefer the per-profile subprocess home when available, then fall back to HOME, expanduser('~'), pwd.getpwuid(...), and /home/openclaw. Add regression tests for both profile-home preference and clean HOME fallback. Refs #11068. --- agent/copilot_acp_client.py | 42 +++++++++++++++++++ tests/agent/test_copilot_acp_client.py | 57 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/agent/copilot_acp_client.py b/agent/copilot_acp_client.py index 783f94956..94d40d2d9 100644 --- a/agent/copilot_acp_client.py +++ b/agent/copilot_acp_client.py @@ -46,6 +46,47 @@ def _resolve_args() -> list[str]: return shlex.split(raw) +def _resolve_home_dir() -> str: + """Return a stable HOME for child ACP processes.""" + + try: + from hermes_constants import get_subprocess_home + + profile_home = get_subprocess_home() + if profile_home: + return profile_home + except Exception: + pass + + home = os.environ.get("HOME", "").strip() + if home: + return home + + expanded = os.path.expanduser("~") + if expanded and expanded != "~": + return expanded + + try: + import pwd + + resolved = pwd.getpwuid(os.getuid()).pw_dir.strip() + if resolved: + return resolved + except Exception: + pass + + # Last resort: /tmp (writable on any POSIX system). Avoids crashing the + # subprocess with no HOME; callers can set HERMES_HOME explicitly if they + # need a different writable dir. + return "/tmp" + + +def _build_subprocess_env() -> dict[str, str]: + env = os.environ.copy() + env["HOME"] = _resolve_home_dir() + return env + + def _jsonrpc_error(message_id: Any, code: int, message: str) -> dict[str, Any]: return { "jsonrpc": "2.0", @@ -382,6 +423,7 @@ class CopilotACPClient: text=True, bufsize=1, cwd=self._acp_cwd, + env=_build_subprocess_env(), ) except FileNotFoundError as exc: raise RuntimeError( diff --git a/tests/agent/test_copilot_acp_client.py b/tests/agent/test_copilot_acp_client.py index 52ad20a35..63c87fdab 100644 --- a/tests/agent/test_copilot_acp_client.py +++ b/tests/agent/test_copilot_acp_client.py @@ -144,3 +144,60 @@ class CopilotACPClientSafetyTests(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +# ── HOME env propagation tests (from PR #11285) ───────────────────── + +from unittest.mock import patch as _patch +import pytest + + +def _make_home_client(tmp_path): + return CopilotACPClient( + api_key="copilot-acp", + base_url="acp://copilot", + acp_command="copilot", + acp_args=["--acp", "--stdio"], + acp_cwd=str(tmp_path), + ) + + +def _fake_popen_capture(captured): + def _fake(cmd, **kwargs): + captured["cmd"] = cmd + captured["kwargs"] = kwargs + raise FileNotFoundError("copilot not found") + return _fake + + +def test_run_prompt_prefers_profile_home_when_available(monkeypatch, tmp_path): + hermes_home = tmp_path / "hermes" + profile_home = hermes_home / "home" + profile_home.mkdir(parents=True) + + monkeypatch.delenv("HOME", raising=False) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + captured = {} + client = _make_home_client(tmp_path) + + with _patch("agent.copilot_acp_client.subprocess.Popen", side_effect=_fake_popen_capture(captured)): + with pytest.raises(RuntimeError, match="Could not start Copilot ACP command"): + client._run_prompt("hello", timeout_seconds=1) + + assert captured["kwargs"]["env"]["HOME"] == str(profile_home) + + +def test_run_prompt_passes_home_when_parent_env_is_clean(monkeypatch, tmp_path): + monkeypatch.delenv("HOME", raising=False) + monkeypatch.delenv("HERMES_HOME", raising=False) + + captured = {} + client = _make_home_client(tmp_path) + + with _patch("agent.copilot_acp_client.subprocess.Popen", side_effect=_fake_popen_capture(captured)): + with pytest.raises(RuntimeError, match="Could not start Copilot ACP command"): + client._run_prompt("hello", timeout_seconds=1) + + assert "env" in captured["kwargs"] + assert captured["kwargs"]["env"]["HOME"]