mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
feat(gateway,acp): translate cross-boundary cwd when running in WSL
Add shared translators in hermes_constants (Windows drive → /mnt, `\\wsl(.localhost|$)\` UNC → POSIX, gated on is_wsl) and apply them at the gateway session-cwd boundary so a Windows-host UI can hand the WSL backend a path it can actually chdir into. De-dups the ACP adapter's private `_win_path_to_wsl` onto the shared helper and extends it to the UNC spelling. Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
This commit is contained in:
parent
88fbc8825c
commit
3c7b9f2e9d
4 changed files with 90 additions and 21 deletions
|
|
@ -26,31 +26,18 @@ from typing import Any, Dict, List, Optional
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _win_path_to_wsl(path: str) -> str | None:
|
||||
"""Convert a Windows drive path to its WSL /mnt/<drive>/... equivalent."""
|
||||
match = re.match(r"^([A-Za-z]):[\\/](.*)$", path)
|
||||
if not match:
|
||||
return None
|
||||
drive = match.group(1).lower()
|
||||
tail = match.group(2).replace("\\", "/")
|
||||
return f"/mnt/{drive}/{tail}"
|
||||
|
||||
|
||||
def _translate_acp_cwd(cwd: str) -> str:
|
||||
"""Translate Windows ACP cwd values when Hermes itself is running in WSL.
|
||||
|
||||
Windows ACP clients can launch ``hermes acp`` inside WSL while still sending
|
||||
editor workspaces as Windows drive paths such as ``E:\\Projects``. Store
|
||||
and execute against the WSL mount path so agents, tools, and persisted ACP
|
||||
sessions all agree on the usable workspace. Native Linux/macOS keeps the
|
||||
original cwd unchanged.
|
||||
editor workspaces as Windows drive paths (``E:\\Projects``) or
|
||||
``\\\\wsl.localhost\\`` UNC paths. Store and execute against the POSIX form so
|
||||
agents, tools, and persisted ACP sessions all agree on the usable workspace.
|
||||
Native Linux/macOS keeps the original cwd unchanged.
|
||||
"""
|
||||
from hermes_constants import is_wsl
|
||||
from hermes_constants import translate_cwd_for_wsl_backend
|
||||
|
||||
if not is_wsl():
|
||||
return cwd
|
||||
translated = _win_path_to_wsl(str(cwd))
|
||||
return translated if translated is not None else cwd
|
||||
return translate_cwd_for_wsl_backend(str(cwd))
|
||||
|
||||
|
||||
def _normalize_cwd_for_compare(cwd: str | None) -> str:
|
||||
|
|
@ -61,7 +48,9 @@ def _normalize_cwd_for_compare(cwd: str | None) -> str:
|
|||
|
||||
# Normalize Windows drive paths into the equivalent WSL mount form so
|
||||
# ACP history filters match the same workspace across Windows and WSL.
|
||||
translated = _win_path_to_wsl(expanded)
|
||||
from hermes_constants import windows_path_to_wsl
|
||||
|
||||
translated = windows_path_to_wsl(expanded)
|
||||
if translated is not None:
|
||||
expanded = translated
|
||||
elif re.match(r"^/mnt/[A-Za-z]/", expanded):
|
||||
|
|
|
|||
|
|
@ -854,6 +854,48 @@ def is_wsl() -> bool:
|
|||
return _wsl_detected
|
||||
|
||||
|
||||
def windows_path_to_wsl(path: str) -> str | None:
|
||||
"""Convert a Windows drive path (``C:\\...``) to its ``/mnt/<drive>/...`` form."""
|
||||
import re
|
||||
|
||||
match = re.match(r"^([A-Za-z]):[\\/](.*)$", str(path or "").strip())
|
||||
if not match:
|
||||
return None
|
||||
drive = match.group(1).lower()
|
||||
tail = match.group(2).replace("\\", "/")
|
||||
return f"/mnt/{drive}/{tail}"
|
||||
|
||||
|
||||
def wsl_unc_path_to_posix(path: str) -> str | None:
|
||||
"""Convert a Windows WSL UNC path (``\\\\wsl.localhost\\<distro>\\...`` or the
|
||||
legacy ``\\\\wsl$\\...``) to a POSIX path inside the distro."""
|
||||
import re
|
||||
|
||||
normalized = str(path or "").strip().replace("/", "\\")
|
||||
match = re.match(r"^\\\\wsl(?:\.localhost|\$)\\[^\\]+\\(.*)$", normalized, re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
tail = match.group(1).replace("\\", "/")
|
||||
return f"/{tail}" if tail else "/"
|
||||
|
||||
|
||||
def translate_cwd_for_wsl_backend(cwd: str) -> str:
|
||||
"""Normalize a cross-boundary cwd when Hermes itself runs inside WSL.
|
||||
|
||||
A Windows-host UI (native picker / drive path / ``\\\\wsl.localhost\\`` UNC)
|
||||
can hand the WSL backend a path it can't ``chdir`` into. Map it to the POSIX
|
||||
equivalent so the picker, sidebar, and sessions all agree on the workspace.
|
||||
No-op off WSL and for paths that are already POSIX.
|
||||
"""
|
||||
if not is_wsl():
|
||||
return cwd
|
||||
for translator in (wsl_unc_path_to_posix, windows_path_to_wsl):
|
||||
translated = translator(cwd)
|
||||
if translated is not None:
|
||||
return translated
|
||||
return cwd
|
||||
|
||||
|
||||
_container_detected: bool | None = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -834,3 +834,38 @@ class TestGetHermesDir:
|
|||
legacy.symlink_to(empty)
|
||||
result = get_hermes_dir("cache/audio", "audio_cache")
|
||||
assert result == tmp_path / "cache/audio"
|
||||
|
||||
|
||||
class TestWslPathTranslation:
|
||||
"""Cross-boundary path translation for a Windows-host UI + WSL backend."""
|
||||
|
||||
def test_windows_drive_to_wsl_mount(self):
|
||||
assert hermes_constants.windows_path_to_wsl(r"C:\Users\alex") == "/mnt/c/Users/alex"
|
||||
assert hermes_constants.windows_path_to_wsl("C:/Users/alex") == "/mnt/c/Users/alex"
|
||||
assert hermes_constants.windows_path_to_wsl("D:\\") == "/mnt/d/"
|
||||
|
||||
def test_windows_drive_ignores_non_drive_paths(self):
|
||||
assert hermes_constants.windows_path_to_wsl("/home/alex") is None
|
||||
assert hermes_constants.windows_path_to_wsl("relative\\dir") is None
|
||||
|
||||
def test_wsl_unc_to_posix_both_spellings(self):
|
||||
assert hermes_constants.wsl_unc_path_to_posix(r"\\wsl.localhost\Ubuntu\home\alex") == "/home/alex"
|
||||
assert hermes_constants.wsl_unc_path_to_posix(r"\\wsl$\Ubuntu\home\alex") == "/home/alex"
|
||||
# Forward-slash spelling and distro root.
|
||||
assert hermes_constants.wsl_unc_path_to_posix("//wsl.localhost/Debian/srv/app") == "/srv/app"
|
||||
assert hermes_constants.wsl_unc_path_to_posix("\\\\wsl.localhost\\Ubuntu\\") == "/"
|
||||
|
||||
def test_wsl_unc_ignores_non_unc_paths(self):
|
||||
assert hermes_constants.wsl_unc_path_to_posix(r"C:\Users\alex") is None
|
||||
assert hermes_constants.wsl_unc_path_to_posix("/home/alex") is None
|
||||
|
||||
def test_translate_is_noop_off_wsl(self, monkeypatch):
|
||||
monkeypatch.setattr(hermes_constants, "is_wsl", lambda: False)
|
||||
assert hermes_constants.translate_cwd_for_wsl_backend(r"C:\Users\alex") == r"C:\Users\alex"
|
||||
|
||||
def test_translate_maps_windows_and_unc_on_wsl(self, monkeypatch):
|
||||
monkeypatch.setattr(hermes_constants, "is_wsl", lambda: True)
|
||||
assert hermes_constants.translate_cwd_for_wsl_backend(r"C:\Users\alex") == "/mnt/c/Users/alex"
|
||||
assert hermes_constants.translate_cwd_for_wsl_backend(r"\\wsl.localhost\Ubuntu\home\alex") == "/home/alex"
|
||||
# Already-POSIX paths pass through untouched.
|
||||
assert hermes_constants.translate_cwd_for_wsl_backend("/home/alex") == "/home/alex"
|
||||
|
|
|
|||
|
|
@ -1859,7 +1859,10 @@ def _persist_session_git_meta(session: dict, cwd: str) -> None:
|
|||
|
||||
|
||||
def _set_session_cwd(session: dict, cwd: str) -> str:
|
||||
resolved = os.path.abspath(os.path.expanduser(str(cwd)))
|
||||
from hermes_constants import translate_cwd_for_wsl_backend
|
||||
|
||||
cwd = translate_cwd_for_wsl_backend(str(cwd))
|
||||
resolved = os.path.abspath(os.path.expanduser(cwd))
|
||||
if not os.path.isdir(resolved):
|
||||
raise ValueError(f"working directory does not exist: {cwd}")
|
||||
session["cwd"] = resolved
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue