"""Filesystem adapter for Desktop workspaces executed over SSH.""" from __future__ import annotations import base64 import binascii import posixpath import shlex import subprocess import threading import uuid from dataclasses import dataclass from typing import Any, Iterator from hermes_cli._subprocess_compat import windows_hide_flags from tools.environments.ssh import SSHEnvironment class SshWorkspaceFsError(RuntimeError): """A filesystem error reported by the SSH execution target.""" def __init__(self, code: str, message: str | None = None): super().__init__(message or code) self.code = code @dataclass(frozen=True) class _SshFsConfig: host: str user: str port: int key_path: str cwd: str timeout: int _CACHE_LOCK = threading.RLock() _BACKENDS: dict[str, tuple[_SshFsConfig, "SshWorkspaceFs"]] = {} class SshWorkspaceFs: """Binary-safe workspace operations using the configured SSH transport.""" def __init__(self, env: SSHEnvironment): self._env = env self._lock = threading.RLock() @property def cwd(self) -> str: return self._normalize(self._env.cwd) def _normalize(self, path: str) -> str: raw = str(path or "").strip() if not raw: raise SshWorkspaceFsError("EINVAL", "Path is required") if "\0" in raw or raw.lower().startswith("file:"): raise SshWorkspaceFsError("EINVAL", "Invalid path") if raw == "~": raw = self._env._remote_home elif raw.startswith("~/"): raw = posixpath.join(self._env._remote_home, raw[2:]) elif not raw.startswith("/"): raw = posixpath.join(self._env.cwd, raw) return posixpath.normpath(raw) def _execute(self, command: str, *, stdin_data: str | None = None, timeout: int | None = None) -> dict: with self._lock: return self._env.execute( command, cwd=self._env.cwd, stdin_data=stdin_data, timeout=timeout or self._env.timeout, ) @staticmethod def _error_code(output: str, fallback: str = "EIO") -> str: marker = "__HERMES_FS_ERROR__:" for line in (output or "").splitlines(): if line.startswith(marker): return line[len(marker) :].strip() or fallback return fallback def list_dir(self, path: str, hidden_names: set[str] | frozenset[str]) -> dict[str, Any]: target = self._normalize(path) quoted = shlex.quote(target) command = f""" p={quoted} if [ ! -e "$p" ] && [ ! -L "$p" ]; then printf '__HERMES_FS_ERROR__:ENOENT\\n'; exit 44; fi if [ ! -d "$p" ]; then printf '__HERMES_FS_ERROR__:ENOTDIR\\n'; exit 45; fi if [ ! -r "$p" ]; then printf '__HERMES_FS_ERROR__:EACCES\\n'; exit 46; fi for child in "$p"/* "$p"/.[!.]* "$p"/..?*; do [ -e "$child" ] || [ -L "$child" ] || continue name=${{child##*/}} name64=$(printf '%s' "$name" | base64 | tr -d '\\r\\n') path64=$(printf '%s' "$child" | base64 | tr -d '\\r\\n') if [ -d "$child" ]; then kind=d; else kind=f; fi printf '%s\\t%s\\t%s\\n' "$name64" "$path64" "$kind" done """ result = self._execute(command) if result.get("returncode") != 0: return {"entries": [], "error": self._error_code(result.get("output", ""), "read-error")} entries = [] try: for line in result.get("output", "").splitlines(): if not line.strip(): continue name64, path64, kind = line.split("\t", 2) name = base64.b64decode(name64, validate=True).decode("utf-8", errors="replace") child_path = base64.b64decode(path64, validate=True).decode("utf-8", errors="replace") if name in hidden_names: continue entries.append({"name": name, "path": child_path, "isDirectory": kind == "d"}) except (ValueError, UnicodeError, binascii.Error) as exc: raise SshWorkspaceFsError("EIO", "SSH filesystem returned an invalid directory listing") from exc entries.sort(key=lambda item: (not item["isDirectory"], item["name"].lower(), item["name"])) return {"entries": entries} def read_bytes( self, path: str, *, max_bytes: int, read_limit: int | None = None, ) -> tuple[bytes, int, str]: target = self._normalize(path) quoted = shlex.quote(target) read_command = ( f"set -o pipefail; head -c {int(read_limit)} < \"$p\" | base64" if read_limit is not None else 'base64 < "$p"' ) command = f""" p={quoted} if [ ! -e "$p" ] && [ ! -L "$p" ]; then printf '__HERMES_FS_ERROR__:ENOENT\\n'; exit 44; fi if [ ! -f "$p" ]; then printf '__HERMES_FS_ERROR__:ENOTREG\\n'; exit 45; fi size=$(stat -c %s "$p" 2>/dev/null || stat -f %z "$p" 2>/dev/null) || {{ printf '__HERMES_FS_ERROR__:EACCES\\n'; exit 46; }} if [ "$size" -gt {int(max_bytes)} ]; then printf '__HERMES_FS_ERROR__:EFBIG\\n'; exit 47; fi printf '__HERMES_FS_SIZE__:%s\\n' "$size" {{ {read_command}; }} || {{ printf '__HERMES_FS_ERROR__:EACCES\\n'; exit 46; }} """ result = self._execute(command) output = result.get("output", "") if result.get("returncode") != 0: raise SshWorkspaceFsError(self._error_code(output)) lines = output.splitlines() if not lines or not lines[0].startswith("__HERMES_FS_SIZE__:"): raise SshWorkspaceFsError("EIO", "SSH filesystem did not return a file size") try: size = int(lines[0].split(":", 1)[1]) except ValueError as exc: raise SshWorkspaceFsError("EIO", "SSH filesystem returned an invalid file size") from exc encoded = "".join(lines[1:]) try: data = base64.b64decode(encoded, validate=True) except (ValueError, binascii.Error) as exc: raise SshWorkspaceFsError("EIO", "SSH filesystem returned invalid file data") from exc return data, size, target def inspect_file(self, path: str) -> tuple[str, int]: """Return a regular file's resolved remote path and size without reading it.""" target = self._normalize(path) command = f""" p={shlex.quote(target)} target=$(realpath "$p" 2>/dev/null || readlink -f "$p" 2>/dev/null) || {{ printf '__HERMES_FS_ERROR__:ENOENT\\n'; exit 44; }} if [ ! -f "$target" ]; then printf '__HERMES_FS_ERROR__:ENOTREG\\n'; exit 45; fi size=$(stat -c %s "$target" 2>/dev/null || stat -f %z "$target" 2>/dev/null) || {{ printf '__HERMES_FS_ERROR__:EACCES\\n'; exit 46; }} printf '__HERMES_FS_TARGET__:%s\\n' "$(printf '%s' "$target" | base64 | tr -d '\\r\\n')" printf '__HERMES_FS_SIZE__:%s\\n' "$size" """ result = self._execute(command) output = result.get("output", "") if result.get("returncode") != 0: raise SshWorkspaceFsError(self._error_code(output)) target_line = next((line for line in output.splitlines() if line.startswith("__HERMES_FS_TARGET__:")), "") size_line = next((line for line in output.splitlines() if line.startswith("__HERMES_FS_SIZE__:")), "") if not target_line or not size_line: raise SshWorkspaceFsError("EIO", "SSH filesystem did not return file metadata") try: resolved = base64.b64decode(target_line.split(":", 1)[1], validate=True).decode("utf-8", errors="strict") size = int(size_line.split(":", 1)[1]) except (ValueError, UnicodeError, binascii.Error) as exc: raise SshWorkspaceFsError("EIO", "SSH filesystem returned invalid file metadata") from exc return resolved, size def stream_file(self, path: str, chunk_size: int = 64 * 1024) -> Iterator[bytes]: """Stream an already-authorized remote file without buffering it in memory.""" target = self._normalize(path) command = self._env._build_ssh_command() command.extend(["bash", "-c", shlex.quote(f"exec cat < {shlex.quote(target)}")]) process = subprocess.Popen( command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, creationflags=windows_hide_flags(), ) try: assert process.stdout is not None while chunk := process.stdout.read(chunk_size): yield chunk finally: if process.poll() is None: process.terminate() process.wait() def write_text(self, path: str, content: str, *, max_bytes: int) -> tuple[str, int]: target = self._normalize(path) encoded_size = len(content.encode("utf-8")) if encoded_size > max_bytes: raise SshWorkspaceFsError("EFBIG") parent = posixpath.dirname(target) or "/" temp_path = posixpath.join(parent, f".{posixpath.basename(target)}.hermes-tmp-{uuid.uuid4().hex[:12]}") q_target = shlex.quote(target) q_parent = shlex.quote(parent) q_temp = shlex.quote(temp_path) command = f""" target={q_target} parent={q_parent} tmp={q_temp} if [ ! -d "$parent" ]; then printf '__HERMES_FS_ERROR__:ENOENT\\n'; exit 44; fi if [ -e "$target" ] && [ ! -f "$target" ]; then printf '__HERMES_FS_ERROR__:ENOTREG\\n'; exit 45; fi trap 'rm -f "$tmp"' EXIT HUP INT TERM cat > "$tmp" && mv -f "$tmp" "$target" """ result = self._execute(command, stdin_data=content) if result.get("returncode") != 0 or result.get("stdin_error"): raise SshWorkspaceFsError(self._error_code(result.get("output", ""))) return target, encoded_size def git_root(self, path: str) -> str | None: target = self._normalize(path) quoted = shlex.quote(target) command = f""" p={quoted} [ -d "$p" ] || p=${{p%/*}} while [ -n "$p" ]; do if [ -e "$p/.git" ]; then printf '%s\\n' "$p"; exit 0; fi [ "$p" = / ] && break p=${{p%/*}}; [ -n "$p" ] || p=/ done exit 1 """ result = self._execute(command) if result.get("returncode") != 0: return None return (result.get("output") or "").strip() or None def git_branch(self, path: str) -> str: target = self._normalize(path) result = self._execute(f"git -C {shlex.quote(target)} branch --show-current 2>/dev/null") return (result.get("output") or "").strip() if result.get("returncode") == 0 else "" def get_ssh_workspace_fs(profile_key: str, terminal_config: dict[str, Any]) -> SshWorkspaceFs | None: """Return a cached adapter when this profile selects the SSH backend.""" if str(terminal_config.get("backend") or "local").strip().lower() != "ssh": return None config = _SshFsConfig( host=str(terminal_config.get("ssh_host") or "").strip(), user=str(terminal_config.get("ssh_user") or "").strip(), port=int(terminal_config.get("ssh_port") or 22), key_path=str(terminal_config.get("ssh_key") or "").strip(), cwd=str(terminal_config.get("cwd") or "~").strip() or "~", timeout=int(terminal_config.get("timeout") or 180), ) if not config.host or not config.user: raise SshWorkspaceFsError("ECONN", "SSH host and user are required") with _CACHE_LOCK: cached = _BACKENDS.get(profile_key) if cached and cached[0] == config: return cached[1] if cached: cached[1]._env.cleanup() try: env = SSHEnvironment( host=config.host, user=config.user, cwd=config.cwd, timeout=config.timeout, port=config.port, key_path=config.key_path, sync_files=False, ) except Exception as exc: raise SshWorkspaceFsError("ECONN", "Could not connect to the SSH workspace") from exc backend = SshWorkspaceFs(env) _BACKENDS[profile_key] = (config, backend) return backend