From a6113b42293ac242cd6d938ba54db65ea723a04e Mon Sep 17 00:00:00 2001 From: yoniebans Date: Wed, 15 Jul 2026 16:13:12 +0200 Subject: [PATCH] feat(desktop): add SSH to the Cloud-aware connection model Add SSH as a separate saved connection shape while preserving Cloud URL/OAuth semantics, inactive SSH drafts, strict host/port normalization, and profile-specific precedence. --- .../electron/connection-config.test.ts | 50 +++++++++++++++ apps/desktop/electron/connection-config.ts | 64 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/apps/desktop/electron/connection-config.test.ts b/apps/desktop/electron/connection-config.test.ts index e28f6959dab2..e3b682485820 100644 --- a/apps/desktop/electron/connection-config.test.ts +++ b/apps/desktop/electron/connection-config.test.ts @@ -25,9 +25,14 @@ import { cookiesHaveSession, modeIsRemoteLike, normalizeRemoteBaseUrl, + normalizeSshConfig, + localProfileEntry, normAuthMode, pathWithGlobalRemoteProfile, + profileHasRemoteConnection, profileRemoteOverride, + profileSshOverride, + savedProfileSsh, resolveAuthMode, resolveTestWsUrl, RT_COOKIE_VARIANTS, @@ -123,6 +128,51 @@ test('profileRemoteOverride tolerates a missing/!object profiles map', () => { assert.equal(profileRemoteOverride(null, 'coder'), null) }) +test('SSH remains separate from URL-shaped remote modes', () => { + assert.equal(modeIsRemoteLike('ssh'), false) + const config = { profiles: { coder: { mode: 'ssh', host: 'alice@box:2222', keyPath: '/key' } } } + assert.equal(profileRemoteOverride(config, 'coder'), null) + assert.deepEqual(profileSshOverride(config, 'coder'), { + mode: 'ssh', host: 'box', user: 'alice', port: 2222, keyPath: '/key' + }) +}) + +test('normalizeSshConfig handles IPv6 and strict port bounds', () => { + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: '::1', port: 22 }), { + mode: 'ssh', host: '::1' + }) + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: '[::1]:2222' }), { + mode: 'ssh', host: '::1', port: 2222 + }) + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'box', port: '2222junk' }), { + mode: 'ssh', host: 'box' + }) + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'box', port: 65536 }), { + mode: 'ssh', host: 'box' + }) +}) + +test('localProfileEntry preserves inactive SSH drafts but drops Cloud state', () => { + const ssh = { mode: 'ssh', host: 'box', user: 'alice', remoteHermesPath: '/hermes' } + assert.deepEqual(localProfileEntry(ssh), { mode: 'local', savedSsh: ssh }) + assert.deepEqual(localProfileEntry({ mode: 'local', savedSsh: ssh }), { + mode: 'local', savedSsh: ssh + }) + assert.equal(localProfileEntry({ mode: 'cloud', url: 'https://agent' }), null) +}) + +test('saved SSH drafts are inactive and explicit overrides take precedence', () => { + const saved = { mode: 'ssh', host: 'saved' } + const config: any = { profiles: { coder: { mode: 'local', savedSsh: saved } } } + assert.deepEqual(savedProfileSsh(config, 'coder'), saved) + assert.equal(profileSshOverride(config, 'coder'), null) + assert.equal(profileHasRemoteConnection(config, 'coder'), false) + + config.profiles.coder = { mode: 'ssh', host: 'active' } + assert.deepEqual(profileSshOverride(config, 'coder'), { mode: 'ssh', host: 'active' }) + assert.equal(profileHasRemoteConnection(config, 'coder'), true) +}) + // --- pathWithGlobalRemoteProfile --- test('pathWithGlobalRemoteProfile appends profile in global remote mode', () => { diff --git a/apps/desktop/electron/connection-config.ts b/apps/desktop/electron/connection-config.ts index 569f9cc07261..8f4a29bbc494 100644 --- a/apps/desktop/electron/connection-config.ts +++ b/apps/desktop/electron/connection-config.ts @@ -170,6 +170,65 @@ function modeIsRemoteLike(mode) { return mode === 'remote' || mode === 'cloud' } +function normalizeSshConfig(entry) { + if (!entry || typeof entry !== 'object' || entry.mode !== 'ssh') return null + let host = String(entry.host || '').trim() + if (!host) return null + let parsedUser + let parsedPort + const at = host.indexOf('@') + if (at > 0) { + parsedUser = host.slice(0, at) + host = host.slice(at + 1) + } + const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(host) + if (bracketed) { + host = bracketed[1] + if (bracketed[2]) parsedPort = Number(bracketed[2]) + } else if ((host.match(/:/g) || []).length === 1) { + const [name, rawPort] = host.split(':') + if (/^\d+$/.test(rawPort)) { + host = name + parsedPort = Number(rawPort) + } + } + if (!host) return null + const out: any = { mode: 'ssh', host } + const user = String(entry.user || '').trim() || parsedUser || '' + if (user) out.user = user + const rawExplicitPort = String(entry.port ?? '').trim() + const explicitPort = /^\d+$/.test(rawExplicitPort) ? Number(rawExplicitPort) : null + const port = explicitPort ?? parsedPort + if (Number.isInteger(port) && port > 0 && port <= 65535 && port !== 22) out.port = port + const keyPath = String(entry.keyPath || '').trim() + if (keyPath) out.keyPath = keyPath + const remoteHermesPath = String(entry.remoteHermesPath || '').trim() + if (remoteHermesPath) out.remoteHermesPath = remoteHermesPath + return out +} + +function profileSshOverride(config, profile) { + const key = connectionScopeKey(profile) + const entry = key ? config?.profiles?.[key] : null + return normalizeSshConfig(entry) +} + +function savedProfileSsh(config, profile) { + const key = connectionScopeKey(profile) + const entry = key ? config?.profiles?.[key] : null + if (!entry || entry.mode !== 'local') return null + return normalizeSshConfig(entry.savedSsh) +} + +function profileHasRemoteConnection(config, profile) { + return Boolean(profileRemoteOverride(config, profile) || profileSshOverride(config, profile)) +} + +function localProfileEntry(existing) { + const ssh = normalizeSshConfig(existing) || normalizeSshConfig(existing?.savedSsh) + return ssh ? { mode: 'local', savedSsh: ssh } : null +} + /** * Select a profile's explicit remote override from a connection config, or null * when it has none (so the caller falls back to env → global remote → local). @@ -339,10 +398,15 @@ export { cookiesHaveSession, modeIsRemoteLike, normalizeRemoteBaseUrl, + normalizeSshConfig, + localProfileEntry, normAuthMode, pathWithGlobalRemoteProfile, PRIVY_SESSION_COOKIE_VARIANTS, + profileHasRemoteConnection, profileRemoteOverride, + profileSshOverride, + savedProfileSsh, resolveAuthMode, resolveTestWsUrl, RT_COOKIE_VARIANTS,