From 4bc72960427a8d56631dcead9af6ddd920f86661 Mon Sep 17 00:00:00 2001 From: firefly Date: Fri, 29 May 2026 17:19:03 -0400 Subject: [PATCH] feat(agent): add runtime_cwd resolver (single source of truth for working dir) --- agent/runtime_cwd.py | 31 +++++++++++++++++++++++++ tests/agent/test_runtime_cwd.py | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 agent/runtime_cwd.py create mode 100644 tests/agent/test_runtime_cwd.py diff --git a/agent/runtime_cwd.py b/agent/runtime_cwd.py new file mode 100644 index 000000000000..ad084ac89ec7 --- /dev/null +++ b/agent/runtime_cwd.py @@ -0,0 +1,31 @@ +"""Single source of truth for the agent working directory. + +`TERMINAL_CWD` is the runtime carrier for the configured working directory +(design #19214/#19242: `terminal.cwd` is bridged once to `TERMINAL_CWD` at +gateway/cron startup). The local-CLI backend deliberately leaves it unset and +relies on the launch dir. Reading it in one place keeps the system prompt, the +tool surfaces, and context-file discovery agreeing on where the agent lives. + +The #29531 per-session extension point is this function: a future PR adds a +contextvar arm inside `resolve_agent_cwd` and `.set()`s it at the +`set_session_vars` seam — by design, not a reopening hazard. +""" + +import os +from pathlib import Path + + +def resolve_agent_cwd() -> Path: + raw = os.environ.get("TERMINAL_CWD", "").strip() + if raw: + p = Path(raw).expanduser() + if p.is_dir(): + return p + return Path(os.getcwd()) + + +def resolve_context_cwd() -> Path | None: + # None is load-bearing: it signals "skip context-file discovery" so the + # gateway install dir's AGENTS.md isn't slurped (see system_prompt.py). + raw = os.environ.get("TERMINAL_CWD", "").strip() + return Path(raw).expanduser() if raw else None diff --git a/tests/agent/test_runtime_cwd.py b/tests/agent/test_runtime_cwd.py new file mode 100644 index 000000000000..dbc8c974d068 --- /dev/null +++ b/tests/agent/test_runtime_cwd.py @@ -0,0 +1,40 @@ +"""Tests for agent/runtime_cwd.py — the single source of truth for the agent working directory.""" + +import os +from pathlib import Path + +from agent.runtime_cwd import resolve_agent_cwd, resolve_context_cwd + + +class TestResolveAgentCwd: + def test_prefers_terminal_cwd_over_getcwd(self, monkeypatch, tmp_path): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + monkeypatch.chdir(os.path.expanduser("~")) + assert resolve_agent_cwd() == tmp_path + + def test_falls_back_to_getcwd_when_unset(self, monkeypatch, tmp_path): + # The #19242 local-CLI contract: TERMINAL_CWD is unset, so the launch dir wins. + monkeypatch.delenv("TERMINAL_CWD", raising=False) + monkeypatch.chdir(tmp_path) + assert resolve_agent_cwd() == tmp_path + + def test_skips_nonexistent_terminal_cwd(self, monkeypatch, tmp_path): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path / "gone")) + monkeypatch.chdir(tmp_path) + assert resolve_agent_cwd() == tmp_path + + def test_expands_leading_tilde(self, monkeypatch): + monkeypatch.setenv("TERMINAL_CWD", "~") + assert resolve_agent_cwd() == Path(os.path.expanduser("~")) + + +class TestResolveContextCwd: + def test_returns_dir_when_set(self, monkeypatch, tmp_path): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + assert resolve_context_cwd() == tmp_path + + def test_returns_none_when_unset(self, monkeypatch): + # None is load-bearing: it tells the caller to skip context-file discovery + # (don't slurp the gateway install dir's AGENTS.md). + monkeypatch.delenv("TERMINAL_CWD", raising=False) + assert resolve_context_cwd() is None