diff --git a/src/crates/core/src/service/remote_ssh/manager.rs b/src/crates/core/src/service/remote_ssh/manager.rs index f66bec53..a83cf5fc 100644 --- a/src/crates/core/src/service/remote_ssh/manager.rs +++ b/src/crates/core/src/service/remote_ssh/manager.rs @@ -704,8 +704,11 @@ impl SSHConnectionManager { username: config.username.clone(), auth_type: match &config.auth { SSHAuthMethod::Password { .. } => crate::service::remote_ssh::types::SavedAuthType::Password, - SSHAuthMethod::PrivateKey { key_path, .. } => crate::service::remote_ssh::types::SavedAuthType::PrivateKey { key_path: key_path.clone() }, - SSHAuthMethod::Agent => crate::service::remote_ssh::types::SavedAuthType::Agent, + SSHAuthMethod::PrivateKey { key_path, .. } => { + crate::service::remote_ssh::types::SavedAuthType::PrivateKey { + key_path: key_path.clone(), + } + } }, default_workspace: config.default_workspace.clone(), last_connected: Some(chrono::Utc::now().timestamp() as u64), @@ -722,7 +725,7 @@ impl SSHConnectionManager { .with_context(|| format!("store ssh password vault for {}", config.id))?; } } - SSHAuthMethod::PrivateKey { .. } | SSHAuthMethod::Agent => { + SSHAuthMethod::PrivateKey { .. } => { self.password_vault.remove(&config.id).await?; } } @@ -816,7 +819,6 @@ impl SSHConnectionManager { log::info!("Successfully decoded private key"); Some(key_pair) } - SSHAuthMethod::Agent => None, }; let ssh_config = Arc::new(russh::client::Config { @@ -927,12 +929,6 @@ impl SSHConnectionManager { return Err(anyhow!("Failed to load private key")); } } - SSHAuthMethod::Agent => { - log::debug!("Using SSH agent authentication - agent auth not supported, returning false"); - // Agent auth is not supported in russh - return false to indicate auth failed - // The caller should try another auth method - false - } }; if !auth_success { diff --git a/src/crates/core/src/service/remote_ssh/types.rs b/src/crates/core/src/service/remote_ssh/types.rs index 51c5a0cd..48c486ba 100644 --- a/src/crates/core/src/service/remote_ssh/types.rs +++ b/src/crates/core/src/service/remote_ssh/types.rs @@ -1,6 +1,6 @@ //! Type definitions for Remote SSH service -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; /// Workspace backend type #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -39,6 +39,7 @@ pub struct SSHConnectionConfig { /// SSH username pub username: String, /// Authentication method + #[serde(deserialize_with = "deserialize_ssh_auth_method")] pub auth: SSHAuthMethod, /// Default remote working directory #[serde(rename = "defaultWorkspace")] @@ -61,8 +62,40 @@ pub enum SSHAuthMethod { /// Optional passphrase for encrypted private key passphrase: Option, }, - /// SSH agent authentication (uses system SSH agent) - Agent, +} + +/// Legacy `{"type":"Agent"}` in saved config maps to default private key path. +fn deserialize_ssh_auth_method<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(tag = "type")] + enum Helper { + Password { + password: String, + }, + PrivateKey { + #[serde(rename = "keyPath")] + key_path: String, + passphrase: Option, + }, + Agent, + } + match Helper::deserialize(deserializer)? { + Helper::Password { password } => Ok(SSHAuthMethod::Password { password }), + Helper::PrivateKey { + key_path, + passphrase, + } => Ok(SSHAuthMethod::PrivateKey { + key_path, + passphrase, + }), + Helper::Agent => Ok(SSHAuthMethod::PrivateKey { + key_path: "~/.ssh/id_rsa".to_string(), + passphrase: None, + }), + } } /// Connection state @@ -87,7 +120,7 @@ pub struct SavedConnection { pub host: String, pub port: u16, pub username: String, - #[serde(rename = "authType")] + #[serde(rename = "authType", deserialize_with = "deserialize_saved_auth_type")] pub auth_type: SavedAuthType, #[serde(rename = "defaultWorkspace")] pub default_workspace: Option, @@ -104,7 +137,29 @@ pub enum SavedAuthType { #[serde(rename = "keyPath")] key_path: String, }, - Agent, +} + +fn deserialize_saved_auth_type<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(tag = "type")] + enum Helper { + Password, + PrivateKey { + #[serde(rename = "keyPath")] + key_path: String, + }, + Agent, + } + match Helper::deserialize(deserializer)? { + Helper::Password => Ok(SavedAuthType::Password), + Helper::PrivateKey { key_path } => Ok(SavedAuthType::PrivateKey { key_path }), + Helper::Agent => Ok(SavedAuthType::PrivateKey { + key_path: "~/.ssh/id_rsa".to_string(), + }), + } } /// Remote file entry information diff --git a/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx b/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx index d4051411..6152e3b5 100644 --- a/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx +++ b/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx @@ -1,5 +1,5 @@ /** - * Unified SSH authentication prompt: password, private key, or SSH agent. + * Unified SSH authentication prompt: password or private key. */ import React, { useState, useEffect, useRef, useCallback } from 'react'; @@ -9,7 +9,7 @@ import { Button } from '@/component-library'; import { Input } from '@/component-library'; import { Select } from '@/component-library'; import { IconButton } from '@/component-library'; -import { FolderOpen, Key, Loader2, Lock, Server, Terminal, User } from 'lucide-react'; +import { FolderOpen, Key, Loader2, Lock, Server, User } from 'lucide-react'; import type { SSHAuthMethod } from './types'; import { pickSshPrivateKeyPath } from './pickSshPrivateKeyPath'; import './SSHAuthPromptDialog.scss'; @@ -24,7 +24,7 @@ interface SSHAuthPromptDialogProps { open: boolean; /** Shown in the header area (e.g. user@host:port or alias) */ targetDescription: string; - defaultAuthMethod: 'password' | 'privateKey' | 'agent'; + defaultAuthMethod: 'password' | 'privateKey'; defaultKeyPath?: string; initialUsername: string; /** If false, user can edit username (e.g. SSH config without User) */ @@ -46,7 +46,7 @@ export const SSHAuthPromptDialog: React.FC = ({ onCancel, }) => { const { t } = useI18n('common'); - const [authMethod, setAuthMethod] = useState<'password' | 'privateKey' | 'agent'>(defaultAuthMethod); + const [authMethod, setAuthMethod] = useState<'password' | 'privateKey'>(defaultAuthMethod); const [username, setUsername] = useState(initialUsername); const [password, setPassword] = useState(''); const [keyPath, setKeyPath] = useState(defaultKeyPath); @@ -71,15 +71,13 @@ export const SSHAuthPromptDialog: React.FC = ({ const authOptions = [ { label: t('ssh.remote.password') || 'Password', value: 'password', icon: }, { label: t('ssh.remote.privateKey') || 'Private Key', value: 'privateKey', icon: }, - { label: t('ssh.remote.sshAgent') || 'SSH Agent', value: 'agent', icon: }, ]; const canSubmit = (): boolean => { const u = username.trim(); if (!u) return false; if (authMethod === 'password') return password.length > 0; - if (authMethod === 'privateKey') return keyPath.trim().length > 0; - return true; + return keyPath.trim().length > 0; }; const handleSubmit = () => { @@ -88,14 +86,12 @@ export const SSHAuthPromptDialog: React.FC = ({ let auth: SSHAuthMethod; if (authMethod === 'password') { auth = { type: 'Password', password }; - } else if (authMethod === 'privateKey') { + } else { auth = { type: 'PrivateKey', keyPath: keyPath.trim(), passphrase: passphrase.trim() || undefined, }; - } else { - auth = { type: 'Agent' }; } onSubmit({ auth, username: u }); }; @@ -154,7 +150,7 @@ export const SSHAuthPromptDialog: React.FC = ({