Skip to content

fix(desktop): browse SSH-backed workspaces remotely - #99538

Merged
OutThisLife merged 1 commit into
NousResearch:mainfrom
nateEc:codex/fix-ssh-workspace-files
Sep 29, 2026
Merged

OutThisLife merged 1 commit into
NousResearch:mainfrom
nateEc:codex/fix-ssh-workspace-files

Conversation

@nateEc

@nateEc nateEc commented Aug 31, 2026

Copy link
Copy Markdown

Fixes #98056

Summary

  • Route remote Desktop workspace list, read, write, download, and git metadata operations through the configured SSH execution environment.
  • Keep local workspace behavior unchanged and skip agent-file synchronization for browse-only SSH sessions.
  • Preserve the original implementation author by cherry-picking and adapting #98076.

Tests

  • scripts/run_tests.sh tests/hermes_cli/test_web_server_fs.py tests/hermes_cli/test_ssh_workspace_fs.py tests/tools/test_ssh_environment.py tests/tools/test_ssh_bulk_upload.py tests/tools/test_sync_back_backends.py -q
    • 42 passed, 5 skipped

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) backend/ssh SSH remote execution P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 31, 2026
@Enough1122

Copy link
Copy Markdown

AI code review — automated review; please use your judgment.

Well-hardened SSH filesystem adapter. Injection safety is handled consistently: all remote paths go through shlex.quote after _normalize (hermes_cli/ssh_workspace_fs.py:56-68), which also rejects NUL bytes and file: schemes; file names in listings are base64-encoded before transport so tabs/newlines in names survive the TSV protocol (ssh_workspace_fs.py:98-101). write_text uses a temp file + mv -f with a trap cleanup (ssh_workspace_fs.py:176-184), and the size cap is enforced client-side before any bytes are sent. The sync_files=False opt-out on SSHEnvironment (tools/environments/ssh.py:795-825) cleanly prevents browsing from mutating the target, with a test that fails loudly if file sync starts. Web routes delegate to the backend under asyncio.to_thread and map error codes to HTTP statuses via _raise_fs_backend_error; the SSH fs_download path correctly applies _is_sensitive_path like the local path.

Non-blocking:

  • fs_read_text over SSH reads up to _FS_TEXT_SOURCE_MAX_BYTES to serve a _FS_TEXT_PREVIEW_MAX_BYTES preview (web_server.py:3148-3155); passing max_bytes=_FS_TEXT_PREVIEW_MAX_BYTES would avoid transferring data that is then sliced away, at the cost of a different EFBIG behavior for large files — worth a look if previews on slow links matter.
  • _BACKENDS entries are only replaced on config mismatch, never evicted (ssh_workspace_fs.py:41-42); stale connections for profiles that stopped using SSH linger until the next lookup with that profile key.
  • read_bytes's stat-then-read has a benign TOCTOU (file can grow between stat and head), but read_limit/head -c bounds the damage.

Verdict: LGTM

Copy link
Copy Markdown

One detail in the current review may be worth revisiting before merge.

In the SSH branch of fs_download(), the sensitive-path check happens after:

data, _size, target = await asyncio.to_thread(
    backend.read_bytes,
    path,
    max_bytes=_FS_TEXT_SOURCE_MAX_BYTES,
)

SshWorkspaceFs.read_bytes() does not apply the sensitive-path policy itself, so the remote file is read and transferred into the Gateway before _is_sensitive_path(target_path) can return 403.

I also noticed two related SSH-download differences from the local path:

  1. The remote download uses _FS_TEXT_SOURCE_MAX_BYTES and can therefore return EFBIG/413 for a file that local FileResponse would allow to download.

  2. The remote path returns Response(content=data, ...), so the complete file is buffered in Gateway memory instead of being streamed.

There also appears to be a sensitive-path symlink case worth a regression test: read_bytes() follows regular-file symlinks, does not canonicalize the remote target, and _is_sensitive_path() evaluates the lexical path returned by the adapter. A benignly named symlink pointing to a sensitive target may therefore avoid the sensitive-name/path-component check.

Suggested focused tests:

  • sensitive remote download denied before content is read;
  • benign-name symlink to a sensitive remote target denied;
  • remote download not constrained by the text-preview/source cap;
  • larger remote download preserves streaming behavior.

The asyncio.to_thread(_fs_backend, profile) acquisition itself looks good.

@nateEc

nateEc commented Sep 4, 2026

Copy link
Copy Markdown
Author

Addressed in 08e8322f1d. SSH downloads now resolve the remote regular-file target before applying the sensitive-path policy, so a benignly named symlink resolving to a sensitive target is denied before content transfer. Authorized downloads are streamed in chunks rather than buffered or constrained by the text-preview source cap.

Tests: python -m pytest tests/hermes_cli/test_web_server_fs.py tests/hermes_cli/test_ssh_workspace_fs.py -q (14 passed).

@nateEc
nateEc force-pushed the codex/fix-ssh-workspace-files branch from 08e8322 to b2ddc0e Compare September 7, 2026 03:55
@nateEc

nateEc commented Sep 7, 2026

Copy link
Copy Markdown
Author

Rebased onto current main in b2ddc0ea31 and retained the requested safety boundary in the extracted files router: the SSH download resolves the real remote target before the sensitive-path check, then streams only an authorized file without a preview-size cap. Verified locally: 28 passed, 5 skipped across SSH workspace, files-router, and SSH environment tests.

@alt-glitch alt-glitch removed the sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades label Sep 7, 2026
@OutThisLife
OutThisLife force-pushed the codex/fix-ssh-workspace-files branch from b2ddc0e to 97ee411 Compare September 29, 2026 01:48
@OutThisLife
OutThisLife enabled auto-merge (rebase) September 29, 2026 02:28
Route remote workspace filesystem operations through the extracted files router without starting agent-file synchronization.

Resolve SSH download targets before applying sensitive-path policy and stream authorized files without buffering or preview-size limits.

Tests: python -m pytest tests/hermes_cli/test_web_server_fs.py tests/hermes_cli/test_ssh_workspace_fs.py tests/tools/test_ssh_environment.py -q.

Original implementation authored by fangliquanflq.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend/ssh SSH remote execution comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Desktop file browser / Workspace UI reads Gateway's local path instead of SSH backend's CWD

6 participants