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"]