fix(agent): resolve agent cwd from TERMINAL_CWD via one reader (closes #24882, #24969, #27383)#35028
Merged
teknium1 merged 9 commits intoJun 1, 2026
Conversation
…p redundant docker case)
…d fallback, not skip)
…very not skipped)
Cover the two new hardening behaviors that were unpinned: whitespace-only TERMINAL_CWD falling through to getcwd/None, and OSError from the getcwd fallback arm propagating to the build_environment_hints try/except guard.
…earch#29265) Port PR NousResearch#29365's tool-surface contract test: terminal/file/execute_code already honor TERMINAL_CWD (out of scope for the resolver cluster). Pinning the behavior makes the supersession of NousResearch#29365 airtight and guards against a future refactor silently regressing the workspace contract.
2129623 to
1dbfa92
Compare
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The agent reports — and
cds to — the daemon launch directory (the hermes-agent install dir) instead of the configuredterminal.cwd/TERMINAL_CWD, so the model ends up working in the wrong place. This bit gateway, cron, and Telegram sessions.build_environment_hints()emittedCurrent working directory: {os.getcwd()}(agent/prompt_builder.py:805), ignoringTERMINAL_CWDentirely.TERMINAL_CWDad-hoc (os.getenv("TERMINAL_CWD") or None) inagent/system_prompt.py, duplicating the cwd-resolution decision so the two prompt tiers could disagree.This is a reader-only correctness bug — not a config, transport, or service-manager bug.
Fix
One reader resolver as the single source of truth, with the two prompt-tier read sites routed through it.
agent/runtime_cwd.pycentralizes both reads. The docstring records the design lineage (terminal.cwd config is a foot-gun: CLI/TUI should always use launch directory #19214 / fix(cli): CLI/TUI on local backend always uses launch directory, ignores terminal.cwd #19242:terminal.cwdis bridged once toTERMINAL_CWDat gateway/cron startup; local-CLI deliberately leaves it unset) and the documented future seam Per-session working directory for gateway (OpenAI-compatible API) sessions #29531 (per-sessioncontextvar) — nothing speculative is pre-built.agent/prompt_builder.py:806—os.getcwd()→resolve_agent_cwd()for the "Current working directory:" display line. This is the bug.agent/system_prompt.py— context-file discovery →resolve_context_cwd(); drops the now-deadimport os.Two intentionally asymmetric resolvers:
resolve_agent_cwd()Pathexpanduser+is_dir()guard +os.getcwd()fallbackresolve_context_cwd()Path | Noneexpanduser, no isdir guard, no getcwd armNone= unset → the caller (build_context_files_prompt) getcwds, so discovery still runs (never skipped). A set-but-missing dir is returned as-is → discovery simply finds nothing. The getcwd fallback is owned by the caller here, by the resolver there.Remote backends unchanged: both fixes sit inside the existing
is_remote_backendgate (prompt_builder.py:791). docker / modal / ssh still suppress the host cwd line and use the live in-backend probe; the probe's cache key (:694) intentionally keeps readingos.getenv.Behavior-preserving: local-CLI (
TERMINAL_CWDunset) and remote backends produce output identical to before. The only new behavior is.strip()+expanduser()— strict improvements (whitespace-only no longer yields a" "path;~now expands).Design decision: why no gateway re-bridge
The superseded PRs #27488 / #29365 also added a per-turn re-bridge (
_reload_runtime_env_preserving_config_authorityingateway/run.py) that re-readsterminal.cwdand re-setsTERMINAL_CWDevery session. We deliberately do not adopt it:TERMINAL_CWDis set once at gateway/cron startup and is never deleted — it persists across/new. The re-bridge patches an env loss that does not occur../auto/cwdplaceholder-clobber zone (refs bug(gateway): macOS launchd can re-poison TERMINAL_CWD and load repo AGENTS.md #10817, fix(cli): load_cli_config() overwrites gateway's TERMINAL_CWD from MESSAGING_CWD #10225)./new" symptom ([Bug]: change working directory for Telegram agent #27383) was the prompt reader re-emittingos.getcwd()— fixed here inresolve_agent_cwd(), not by rewriting env. This diff contains no re-bridge code.Linkage
terminal.cwdnot injected into the system prompt — fixed atprompt_builder.py:805.--workdirnot reflected in prompt cwd — same read site.TERMINAL_CWD, now read correctly.WorkingDirectory/hermes updateenv loss — a different root cause.WorkingDirectory; resolves #11312's actual cause, complementary to this PR.Closes #24882, closes #24969, closes #27383.
Behavior changes & regression analysis
build_context_files_prompt(cwd=)str/NonePath/NonePath(cwd).resolve(); one prod callertest_system_prompt.py.strip().expanduser()test_expands_leading_tilde×2.resolve()in resolver:805test_propagates_oserror_from_getcwdimport osinsystem_prompt.pyScope / out of scope
Deliberately limited to the 2 prompt read sites. These readers were already
TERMINAL_CWD-first and are untouched:tools/terminal_tool.py:1039,tools/file_tools.py:124,agent/tool_executor.py:198/667,tools/code_execution_tool.py:1681,tools/delegate_tool.py:653,agent/agent_init.py:1539,gateway/run.py(the bridge writer), andprompt_builder.py:694(remote-probe cache key).Edge cases
TERMINAL_CWD→ strip → falsy → agent uses getcwd, context returnsNone.TERMINAL_CWD→ resolver does not.resolve(); acceptable because the value is bridged from config and expected absolute.TERMINAL_CWD→ agent:is_dir()fails → getcwd fallback; context: returns the missing dir (no guard) → discovery finds nothing.Testing
Touched-area suites pass — 146 passed, 1 skipped;
ruffclean on all 7 changed files. Pinned behaviors: whitespace strip · OSError propagation · isdir asymmetry ·None→getcwd ·~expansion (both resolvers) · end-to-end hint (set → value, unset → launch dir) · tool-surface contract.Infographic