diff --git a/CHANGELOG.md b/CHANGELOG.md index b3506cd45..6734e6667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,12 +25,10 @@ ### Fixed -- Write the SSH config to the file the active Remote-SSH extension actually - reads. Windsurf/Devin and Antigravity renamed the whole `remote.SSH` settings - section, so a custom config file set as `remote.devinSSH.configFile`, - `remote.windsurfSSH.configFile`, or `remote.antigravitySSH.configFile` was - ignored and the workspace host was written to `~/.ssh/config` instead, where - those editors never looked for it. +- Ignore the SSH config file setting on Antigravity and Windsurf/Devin. They + launch ssh without pointing it at a config file, so it always reads + `~/.ssh/config`, and honoring the setting wrote the workspace host where the + connection never looked. - Apply a 60-second default timeout to REST requests, so requests hung on a half-open TCP connection don't stall pollers forever. - Change `coder.binarySource`, `coder.binaryDestination`, `coder.headerCommand`, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66a9ef5ec..9939445ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,13 @@ Host coder-vscode.dev.coder.com--* LogLevel ERROR ``` +Which file that entry goes in depends on the Remote - SSH extension. Microsoft's +and Cursor's pass `remote.SSH.configFile` to ssh with `-F`, and VSCodium's parses +the file itself instead of running ssh, so all three connect through it. +Antigravity and Windsurf/Devin renamed the setting but spawn ssh without `-F`, so +ssh reads `~/.ssh/config` regardless; we ignore it there rather than write the +host where the connection never looks. + If any step fails, we show an error message. Once the error message is closed we close the remote so the Remote - SSH connection does not continue to connection. Otherwise, we yield, which lets the Remote - SSH continue. diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 51d1a37a1..3f570b085 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -55,7 +55,7 @@ import { parseCoderSshOptions, parseSshConfig, } from "./sshConfig"; -import { getRemoteSshSetting } from "./sshExtension"; +import { getRemoteSshConfigFile } from "./sshExtension"; import { applySettingOverrides, buildSshOverrides } from "./sshOverrides"; import { SshProcessMonitor } from "./sshProcess"; import { computeSshProperties, sshSupportsSetEnv } from "./sshSupport"; @@ -919,7 +919,7 @@ export class Remote { } private getSshConfigPath(): string { - const configured = getRemoteSshSetting("configFile"); + const configured = getRemoteSshConfigFile(); return expandPath(configured || path.join("~", ".ssh", "config")); } diff --git a/src/remote/sshExtension.ts b/src/remote/sshExtension.ts index a56e98060..f2d80dec4 100644 --- a/src/remote/sshExtension.ts +++ b/src/remote/sshExtension.ts @@ -11,31 +11,25 @@ export const REMOTE_SSH_EXTENSION_IDS = [ export type RemoteSshExtensionId = (typeof REMOTE_SSH_EXTENSION_IDS)[number]; /** - * Sections each extension reads, in order. The rebranded forks renamed the - * whole `remote.SSH` section, so reading it directly misses them. + * Extensions that spawn ssh without `-F`, so it reads ~/.ssh/config whatever + * their renamed setting says. Honoring one would write the workspace host + * where the connection never looks. */ -const SETTING_SECTIONS: Readonly< - Record -> = { - "jeanp413.open-remote-ssh": ["remote.SSH"], - // Windsurf became Devin and reads both, preferring the new name. - "codeium.windsurf-remote-openssh": ["remote.devinSSH", "remote.windsurfSSH"], - "anysphere.remote-ssh": ["remote.SSH"], - "ms-vscode-remote.remote-ssh": ["remote.SSH"], - "google.antigravity-remote-openssh": ["remote.antigravitySSH"], -}; +const IGNORED_CONFIG_FILE: readonly RemoteSshExtensionId[] = [ + "google.antigravity-remote-openssh", + "codeium.windsurf-remote-openssh", +]; -/** First non-empty value for a string setting, e.g. `configFile`. */ -export function getRemoteSshSetting(key: string): string | undefined { +/** The SSH config file the active extension connects through, if configured. */ +export function getRemoteSshConfigFile(): string | undefined { const id = getRemoteSshExtension()?.id; - const sections = id ? SETTING_SECTIONS[id] : ["remote.SSH"]; - for (const section of sections) { - const value = vscode.workspace.getConfiguration(section).get(key); - if (value) { - return value; - } + if (id && IGNORED_CONFIG_FILE.includes(id)) { + return undefined; } - return undefined; + return ( + vscode.workspace.getConfiguration("remote.SSH").get("configFile") || + undefined + ); } /** diff --git a/test/unit/remote/sshExtension.test.ts b/test/unit/remote/sshExtension.test.ts index e3283fb16..a59fc2211 100644 --- a/test/unit/remote/sshExtension.test.ts +++ b/test/unit/remote/sshExtension.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import * as vscode from "vscode"; -import { getRemoteSshSetting } from "@/remote/sshExtension"; +import { getRemoteSshConfigFile } from "@/remote/sshExtension"; import { config, type Settings } from "../../mocks/testHelpers"; @@ -13,47 +13,41 @@ function setup(extensionId: string, settings: Settings = {}): void { ); } -describe("getRemoteSshSetting", () => { +describe("getRemoteSshConfigFile", () => { it.each([ - ["ms-vscode-remote.remote-ssh", "remote.SSH.configFile"], - ["anysphere.remote-ssh", "remote.SSH.configFile"], - ["jeanp413.open-remote-ssh", "remote.SSH.configFile"], - ["google.antigravity-remote-openssh", "remote.antigravitySSH.configFile"], - ["codeium.windsurf-remote-openssh", "remote.devinSSH.configFile"], - ])("reads the section %s uses", (extensionId, settingKey) => { - setup(extensionId, { [settingKey]: "/custom/config" }); + "ms-vscode-remote.remote-ssh", + "anysphere.remote-ssh", + "jeanp413.open-remote-ssh", + ])("reads the configured file for %s", (extensionId) => { + setup(extensionId, { "remote.SSH.configFile": "/custom/config" }); - expect(getRemoteSshSetting("configFile")).toBe("/custom/config"); + expect(getRemoteSshConfigFile()).toBe("/custom/config"); }); - it("falls back to the legacy Windsurf section", () => { - setup("codeium.windsurf-remote-openssh", { - "remote.windsurfSSH.configFile": "/legacy/config", - }); - - expect(getRemoteSshSetting("configFile")).toBe("/legacy/config"); - }); - - it("prefers the Devin section over the legacy Windsurf one", () => { - setup("codeium.windsurf-remote-openssh", { - "remote.devinSSH.configFile": "/devin/config", - "remote.windsurfSSH.configFile": "/legacy/config", - }); - - expect(getRemoteSshSetting("configFile")).toBe("/devin/config"); - }); + it.each([ + ["google.antigravity-remote-openssh", "remote.antigravitySSH.configFile"], + ["codeium.windsurf-remote-openssh", "remote.devinSSH.configFile"], + ])( + "ignores the file %s never connects through", + (extensionId, settingKey) => { + setup(extensionId, { + [settingKey]: "/custom/config", + "remote.SSH.configFile": "/stale/config", + }); + + expect(getRemoteSshConfigFile()).toBeUndefined(); + }, + ); - it("ignores another extension's section", () => { - setup("google.antigravity-remote-openssh", { - "remote.SSH.configFile": "/custom/config", - }); + it("reads remote.SSH when no extension is installed", () => { + setup("", { "remote.SSH.configFile": "/custom/config" }); - expect(getRemoteSshSetting("configFile")).toBeUndefined(); + expect(getRemoteSshConfigFile()).toBe("/custom/config"); }); - it("defaults to remote.SSH when no extension is installed", () => { - setup("", { "remote.SSH.configFile": "/custom/config" }); + it("returns undefined when nothing is configured", () => { + setup("ms-vscode-remote.remote-ssh"); - expect(getRemoteSshSetting("configFile")).toBe("/custom/config"); + expect(getRemoteSshConfigFile()).toBeUndefined(); }); });