mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
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.
This commit is contained in:
parent
ae2175a584
commit
a6113b4229
2 changed files with 114 additions and 0 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue