From fa125988c47ce292b1488638a5e2615f1aca3402 Mon Sep 17 00:00:00 2001 From: dgokeeffe <17697537+dgokeeffe@users.noreply.github.com> Date: Sat, 25 Jul 2026 08:51:30 +1000 Subject: [PATCH] fix(pi): point pi at config dir via PI_CODING_AGENT_DIR instead of redirecting HOME Pi honors the PI_CODING_AGENT_DIR env var to resolve its config directory (~/.pi/agent), so redirecting /Users/david.okeeffe to APP_DIR/pi-home was unnecessary. The HOME redirect broke macOS keychain default resolution under ucode: the Security framework looks for the login keychain under the redirected HOME, finds none, and security default-keychain returns 'A default keychain could not be found'. As a result gh auth, the git credential helper, and any keychain-backed tool failed inside pi. Setting PI_CODING_AGENT_DIR to the existing PI_CONFIG_DIR preserves config isolation (models.json/settings.json/sessions still land under APP_DIR/pi-home/.pi/agent) while leaving /Users/david.okeeffe as the user's real home, so the login keychain stays discoverable. Tests: the two pi e2e sites monkeypatched PI_UCODE_HOME/PI_CONFIG_PATH to redirect pi at a tmp home. They now also patch PI_CONFIG_DIR (read by build_runtime_env) and PI_SETTINGS_PATH/PI_SETTINGS_BACKUP_PATH (previously masked because the HOME redirect made pi read settings from the un-patched real APP_DIR path). --- src/ucode/agents/pi.py | 6 +++--- tests/test_agent_pi.py | 8 ++++++-- tests/test_e2e.py | 7 +++++-- tests/test_e2e_user_agent.py | 3 +++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/ucode/agents/pi.py b/src/ucode/agents/pi.py index e7c1760..b27e427 100644 --- a/src/ucode/agents/pi.py +++ b/src/ucode/agents/pi.py @@ -1,4 +1,4 @@ -"""Pi coding agent: writes ~/.pi/agent/models.json with Databricks-backed providers. +"""Pi coding agent: writes a ucode-private models.json with Databricks-backed providers. Pi (https://pi.dev) is a multi-provider coding agent. We register three providers in its `models.json`, each speaking the API dialect best suited to @@ -108,7 +108,7 @@ def render_overlay( codex_models: list[str], gemini_models: list[str], ) -> tuple[dict, list[list[str]]]: - """Return (overlay, managed_key_paths) for ~/.pi/agent/models.json.""" + """Return (overlay, managed_key_paths) for Pi's private agent config.""" providers: dict = {} keys: list[list[str]] = [["model"]] # Pi expands header values that match an env var name. Our UA contains @@ -237,7 +237,7 @@ def _refresh_forever(state: dict, stop_event: threading.Event) -> None: def build_runtime_env(token: str) -> dict[str, str]: env = os.environ.copy() env["OAUTH_TOKEN"] = token - env["HOME"] = str(PI_UCODE_HOME) + env["PI_CODING_AGENT_DIR"] = str(PI_CONFIG_DIR) return env diff --git a/tests/test_agent_pi.py b/tests/test_agent_pi.py index 0afc5fb..0d3be80 100644 --- a/tests/test_agent_pi.py +++ b/tests/test_agent_pi.py @@ -240,9 +240,13 @@ def test_sets_oauth_token(self): env = pi.build_runtime_env("tok") assert env["OAUTH_TOKEN"] == "tok" - def test_sets_ucode_home(self): + def test_sets_private_agent_dir_without_replacing_home(self, monkeypatch): + monkeypatch.setenv("HOME", "/real-user-home") + env = pi.build_runtime_env("tok") - assert env["HOME"] == str(pi.PI_UCODE_HOME) + + assert env["PI_CODING_AGENT_DIR"] == str(pi.PI_CONFIG_DIR) + assert env["HOME"] == "/real-user-home" class TestPiValidateCmd: diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 30ce287..80eb04a 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -903,14 +903,17 @@ def test_launch_pi_per_model(self, tmp_path, monkeypatch, e2e_state, e2e_workspa pytest.skip("No Pi-compatible models available on this workspace") monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) - # Pi reads models.json below HOME/.pi/agent. Point both pi's runtime - # HOME and our writer at the same isolated tmp home. + # Point PI_CODING_AGENT_DIR and ucode's config writer at the same + # isolated directory without changing the process HOME. pi_home = tmp_path / "pi-home" pi_dir = pi_home / ".pi" / "agent" config_path = pi_dir / "models.json" backup_path = tmp_path / "pi-models.backup.json" monkeypatch.setattr(pi, "PI_UCODE_HOME", pi_home) + monkeypatch.setattr(pi, "PI_CONFIG_DIR", pi_dir) monkeypatch.setattr(pi, "PI_CONFIG_PATH", config_path) + monkeypatch.setattr(pi, "PI_SETTINGS_PATH", pi_dir / "settings.json") + monkeypatch.setattr(pi, "PI_SETTINGS_BACKUP_PATH", tmp_path / "pi-settings.backup.json") monkeypatch.setattr(pi, "PI_BACKUP_PATH", backup_path) failures = [] diff --git a/tests/test_e2e_user_agent.py b/tests/test_e2e_user_agent.py index 884e663..e6cec21 100644 --- a/tests/test_e2e_user_agent.py +++ b/tests/test_e2e_user_agent.py @@ -325,7 +325,10 @@ def test_user_agent_arrives_at_gateway(self, tmp_path, monkeypatch, capture_serv monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) monkeypatch.setattr(pi, "PI_UCODE_HOME", pi_home) + monkeypatch.setattr(pi, "PI_CONFIG_DIR", pi_dir) monkeypatch.setattr(pi, "PI_CONFIG_PATH", config_path) + monkeypatch.setattr(pi, "PI_SETTINGS_PATH", pi_dir / "settings.json") + monkeypatch.setattr(pi, "PI_SETTINGS_BACKUP_PATH", tmp_path / "pi-settings.backup.json") monkeypatch.setattr(pi, "PI_BACKUP_PATH", tmp_path / "pi.backup.json") state = {