Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions agent/copilot_acp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,44 @@ 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

return "/home/openclaw"


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",
Expand Down Expand Up @@ -382,6 +420,7 @@ def _run_prompt(self, prompt_text: str, *, timeout_seconds: float) -> tuple[str,
text=True,
bufsize=1,
cwd=self._acp_cwd,
env=_build_subprocess_env(),
)
except FileNotFoundError as exc:
raise RuntimeError(
Expand Down
57 changes: 57 additions & 0 deletions tests/agent/test_copilot_acp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,60 @@ def test_write_text_file_respects_safe_root(self) -> None:

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