mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(local): recover when persistent_shell cwd is deleted (#17558)
When a tool call deletes its own working directory (`cd /tmp/foo && rm -rf /tmp/foo`), the next `subprocess.Popen(args, cwd=self.cwd)` raised `FileNotFoundError: [Errno 2]` before bash even started — every subsequent terminal/file-tool call hit the same wedge until the gateway restarted. Fix in `LocalEnvironment._run_bash`: before handing `self.cwd` to Popen, resolve a safe alternative when the path is gone (walk up to the nearest existing ancestor, falling back to `tempfile.gettempdir()` only as a last resort). Log a warning so the recovery is visible — not silent — and update `self.cwd` so the next call doesn't repeat the message. Defense in depth in `LocalEnvironment._update_cwd`: only adopt the new cwd when it still exists as a directory. `pwd -P` from a deleted cwd can leave a stale value in the marker file; refusing to store a missing path keeps `self.cwd` valid by construction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8fb9270c4
commit
9644b8ae67
2 changed files with 199 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
|||
"""Local execution environment — spawn-per-call with session snapshot."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
|
|
@ -12,6 +13,30 @@ from tools.environments.base import BaseEnvironment, _pipe_stdin
|
|||
|
||||
_IS_WINDOWS = platform.system() == "Windows"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_safe_cwd(cwd: str) -> str:
|
||||
"""Return ``cwd`` if it exists as a directory, else the nearest existing
|
||||
ancestor. Falls back to ``tempfile.gettempdir()`` only if walking up the
|
||||
path can't find any existing directory (effectively never on a healthy
|
||||
filesystem, but cheap belt-and-braces).
|
||||
|
||||
Used by ``_run_bash`` to recover when the configured cwd is gone — most
|
||||
commonly because a previous tool call deleted its own working directory
|
||||
(issue #17558). Without this guard, ``subprocess.Popen(..., cwd=...)``
|
||||
raises ``FileNotFoundError`` before bash starts, wedging every subsequent
|
||||
terminal call until the gateway restarts.
|
||||
"""
|
||||
if cwd and os.path.isdir(cwd):
|
||||
return cwd
|
||||
parent = os.path.dirname(cwd) if cwd else ""
|
||||
while parent and parent != os.path.dirname(parent):
|
||||
if os.path.isdir(parent):
|
||||
return parent
|
||||
parent = os.path.dirname(parent)
|
||||
return tempfile.gettempdir()
|
||||
|
||||
|
||||
# Hermes-internal env vars that should NOT leak into terminal subprocesses.
|
||||
_HERMES_PROVIDER_ENV_FORCE_PREFIX = "_HERMES_FORCE_"
|
||||
|
|
@ -358,6 +383,21 @@ class LocalEnvironment(BaseEnvironment):
|
|||
args = [bash, "-l", "-c", cmd_string] if login else [bash, "-c", cmd_string]
|
||||
run_env = _make_run_env(self.env)
|
||||
|
||||
# Recover when the cwd has been deleted out from under us — usually by
|
||||
# a previous tool call that ran ``rm -rf`` on its own working dir
|
||||
# (issue #17558). Popen would otherwise raise FileNotFoundError on
|
||||
# the cwd before bash starts, wedging every subsequent call until the
|
||||
# gateway restarts.
|
||||
safe_cwd = _resolve_safe_cwd(self.cwd)
|
||||
if safe_cwd != self.cwd:
|
||||
logger.warning(
|
||||
"LocalEnvironment cwd %r is missing on disk; "
|
||||
"falling back to %r so terminal commands keep working.",
|
||||
self.cwd,
|
||||
safe_cwd,
|
||||
)
|
||||
self.cwd = safe_cwd
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
text=True,
|
||||
|
|
@ -452,11 +492,17 @@ class LocalEnvironment(BaseEnvironment):
|
|||
pass
|
||||
|
||||
def _update_cwd(self, result: dict):
|
||||
"""Read CWD from temp file (local-only, no round-trip needed)."""
|
||||
"""Read CWD from temp file (local-only, no round-trip needed).
|
||||
|
||||
Skip the assignment when the path no longer exists as a directory —
|
||||
``pwd -P`` on a deleted cwd can leave a stale value in the marker
|
||||
file, and propagating it would re-wedge the next ``Popen``. The
|
||||
``_run_bash`` recovery path will resolve a safe fallback if needed.
|
||||
"""
|
||||
try:
|
||||
with open(self._cwd_file) as f:
|
||||
cwd_path = f.read().strip()
|
||||
if cwd_path:
|
||||
if cwd_path and os.path.isdir(cwd_path):
|
||||
self.cwd = cwd_path
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue