You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Headless server mode and SSH remote environments #245
Inspired by T3 Code's remote architecture (its biggest differentiator): the backend is a small local server, the UI talks over WebSocket, so 't3 serve' runs headless and phones/other machines connect; SSH-launched environments probe a host, install/reuse the server and port-forward it.
Solenta angle: run the Electron main logic (or a trimmed core) headless on a workstation, pair clients via the QR/one-time-token flow from #151, and treat SSH hosts as first-class environments whose repos appear next to local projects. Tailscale Serve integration would make this zero-config on the tailnet.
User-pain research (round 6): two hard-won lessons for the headless/remote path. (1) Cursor's iOS launch is the cautionary tale — the headline remote-control feature failing at launch turned the story into a bug report; one rock-solid path (approve + steer + notify) beats a broad flaky one. (2) The host staying awake is a universal failure point: ship a keep-awake helper or an explicit 'host asleep' state. (3) Local-first is the unmet demand: 'steer the agents on MY machine with MY MCP servers/Docker/worktrees' — cloud-session mobile apps structurally cannot reach this, Solenta can.
Research update (2026-10-02), based on T3 Code source at 54084ae:
T3 Connections attach a client to one environment server. That host owns projects, threads, worktrees, provider processes, terminals, credentials, and durable state. Direct pairing, SSH forwarding, Tailscale/direct HTTPS, and T3 Connect all reach the same authenticated HTTP/WebSocket RPC server. Connection identity is the environment UUID, not the URL/port; reconnect validates identity and refreshes subscriptions. SSH can install/reuse a pinned remote server and forward its loopback port. T3 Connect brokers discovery/bootstrap rather than proxying normal app traffic. Sources: remote architecture, connection runtime, SSH tunnel.
Solenta's --serve-web already exposes the live Store/Runner/IPCs via HTTP + WebSocket and keeps the host process alive after its window closes. A loopback server on the work machine plus SSH local forwarding is the shortest path to prove remote execution with remote worktrees. Current SSH project mode only wraps selected commands and leaves Store/UI/terminal semantics local, so it should stay distinct; #1402 tracks its shared-checkout worker defect.
Started the native Connections implementation on branch coder/native-remote-connections (commit 8a4791dc). Settings → Connections now saves SSH host profiles, opens a loopback SSH forward to the host's existing --serve-web port, verifies the WebSocket token before displaying the host, and loads the remote UI in an isolated Electron window. The token is entered per connection and is not saved with the profile. SSH exit closes the remote window with an explanation. The existing local desktop stays separate, so its thread cache cannot mix with the remote host.
Verification: production npm run build; 73 focused renderer tests; 20 existing web-server tests; 2 new SSH connection tests including wrong-token rejection; all pass. This is a manual-connect first slice, not completion of #245: stable host identity, credential storage, automatic reconnect, and unattended host startup remain open.
Follow-up on draft PR #1408: added the SSH connection test to the Windows Electron test list (9a84bef2). The latest CI run 37033320972 is green across all six macOS, Ubuntu, and Windows smoke/test jobs. #245 remains in progress for host identity, reconnect, credential storage, and unattended host work.
Credential storage slice pushed to draft PR #1408 (32014f94). After a token is verified, it is saved per host:port with Electron safeStorage (OS keychain) in userData/remote-connections.json. The renderer keeps only a tokenSaved hint. A blank token reuses the saved one. A saved token the host rejects is deleted and the user is asked for the current one. Forget removes the saved token. Linux basic_text (plaintext) storage is refused. There is a "Remember token" checkbox, on by default.
Verification: typecheck clean. Renderer 2641/2641. Electron 4080/4081: the one failure is fetchCodex process lifecycle in provider-usage.test.js, which passes 22/22 in isolation with and without this change, so it is a load flake. Remaining for #245: stable host identity, automatic reconnect, unattended host startup.
Reconnect slice pushed to draft PR #1408 (aabec290). An unexpected SSH exit no longer closes the remote window. The tunnel is respawned on the same loopback port with backoff (1/2/4/8/15/30 s, about a minute). The window origin stays the same, so the page's wire client reconnects its socket and resyncs threads on its own (src/wireClient.ts scheduleReconnect). The window title shows "Reconnecting…" meanwhile, and the remote page can no longer overwrite the title. A rejected token or a non-Solenta port stops retrying immediately. Giving up closes the window with the last SSH error, as before.
Test: tunnel drop → respawn on the same port with the window kept open; unreachable host → 6 retries, then close plus a dialog showing the SSH stderr. A mutation check with retries disabled fails the test. Electron suite 4080/4081: the one failure is the unrelated load flake fetchCodex process lifecycle (missing pid file under load), offered as separate work.
Remaining for #245: stable host identity (pinning which host a saved profile/token belongs to) and unattended host startup (launching --serve-web over SSH when nothing is listening).
Host identity slice pushed to draft PR #1408 (32723c37). SSH known_hosts is the pin: Connections now passes -o StrictHostKeyChecking=yes (command line overrides a relaxed ~/.ssh/config), so an unknown or changed host key refuses the tunnel before the web token is sent. Raw ssh stderr is replaced with instructions for the three cases BatchMode cannot prompt for: unknown host (run ssh host once in a terminal), changed key (verify and update known_hosts), and password-only login (key-based login required). Real messages were captured from OpenSSH and used in the tests. CI is green on the token (32014f94) and reconnect (aabec290) commits.
Deliberately not built: a protocol-level host id or challenge-response. The SSH host key already identifies the machine. The remaining exposure is another local process on the remote host squatting on port 4620 and receiving the token, which is a multi-user-host concern. HMAC challenge-response in webBridge would close it if it ever matters.
Left for #245: unattended host startup (launch --serve-web over SSH when nothing is listening). That needs a decision on what is installed remotely and how the token is minted and returned.
Final slice pushed to PR #1408 (844cda63), per the decisions on this thread: read the token over SSH, never auto-start.
A blank token reads userData/web-token on the host over SSH: ~/Library/Application Support/Solenta/web-token, then ${XDG_CONFIG_HOME:-~/.config}/Solenta/web-token, under sh -c so the login shell does not matter. Both release channels share productName "Solenta". The keychain copy is the fallback. SSH refusals surface before any tunnel is spawned.
When nothing listens on the remote port, ssh's open failed: connect failed now fails fast with "Nothing is listening on port N on host. Start Solenta there with --serve-web[=N]". Connect never launches the host.
The PR description now covers the whole feature. Once #1408 merges, #245 is done as scoped. A Tailscale Serve zero-config path and a headless (non-Electron) host binary were never part of these slices; file them separately if wanted.
Shipped in #1408 (squash 99394430 on main). Settings → Connections opens a remote --serve-web host through SSH in an isolated window. A blank token is read from the host over SSH, with a keychain copy as fallback. Host keys are pinned (StrictHostKeyChecking=yes). Dropped tunnels reconnect behind a front port this process holds. The token only reaches ports ssh has bound. Nothing listening on the remote port fails fast; Connect never auto-starts the host, by decision. Verified by CI on macOS, Ubuntu and Windows, and end to end against real OpenSSH. Not in scope and not filed: Tailscale Serve zero-config, and a headless non-Electron host binary.
Activity
currentbits commentedon Aug 17, 2026
User-pain research (round 6): two hard-won lessons for the headless/remote path. (1) Cursor's iOS launch is the cautionary tale — the headline remote-control feature failing at launch turned the story into a bug report; one rock-solid path (approve + steer + notify) beats a broad flaky one. (2) The host staying awake is a universal failure point: ship a keep-awake helper or an explicit 'host asleep' state. (3) Local-first is the unmet demand: 'steer the agents on MY machine with MY MCP servers/Docker/worktrees' — cloud-session mobile apps structurally cannot reach this, Solenta can.
currentbits commentedon Oct 2, 2026
Research update (2026-10-02), based on T3 Code source at 54084ae:
T3 Connections attach a client to one environment server. That host owns projects, threads, worktrees, provider processes, terminals, credentials, and durable state. Direct pairing, SSH forwarding, Tailscale/direct HTTPS, and T3 Connect all reach the same authenticated HTTP/WebSocket RPC server. Connection identity is the environment UUID, not the URL/port; reconnect validates identity and refreshes subscriptions. SSH can install/reuse a pinned remote server and forward its loopback port. T3 Connect brokers discovery/bootstrap rather than proxying normal app traffic. Sources: remote architecture, connection runtime, SSH tunnel.
Solenta's
--serve-webalready exposes the live Store/Runner/IPCs via HTTP + WebSocket and keeps the host process alive after its window closes. A loopback server on the work machine plus SSH local forwarding is the shortest path to prove remote execution with remote worktrees. Current SSH project mode only wraps selected commands and leaves Store/UI/terminal semantics local, so it should stay distinct; #1402 tracks its shared-checkout worker defect.Recommended sequence:
--serve-webon loopback, SSH forward, viewer browser; verify agent run, isolated worker fork, permission response, artifact, reconnect, and host-side process/worktree ownership.createWireCoderselection, host-scoped caches and selection. No mutation replay on uncertain disconnect.Full local research report:
reports/T3 Code remote connections.mdin this checkout. No live two-machine trial has been run yet.currentbits commentedon Oct 2, 2026
Started the native Connections implementation on branch
coder/native-remote-connections(commit8a4791dc). Settings → Connections now saves SSH host profiles, opens a loopback SSH forward to the host's existing--serve-webport, verifies the WebSocket token before displaying the host, and loads the remote UI in an isolated Electron window. The token is entered per connection and is not saved with the profile. SSH exit closes the remote window with an explanation. The existing local desktop stays separate, so its thread cache cannot mix with the remote host.Verification: production
npm run build; 73 focused renderer tests; 20 existing web-server tests; 2 new SSH connection tests including wrong-token rejection; all pass. This is a manual-connect first slice, not completion of #245: stable host identity, credential storage, automatic reconnect, and unattended host startup remain open.currentbits commentedon Oct 2, 2026
Draft PR for the manual SSH Connections slice: #1408
currentbits commentedon Oct 2, 2026
Follow-up on draft PR #1408: added the SSH connection test to the Windows Electron test list (
9a84bef2). The latest CI run 37033320972 is green across all six macOS, Ubuntu, and Windows smoke/test jobs. #245 remains in progress for host identity, reconnect, credential storage, and unattended host work.currentbits commentedon Oct 3, 2026
Credential storage slice pushed to draft PR #1408 (
32014f94). After a token is verified, it is saved perhost:portwith ElectronsafeStorage(OS keychain) inuserData/remote-connections.json. The renderer keeps only atokenSavedhint. A blank token reuses the saved one. A saved token the host rejects is deleted and the user is asked for the current one. Forget removes the saved token. Linuxbasic_text(plaintext) storage is refused. There is a "Remember token" checkbox, on by default.Verification: typecheck clean. Renderer 2641/2641. Electron 4080/4081: the one failure is
fetchCodex process lifecyclein provider-usage.test.js, which passes 22/22 in isolation with and without this change, so it is a load flake. Remaining for #245: stable host identity, automatic reconnect, unattended host startup.currentbits commentedon Oct 3, 2026
Reconnect slice pushed to draft PR #1408 (
aabec290). An unexpected SSH exit no longer closes the remote window. The tunnel is respawned on the same loopback port with backoff (1/2/4/8/15/30 s, about a minute). The window origin stays the same, so the page's wire client reconnects its socket and resyncs threads on its own (src/wireClient.ts scheduleReconnect). The window title shows "Reconnecting…" meanwhile, and the remote page can no longer overwrite the title. A rejected token or a non-Solenta port stops retrying immediately. Giving up closes the window with the last SSH error, as before.Test: tunnel drop → respawn on the same port with the window kept open; unreachable host → 6 retries, then close plus a dialog showing the SSH stderr. A mutation check with retries disabled fails the test. Electron suite 4080/4081: the one failure is the unrelated load flake
fetchCodex process lifecycle(missing pid file under load), offered as separate work.Remaining for #245: stable host identity (pinning which host a saved profile/token belongs to) and unattended host startup (launching
--serve-webover SSH when nothing is listening).currentbits commentedon Oct 3, 2026
Host identity slice pushed to draft PR #1408 (
32723c37). SSH known_hosts is the pin: Connections now passes-o StrictHostKeyChecking=yes(command line overrides a relaxed~/.ssh/config), so an unknown or changed host key refuses the tunnel before the web token is sent. Raw ssh stderr is replaced with instructions for the three cases BatchMode cannot prompt for: unknown host (runssh hostonce in a terminal), changed key (verify and update known_hosts), and password-only login (key-based login required). Real messages were captured from OpenSSH and used in the tests. CI is green on the token (32014f94) and reconnect (aabec290) commits.Deliberately not built: a protocol-level host id or challenge-response. The SSH host key already identifies the machine. The remaining exposure is another local process on the remote host squatting on port 4620 and receiving the token, which is a multi-user-host concern. HMAC challenge-response in webBridge would close it if it ever matters.
Left for #245: unattended host startup (launch
--serve-webover SSH when nothing is listening). That needs a decision on what is installed remotely and how the token is minted and returned.currentbits commentedon Oct 3, 2026
Final slice pushed to PR #1408 (
844cda63), per the decisions on this thread: read the token over SSH, never auto-start.userData/web-tokenon the host over SSH:~/Library/Application Support/Solenta/web-token, then${XDG_CONFIG_HOME:-~/.config}/Solenta/web-token, undersh -cso the login shell does not matter. Both release channels share productName "Solenta". The keychain copy is the fallback. SSH refusals surface before any tunnel is spawned.open failed: connect failednow fails fast with "Nothing is listening on port N on host. Start Solenta there with --serve-web[=N]". Connect never launches the host.The PR description now covers the whole feature. Once #1408 merges, #245 is done as scoped. A Tailscale Serve zero-config path and a headless (non-Electron) host binary were never part of these slices; file them separately if wanted.
currentbits commentedon Oct 3, 2026
Shipped in #1408 (squash
99394430on main). Settings → Connections opens a remote--serve-webhost through SSH in an isolated window. A blank token is read from the host over SSH, with a keychain copy as fallback. Host keys are pinned (StrictHostKeyChecking=yes). Dropped tunnels reconnect behind a front port this process holds. The token only reaches ports ssh has bound. Nothing listening on the remote port fails fast; Connect never auto-starts the host, by decision. Verified by CI on macOS, Ubuntu and Windows, and end to end against real OpenSSH. Not in scope and not filed: Tailscale Serve zero-config, and a headless non-Electron host binary.Merge pull request #1515 from currentbits/coder/1512-i2-web-access-ux…