1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

Address feedback

This commit is contained in:
Bernd Schoolmann
2025-10-31 11:55:18 +01:00
parent 4a9186c5fc
commit 81771da93b
11 changed files with 215 additions and 186 deletions

View File

@@ -55,20 +55,6 @@ tracing = { workspace = true }
typenum = { workspace = true }
zeroizing-alloc = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
ashpd = { workspace = true }
libc = { workspace = true }
oo7 = { workspace = true }
zbus = { workspace = true, optional = true }
zbus_polkit = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { workspace = true, optional = true }
desktop_objc = { path = "../objc" }
security-framework = { workspace = true, optional = true }
security-framework-sys = { workspace = true, optional = true }
[target.'cfg(windows)'.dependencies]
widestring = { workspace = true, optional = true }
windows = { workspace = true, features = [
@@ -88,5 +74,19 @@ windows-future = { workspace = true }
[target.'cfg(windows)'.dev-dependencies]
keytar = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
ashpd = { workspace = true }
libc = { workspace = true }
oo7 = { workspace = true }
zbus = { workspace = true, optional = true }
zbus_polkit = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { workspace = true, optional = true }
desktop_objc = { path = "../objc" }
security-framework = { workspace = true, optional = true }
security-framework-sys = { workspace = true, optional = true }
[lints]
workspace = true

View File

@@ -1072,6 +1072,8 @@ pub mod sshagent_v2 {
use tokio::{self, sync::Mutex};
use tracing::{error, info};
/// Wrapper struct to hold the SSH agent state. This is exposed via NAPI for which the
/// macro generates all the necessary boilerplate.
#[napi]
pub struct SshAgentState {
agent: BitwardenDesktopAgent,
@@ -1100,105 +1102,118 @@ pub mod sshagent_v2 {
pub namespace: Option<String>,
}
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
#[napi]
pub async fn serve(
pub fn serve(
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::<ssh_agent::agent::ui_requester::UiRequestMessage>(32);
// The size is arbitrary, as the channel responses are expected to be read immediately,
// a smaller or larger buffer would also work.
const BUFFER_SIZE: usize = 32;
let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<
ssh_agent::agent::ui_requester::UiRequestMessage,
>(BUFFER_SIZE);
let (auth_response_tx, auth_response_rx) =
tokio::sync::broadcast::channel::<(u32, bool)>(32);
tokio::sync::broadcast::channel::<(u32, bool)>(BUFFER_SIZE);
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
let ui_requester =
ui_requester::UiRequester::new(auth_request_tx, Arc::new(Mutex::new(auth_response_rx)));
tokio::spawn(async move {
let _ = ui_requester;
while let Some(request) = auth_request_rx.recv().await {
let cloned_response_tx_arc = auth_response_tx_arc.clone();
let cloned_callback = callback.clone();
tokio::spawn(async move {
let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback;
let js_request = match request.clone() {
UiRequestMessage::ListRequest {
request_id: _,
connection_info,
} => SshUIRequest {
cipher_id: None,
is_list: true,
process_name: connection_info.peer_info().process_name().to_string(),
is_forwarding: connection_info.is_forwarding(),
namespace: None,
},
UiRequestMessage::AuthRequest {
request_id: _,
connection_info,
cipher_id,
} => SshUIRequest {
cipher_id: Some(cipher_id),
is_list: false,
process_name: connection_info.peer_info().process_name().to_string(),
is_forwarding: connection_info.is_forwarding(),
namespace: None,
},
UiRequestMessage::SignRequest {
request_id: _,
connection_info,
cipher_id,
namespace,
} => SshUIRequest {
cipher_id: Some(cipher_id),
is_list: false,
process_name: connection_info.peer_info().process_name().to_string(),
is_forwarding: connection_info.is_forwarding(),
namespace: Some(namespace),
},
};
let promise_result: Result<Promise<bool>, napi::Error> =
callback.call_async(Ok(js_request)).await;
match promise_result {
Ok(promise_result) => match promise_result.await {
Ok(result) => {
let _ = auth_response_tx_arc
.lock()
.await
.send((request.id(), result))
.expect("should be able to send auth response to agent");
}
Err(e) => {
error!(error = %e, "Calling UI callback promise was rejected");
let _ = auth_response_tx_arc
.lock()
.await
.send((request.id(), false))
.expect("should be able to send auth response to agent");
}
},
Err(e) => {
error!(error = %e, "Calling UI callback could not create promise");
let _ = auth_response_tx_arc
.lock()
.await
.send((request.id(), false))
.expect("should be able to send auth response to agent");
}
}
});
handle_ui_request(request, auth_response_tx_arc.clone(), callback.clone());
}
});
let agent = BitwardenDesktopAgent::new(ui_requester);
let agent_copy = agent.clone();
PlatformListener::spawn_listeners(agent_copy);
if let Err(e) = PlatformListener::spawn_listeners(agent_copy) {
return Err(napi::Error::from_reason(format!(
"Failed to start SSH Agent platform listeners - Error: {e} - {e:?}"
)));
}
Ok(SshAgentState { agent })
}
fn handle_ui_request(
request_message: UiRequestMessage,
auth_response_tx_arc: Arc<Mutex<tokio::sync::broadcast::Sender<(u32, bool)>>>,
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
) {
tokio::spawn(async move {
let mut ui_request = SshUIRequest {
cipher_id: None,
is_list: false,
process_name: request_message
.connection_info()
.peer_info()
.process_name()
.to_string(),
is_forwarding: request_message.connection_info().is_forwarding(),
namespace: None,
};
let js_request = match request_message.clone() {
UiRequestMessage::ListRequest {
request_id: _,
connection_info: _,
} => {
ui_request.is_list = true;
ui_request
}
UiRequestMessage::AuthRequest {
request_id: _,
connection_info: _,
cipher_id,
} => {
ui_request.cipher_id = Some(cipher_id);
ui_request
}
UiRequestMessage::SignRequest {
request_id: _,
connection_info: _,
cipher_id,
namespace,
} => {
ui_request.cipher_id = Some(cipher_id);
ui_request.namespace = Some(namespace);
ui_request
}
};
let promise_result: Result<Promise<bool>, napi::Error> =
callback.call_async(Ok(js_request)).await;
match promise_result {
Ok(promise_result) => match promise_result.await {
Ok(result) => {
let _ = auth_response_tx_arc
.lock()
.await
.send((request_message.id(), result))
.expect("should be able to send auth response to agent");
}
Err(e) => {
error!(error = %e, "Calling UI callback promise was rejected");
let _ = auth_response_tx_arc
.lock()
.await
.send((request_message.id(), false))
.expect("should be able to send auth response to agent");
}
},
Err(e) => {
error!(error = %e, "Calling UI callback could not create promise");
let _ = auth_response_tx_arc
.lock()
.await
.send((request_message.id(), false))
.expect("should be able to send auth response to agent");
}
}
});
}
#[napi]
pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> {
info!("Stopping SSH Agent");

View File

@@ -5,15 +5,21 @@ use tokio_util::sync::CancellationToken;
use crate::{
agent::ui_requester::UiRequester,
memory::UnlockedSshItem,
protocol::{self, agent_listener::serve_listener, key_store::Agent, types::PublicKeyWithName},
memory::{KeyStore, UnlockedSshItem},
protocol::{
self,
agent_listener::serve_listener,
key_store::Agent,
requests::{ParsedSignRequest, SshSignRequest},
types::PublicKeyWithName,
},
transport::peer_info::PeerInfo,
};
#[derive(Clone)]
pub struct BitwardenDesktopAgent {
cancellation_token: CancellationToken,
key_store: Arc<Mutex<crate::memory::KeyStore>>,
key_store: Arc<Mutex<KeyStore>>,
ui_requester: UiRequester,
}
@@ -21,7 +27,7 @@ impl BitwardenDesktopAgent {
pub fn new(ui_requester: UiRequester) -> Self {
Self {
cancellation_token: CancellationToken::new(),
key_store: Arc::new(Mutex::new(crate::memory::KeyStore::new())),
key_store: Arc::new(Mutex::new(KeyStore::new())),
ui_requester,
}
}
@@ -31,10 +37,7 @@ impl BitwardenDesktopAgent {
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Sync + Unpin + 'static,
L: Stream<Item = tokio::io::Result<(S, PeerInfo)>> + Unpin,
{
let err = serve_listener(listener, self.cancellation_token.clone(), self).await;
if let Err(e) = err {
tracing::error!("Error in agent listener: {e}");
}
serve_listener(listener, self.cancellation_token.clone(), self).await;
}
pub fn stop(&self) {
@@ -58,10 +61,6 @@ impl BitwardenDesktopAgent {
pub fn is_running(&self) -> bool {
!self.cancellation_token.is_cancelled()
}
pub fn cancellation_token(&self) -> CancellationToken {
self.cancellation_token.clone()
}
}
impl Agent for &BitwardenDesktopAgent {
@@ -95,6 +94,7 @@ impl Agent for &BitwardenDesktopAgent {
&self,
public_key: &protocol::types::PublicKey,
connection_info: &protocol::connection::ConnectionInfo,
sign_request: &ParsedSignRequest,
) -> Result<bool, anyhow::Error> {
let id = self
.key_store
@@ -102,12 +102,16 @@ impl Agent for &BitwardenDesktopAgent {
.expect("Failed to lock key store")
.get_cipher_id(public_key);
if let Some(cipher_id) = id {
let namespace = match &sign_request {
ParsedSignRequest::SshSigRequest { namespace } => Some(namespace.clone()),
ParsedSignRequest::SignRequest {} => None,
};
return Ok(self
.ui_requester
.request_sign(connection_info, cipher_id, "unknown".to_string())
.request_sign(connection_info, cipher_id, namespace)
.await);
} else {
Ok(false)
}
Ok(false)
}
}

View File

@@ -1,89 +1,76 @@
use crate::agent::BitwardenDesktopAgent;
#[cfg(any(target_os = "linux", target_os = "macos"))]
const SSH_AGENT_SOCK_NAME: &str = ".bitwarden-ssh-agent.sock";
#[cfg(target_os = "linux")]
const FLATPAK_SSH_AGENT_SOCK_NAME: &str =
".var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock";
#[cfg(target_os = "windows")]
use crate::transport::named_pipe_listener_stream::NamedPipeServerStream;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::transport::unix_listener_stream::UnixListenerStream;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use homedir::my_home;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use tracing::info;
const WINDOWS_NAMED_PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent";
pub struct PlatformListener {}
impl PlatformListener {
/// Spawns all listeners for the current platform. A platform may have a single listener, or multiple.
pub fn spawn_listeners(agent: BitwardenDesktopAgent) {
#[cfg(target_os = "linux")]
{
Self::spawn_linux_listeners(agent);
}
#[cfg(target_os = "macos")]
{
Self::spawn_macos_listeners(agent);
}
#[cfg(target_os = "windows")]
{
Self::spawn_windows_listeners(agent);
}
}
#[cfg(target_os = "linux")]
fn spawn_linux_listeners(agent: BitwardenDesktopAgent) {
let ssh_agent_directory = match my_home() {
Ok(Some(home)) => home,
_ => {
info!("Could not determine home directory");
return;
}
pub fn spawn_listeners(agent: BitwardenDesktopAgent) -> Result<(), anyhow::Error> {
use crate::transport::unix_listener_stream::UnixListenerStream;
use homedir::my_home;
let ssh_agent_directory = if let Ok(Some(home)) = my_home() {
home
} else {
return Err(anyhow::anyhow!("Could not determine home directory"));
};
let is_flatpak = std::env::var("container") == Ok("flatpak".to_string());
let path = if !is_flatpak {
ssh_agent_directory
.join(".bitwarden-ssh-agent.sock")
.join(SSH_AGENT_SOCK_NAME)
.to_str()
.expect("Path should be valid")
.to_owned()
} else {
ssh_agent_directory
.join(".var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock")
.join(FLATPAK_SSH_AGENT_SOCK_NAME)
.to_str()
.expect("Path should be valid")
.to_owned()
};
tokio::spawn(UnixListenerStream::listen(path, agent));
Ok(())
}
#[cfg(target_os = "macos")]
fn spawn_macos_listeners(agent: BitwardenDesktopAgent) {
let ssh_agent_directory = match my_home() {
Ok(Some(home)) => home,
_ => {
info!("Could not determine home directory");
return;
}
pub fn spawn_listeners(agent: BitwardenDesktopAgent) -> Result<(), anyhow::Error> {
use crate::transport::unix_listener_stream::UnixListenerStream;
use homedir::my_home;
let ssh_agent_directory = if let Ok(Some(home)) = my_home() {
home
} else {
return Err(anyhow::anyhow!("Could not determine home directory"));
};
let path = ssh_agent_directory
.join(".bitwarden-ssh-agent.sock")
.join(SSH_AGENT_SOCK_NAME)
.to_str()
.expect("Path should be valid")
.to_owned();
tokio::spawn(UnixListenerStream::listen(path, agent));
Ok(())
}
#[cfg(target_os = "windows")]
pub fn spawn_windows_listeners(agent: BitwardenDesktopAgent) {
pub fn spawn_listeners(agent: BitwardenDesktopAgent) -> Result<(), anyhow::Error> {
use crate::transport::named_pipe_listener_stream::NamedPipeServerStream;
tokio::spawn(async move {
// Windows by default uses the named pipe \\.\pipe\openssh-ssh-agent. It also supports external SSH auth sock variables, which are
// not supported here. Windows also supports putty (not implemented here) and unix sockets for WSL (not implemented here).
const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent";
tokio::spawn(NamedPipeServerStream::listen(PIPE_NAME.to_string(), agent));
tokio::spawn(NamedPipeServerStream::listen(
WINDOWS_NAMED_PIPE_NAME.to_string(),
agent,
));
});
Ok(())
}
}

View File

@@ -4,7 +4,8 @@ use tokio::sync::Mutex;
use crate::protocol::connection::ConnectionInfo;
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
/// Determines how long to wait for a UI response before timing out.
const SSH_UI_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
static REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
@@ -59,7 +60,7 @@ impl UiRequester {
&self,
connection_info: &ConnectionInfo,
cipher_id: String,
namespace: String,
namespace: Option<String>,
) -> bool {
let request_id = REQUEST_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.request(UiRequestMessage::SignRequest {
@@ -78,7 +79,7 @@ impl UiRequester {
.await
.expect("Should send request to ui");
tokio::time::timeout(TIMEOUT, async move {
tokio::time::timeout(SSH_UI_REQUEST_TIMEOUT, async move {
while let Ok((id, response)) = rx_channel.recv().await {
if id == request.id() {
return response;
@@ -106,16 +107,30 @@ pub enum UiRequestMessage {
request_id: u32,
connection_info: ConnectionInfo,
cipher_id: String,
namespace: String,
namespace: Option<String>,
},
}
impl UiRequestMessage {
pub fn connection_info(&self) -> &ConnectionInfo {
match self {
UiRequestMessage::ListRequest {
connection_info, ..
}
| UiRequestMessage::AuthRequest {
connection_info, ..
}
| UiRequestMessage::SignRequest {
connection_info, ..
} => connection_info,
}
}
pub fn id(&self) -> u32 {
match self {
UiRequestMessage::ListRequest { request_id, .. } => *request_id,
UiRequestMessage::AuthRequest { request_id, .. } => *request_id,
UiRequestMessage::SignRequest { request_id, .. } => *request_id,
UiRequestMessage::ListRequest { request_id, .. }
| UiRequestMessage::AuthRequest { request_id, .. }
| UiRequestMessage::SignRequest { request_id, .. } => *request_id,
}
}
}

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use tracing::info;
use tracing::warn;
use crate::protocol::types::PublicKey;
@@ -75,7 +75,7 @@ impl KnownHostsReader {
// Split by the first space
let first_space_index = line.find(' ');
let Some(first_space_index) = first_space_index else {
info!("Invalid known_hosts line (no spaces): {}", line);
warn!("Invalid known_hosts line (no spaces): {}", line);
continue;
};
let (hostnames, rest) = line.split_at(first_space_index);

View File

@@ -24,8 +24,7 @@ pub async fn serve_listener<PeerStream, Listener>(
mut listener: Listener,
cancellation_token: CancellationToken,
agent: impl Agent,
) -> Result<(), anyhow::Error>
where
) where
PeerStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
Listener: Stream<Item = tokio::io::Result<(PeerStream, PeerInfo)>> + Unpin,
{
@@ -44,7 +43,6 @@ where
}
}
}
Ok(())
}
async fn handle_connection(
@@ -90,7 +88,11 @@ async fn handle_connection(
span.in_scope(|| info!("Received SignRequest {:?}", sign_request));
let Ok(true) = agent
.request_can_sign(sign_request.public_key(), connection)
.request_can_sign(
sign_request.public_key(),
connection,
sign_request.parsed_payload(),
)
.await
else {
span.in_scope(|| error!("Sign request denied by UI"));

View File

@@ -2,6 +2,7 @@ use crate::{
memory::UnlockedSshItem,
protocol::{
connection::ConnectionInfo,
requests::ParsedSignRequest,
types::{PublicKey, PublicKeyWithName},
},
};
@@ -16,6 +17,7 @@ pub(crate) trait Agent: Send + Sync {
&self,
public_key: &PublicKey,
connection_info: &ConnectionInfo,
parsed_sign_request: &ParsedSignRequest,
) -> Result<bool, anyhow::Error>;
async fn find_ssh_item(
&self,

View File

@@ -22,7 +22,7 @@ pub enum ReplyType {
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-requesting-a-list-of-keys`
/// Response to `RequestType::SSH_AGENTC_REQUEST_IDENTITIES`
SSH_AGENT_IDENTITIES_ANSWER = 12,
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-private-key-operations``
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-private-key-operations`
/// Response to `RequestType::SSH_AGENTC_SIGN_REQUEST`
SSH_AGENT_SIGN_RESPONSE = 14,
/// Invalid reply type
@@ -39,12 +39,12 @@ pub struct ReplyFrame {
}
impl ReplyFrame {
pub fn new(reply: ReplyType, payload: Vec<u8>) -> Self {
pub fn new(reply: ReplyType, payload: &[u8]) -> Self {
let mut raw_frame = Vec::new();
Into::<u8>::into(reply)
.encode(&mut raw_frame)
.expect("Encoding into Vec cannot fail");
raw_frame.extend_from_slice(&payload);
raw_frame.extend_from_slice(payload);
Self { raw_frame }
}
}
@@ -75,14 +75,15 @@ impl IdentitiesReply {
/// ... (nkeys times)
/// ]
pub fn encode(&self) -> Result<ReplyFrame, ssh_encoding::Error> {
let mut reply_message = Vec::new();
Ok(ReplyFrame::new(ReplyType::SSH_AGENT_IDENTITIES_ANSWER, {
let mut reply_message = Vec::new();
reply_message.clear();
(self.keys.len() as u32).encode(&mut reply_message)?;
for key in &self.keys {
key.key.encode(&mut reply_message)?;
key.name.encode(&mut reply_message)?;
}
reply_message
&reply_message
}))
}
}
@@ -107,10 +108,10 @@ impl SshSignReply {
/// byte SSH_AGENT_SIGN_RESPONSE
/// string signature blob
pub fn encode(&self) -> Result<ReplyFrame, ssh_encoding::Error> {
let mut reply_payload = Vec::new();
Ok(ReplyFrame::new(ReplyType::SSH_AGENT_SIGN_RESPONSE, {
let mut reply_payload = Vec::new();
self.0.encode()?.encode(&mut reply_payload)?;
reply_payload
&reply_payload
}))
}
}
@@ -124,7 +125,7 @@ impl AgentExtensionFailure {
impl From<AgentExtensionFailure> for ReplyFrame {
fn from(_value: AgentExtensionFailure) -> Self {
ReplyFrame::new(ReplyType::SSH_AGENT_EXTENSION_FAILURE, Vec::new())
ReplyFrame::new(ReplyType::SSH_AGENT_EXTENSION_FAILURE, &[])
}
}
@@ -137,7 +138,7 @@ impl AgentFailure {
impl From<AgentFailure> for ReplyFrame {
fn from(_value: AgentFailure) -> Self {
ReplyFrame::new(ReplyType::SSH_AGENT_FAILURE, Vec::new())
ReplyFrame::new(ReplyType::SSH_AGENT_FAILURE, &[])
}
}
@@ -150,6 +151,6 @@ impl AgentSuccess {
impl From<AgentSuccess> for ReplyFrame {
fn from(_value: AgentSuccess) -> Self {
ReplyFrame::new(ReplyType::SSH_AGENT_SUCCESS, Vec::new())
ReplyFrame::new(ReplyType::SSH_AGENT_SUCCESS, &[])
}
}

View File

@@ -135,7 +135,6 @@ impl SshSignRequest {
&self.payload_to_sign
}
#[allow(unused)]
pub fn parsed_payload(&self) -> &ParsedSignRequest {
&self.parsed_sign_request
}
@@ -154,7 +153,7 @@ impl TryFrom<&[u8]> for SshSignRequest {
///
/// In this case, the message already has the leading byte stripped off by the previous parsing code.
fn try_from(mut message: &[u8]) -> Result<Self, Self::Error> {
let public_key_blob = read_bytes(&mut message)?.to_vec();
let public_key_blob = read_bytes(&mut message)?.clone();
let data = read_bytes(&mut message)?;
let flags = message
.read_u32::<byteorder::BigEndian>()
@@ -170,11 +169,8 @@ impl TryFrom<&[u8]> for SshSignRequest {
}
#[derive(Debug)]
pub(crate) enum ParsedSignRequest {
#[allow(unused)]
SshSigRequest {
namespace: String,
},
pub enum ParsedSignRequest {
SshSigRequest { namespace: String },
SignRequest {},
}

View File

@@ -38,6 +38,13 @@ import { ApproveSshRequestComponent } from "../../platform/components/approve-ss
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { SshAgentPromptType } from "../models/ssh-agent-setting";
// Note: There are two implementations of the SSH agent, for a
// transition phase, V1, and V2. The version is selected
// via a feature flag when the agent is initialized. Once the feature
// flag is rolled out, V1 can be removed.
const SSH_AGENT_MODULE_VERSION_V1 = 1;
const SSH_AGENT_MODULE_VERSION_V2 = 2;
@Injectable({
providedIn: "root",
})
@@ -63,7 +70,7 @@ export class SshAgentService implements OnDestroy {
private desktopSettingsService: DesktopSettingsService,
private accountService: AccountService,
private configService: ConfigService,
) {}
) { }
async init() {
this.desktopSettingsService.sshAgentEnabled$
@@ -73,7 +80,7 @@ export class SshAgentService implements OnDestroy {
const isV2FeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.SshAgentV2,
);
await ipc.platform.sshAgent.init(isV2FeatureFlagEnabled ? 2 : 1);
await ipc.platform.sshAgent.init(isV2FeatureFlagEnabled ? SSH_AGENT_MODULE_VERSION_V2 : SSH_AGENT_MODULE_VERSION_V1);
}
if (!enabled) {