Skip to content

Remote servers - #4

Open
BowgartField wants to merge 25 commits into
mainfrom
remote-servers
Open

BowgartField wants to merge 25 commits into
mainfrom
remote-servers

Conversation

@BowgartField

@BowgartField BowgartField commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds end-to-end remote server support to Jean. A desktop client can
register and provision a Linux server, connect to it through an SSH tunnel,
clone projects and create worktrees on it, then run Jean chat sessions and
terminals against the remote backend while local work remains available.

Jean reuses its existing headless HTTP/WebSocket command surface:

Desktop Jean
  |-- local work        -> native Tauri IPC
  `-- remote work       -> WebSocket over SSH local-forward
                           -> headless Jean on the server

The remote backend binds to 127.0.0.1 and is only reachable through the
authenticated SSH tunnel.

Completed

Server management and provisioning

  • Add, edit, test, provision, connect, disconnect, and remove remote servers.
  • Support SSH keys, encrypted key passphrases through macOS Keychain, SSH
    config aliases, identity loading, agent forwarding, and password auth.
  • Provision supported Linux distributions with Xvfb/WebKitGTK dependencies,
    the matching signed Jean AppImage, and a managed systemd service.
  • Verify the authenticated health endpoint and require matching Jean versions
    before accepting a connection.
  • Track tunnel processes and clean them up on application shutdown.

Remote transport and routing

  • Maintain one WsTransport per remote server while preserving native IPC for
    local work.
  • Route commands with _backendHandle and retain the originating server on
    remote events.
  • Wait for the WebSocket connection event instead of relying on fixed delays.
  • Route project, worktree, session, chat, terminal, git, file, and related
    operations to the owning backend.
  • Prevent remote events or session operations from leaking to the local
    backend.

Projects, worktrees, and sessions

  • Clone projects onto a selected remote server with SSH agent forwarding.
  • Create remote worktrees and preserve their server ownership in frontend
    caches and persisted UI state.
  • Select local or remote execution when creating work.
  • Create, resume, archive, close, cancel, and message remote chat sessions.
  • Detect Claude CLI installation and authentication independently per server.
  • Open the Claude login flow directly on the selected remote backend.

Recovery and restart behavior

  • Auto-connect provisioned servers when Jean starts.
  • Re-register transports only after the tunnel and WebSocket are ready.
  • Reuse active tunnels without leaving stale runtime status.
  • Refresh remote worktree queries after reconnect so existing worktrees remain
    visible after restarting Jean.
  • Preserve remote session routing and allow existing chats to resume.

How to test

Full walkthrough (with recovery and cleanup) lives in
docs/developer/remote-servers.md.

Automated paths:

  • bun run test:remote-tunnel — Docker SSH transport + tunnel/replay recovery.
  • bun run test:remote-provision — full provisioning path against a Lima Linux
    VM (macOS; needs brew install lima and a debug jean binary).

Manual UI walkthrough (macOS + Lima):

  1. bun run test:remote-provision:spinup — boots the
    jean-remote-provision-test Lima VM and prints its SSH host/port/user/
    identity file.
  2. Start the app (bun run tauri:dev).
  3. Preferences → Remote Servers → Add Server: enter the printed host/port/user,
    choose SSH key auth, point at the printed identity file. The card verifies
    SSH and reports the detected OS/arch.
  4. Provision from the server card (version matches desktop build). Watch the
    step timeline + live logs; success only after /api/auth responds.
  5. On connect the SSH tunnel opens, the remote WebSocket registers, and remote
    projects/CLIs appear on the card.
  6. Clone a repo onto the server using an SSH-style git URL (agent forwarding —
    no key copied to the server).
  7. Create a remote worktree, open a session, send a message; verify send /
    cancel / close / archive / resume all route to the remote backend.
  8. Optional recovery: pkill -f "ssh -N -L" or restart the app — the tunnel
    recreates on a new local port and remote worktrees reappear without being
    recreated.
  9. Clean up: remove the server in the UI, then
    limactl delete --force jean-remote-provision-test.

Validation

  • Manually verified the complete flow on a Linux server:
    provision/connect, remote clone, remote worktree creation, remote Claude
    login, session creation, prompt/response, Jean restart, worktree discovery,
    and chat resume.
  • 33 focused frontend tests pass for transport bootstrap, reconnect cache
    invalidation, remote Claude auth, remote server settings, and chat routing.
  • TypeScript typecheck and ESLint pass.
  • Rust tunnel regression test passes.
  • GitGuardian security check passes.
  • Test fixtures use documentation-only hosts and paths; no machine, server, or
    credential values are committed.

Known constraints and follow-ups

  • Remote provisioning currently targets Linux with systemd and a supported
    package manager.
  • Xvfb remains required because Tauri initializes GTK/WebKitGTK in headless
    mode. A true GUI-free server binary is separate work.
  • SSH passwords are still persisted in local preferences; SSH key auth is
    recommended until an encrypted cross-platform password vault is added.
  • Reverse port-forwarding UI is not included yet.
  • Additional release confidence should cover forced tunnel-drop/replay,
    local-and-remote concurrency, other AI backends, supported Linux
    distributions/architectures, and non-macOS desktop clients.
  • Remote Jean self-update/reprovisioning and multi-user server management are
    outside this PR.

jeanbenoit-richez and others added 14 commits July 11, 2026 14:03
Add multi-backend transport layer so native app can route project-scoped
commands to a remote Jean server over SSH tunnel instead of local Tauri IPC.

- WsTransport accepts baseUrl + fixedToken for remote instances
- Remote transport registry with listen() fan-out to active remotes;
  handlers registered before connect() are retroactively applied to new
  remote transports as they come online
- invoke() routes via _backendHandle arg to the matching remote transport
- listen() fans out to all active remote transports in native mode (covers
  streaming events with no call-site changes)
- registerRemoteTransport / unregisterRemoteTransport called automatically
  on connect/disconnect in useConnectRemoteServer / useDisconnectRemoteServer
- Project.server_id (TS + Rust) marks which remote server owns a project
- useProjectBackendHandle + useBackendHandleForWorktree resolve server_id
  from TanStack Query cache for hooks
- useWorktrees passes _backendHandle when project is remote
- terminal-instances: all invoke calls thread backendHandle from PersistentTerminal
- TerminalView + useTerminal thread backendHandle from worktree context
- SSH agent forwarding (-o ForwardAgent=yes) on exec connections so git
  can authenticate on the remote using the local SSH agent; no credentials
  stored on the server
- RemoteClone type (Rust + TS): tracks server_id + remote_path per project
- clone_project_to_remote command: SSH git clone (or fetch if already cloned),
  idempotent, defaults remote path to ~/jean/<project-name>, registered in
  both native handler and WS dispatch
- useAutoConnectRemoteServers: silent startup hook that connects all
  provisioned servers and calls registerRemoteTransport on each
- Sidebar Local/Remote toggle: shown only when a provisioned server exists;
  Remote view grays out projects not yet cloned on any server
- "Clone to remote" context menu on projects: submenu per connected server,
  toast feedback, invalidates project cache on success
- SessionRemoteBackendPicker: chip button in chat toolbar (hidden when no
  remote available) to choose where the session runs — Local or a server
- resolve_ssh_url_aliases(): runs ssh -G on the git remote host before
  cloning so local ~/.ssh/config Host aliases (e.g. github.com-myaccount)
  are rewritten to real hostnames the remote server can actually resolve
- Add GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=accept-new' so the
  remote server accepts github.com host key on first connect without
  a known_hosts entry
- In-memory passphrase cache in keychain.rs: macOS Keychain is only
  accessed once per app session; subsequent SSH invocations (including
  ControlMaster-multiplexed ones) read from cache with no system prompt
ssh -G <alias> now also extracts IdentityFile from the local SSH config.
Before cloning, ssh-add is called on that file (with --apple-use-keychain
on macOS to load passphrase from Keychain silently). This ensures agent
forwarding carries the right key to the remote, fixing "Permission denied
(publickey)" when the user's SSH config uses a host alias with a custom
identity file that isn't pre-loaded in the agent.
feat(doc): update
@BowgartField

Copy link
Copy Markdown
Owner Author
image image image image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants