From c13166030dd752121241250e893297a12b60265b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 19 Aug 2026 07:18:46 +0000 Subject: [PATCH 1/2] Fix release failures from Windows SSH agent and macOS DMG EULA. AgentClient::connect_env is Unix-only, so Windows release builds failed after the SSH tunnel landed; connect via OpenSSH named pipe or Pageant instead. Also feed Agree to hdiutil when verifying DMGs that embed the packager license-file as an EULA. Co-authored-by: Pavitra Golchha --- .github/workflows/release.yml | 4 +++- crates/based-ssh/src/tunnel.rs | 33 +++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c634b4..b4a6965 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,7 +103,9 @@ jobs: MNT=$(mktemp -d) cleanup() { hdiutil detach "$MNT" -quiet 2>/dev/null || true; rmdir "$MNT" 2>/dev/null || true; } trap cleanup EXIT - hdiutil attach "$DMG" -mountpoint "$MNT" -nobrowse + # packager `license-file` is passed as --eula; without Agree on stdin, + # hdiutil prints the license and exits with "attach canceled". + hdiutil attach "$DMG" -mountpoint "$MNT" -nobrowse <<<'Y' APP="$MNT/Based.app" if [ ! -d "$APP" ]; then echo "::error::Based.app not found at $APP" diff --git a/crates/based-ssh/src/tunnel.rs b/crates/based-ssh/src/tunnel.rs index 3aa87a1..cc135e2 100644 --- a/crates/based-ssh/src/tunnel.rs +++ b/crates/based-ssh/src/tunnel.rs @@ -9,7 +9,7 @@ use based_core::SshTunnelConfig; use russh::keys::{HashAlg, PrivateKeyWithHashAlg, PublicKey, load_secret_key}; use russh::{ Channel, client, - keys::agent::client::AgentClient, + keys::agent::client::{AgentClient, AgentStream}, keys::known_hosts::{check_known_hosts, check_known_hosts_path}, }; use tokio::io::{AsyncWriteExt, copy_bidirectional}; @@ -171,13 +171,38 @@ async fn authenticate( authenticate_with_agent(session, &ssh.user).await } +async fn connect_ssh_agent() -> Result>> { + #[cfg(unix)] + { + AgentClient::connect_env() + .await + .map(AgentClient::dynamic) + .context("SSH tunnel: could not connect to ssh-agent (SSH_AUTH_SOCK)") + } + #[cfg(windows)] + { + // `connect_env` is Unix-only (SSH_AUTH_SOCK). On Windows prefer the + // OpenSSH agent named pipe, then Pageant. + match AgentClient::connect_named_pipe(r"\\.\pipe\openssh-ssh-agent").await { + Ok(client) => Ok(client.dynamic()), + Err(openssh_err) => AgentClient::connect_pageant() + .await + .map(AgentClient::dynamic) + .with_context(|| { + format!( + "SSH tunnel: could not connect to OpenSSH agent \ + (\\\\.\\pipe\\openssh-ssh-agent: {openssh_err}) or Pageant" + ) + }), + } + } +} + async fn authenticate_with_agent( session: &mut client::Handle, user: &str, ) -> Result<()> { - let mut agent = AgentClient::connect_env() - .await - .context("SSH tunnel: could not connect to ssh-agent (SSH_AUTH_SOCK)")?; + let mut agent = connect_ssh_agent().await?; let identities = agent .request_identities() .await From 4521d23325fa1a6d1cdb10530d13eff09719d90b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 19 Aug 2026 20:22:35 +0000 Subject: [PATCH 2/2] Avoid AgentClient::dynamic so Postgres spawn stays Send. Boxing the agent stream dropped the 'static bound russh's Signer impl needs and caused higher-ranked lifetime errors in Tokio::spawn_result on the Postgres open/test paths. Keep concrete Unix/Windows agent types. Co-authored-by: Pavitra Golchha --- crates/based-ssh/src/tunnel.rs | 44 +++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/based-ssh/src/tunnel.rs b/crates/based-ssh/src/tunnel.rs index cc135e2..f12c3f4 100644 --- a/crates/based-ssh/src/tunnel.rs +++ b/crates/based-ssh/src/tunnel.rs @@ -171,38 +171,48 @@ async fn authenticate( authenticate_with_agent(session, &ssh.user).await } -async fn connect_ssh_agent() -> Result>> { +async fn authenticate_with_agent( + session: &mut client::Handle, + user: &str, +) -> Result<()> { + // Keep concrete agent stream types (do not `.dynamic()`): boxing breaks the + // `'static` bound russh's `Signer` impl needs and surfaces as HRTB errors + // inside `Tokio::spawn_result` on the Postgres open/test paths. #[cfg(unix)] { - AgentClient::connect_env() + let mut agent = AgentClient::connect_env() .await - .map(AgentClient::dynamic) - .context("SSH tunnel: could not connect to ssh-agent (SSH_AUTH_SOCK)") + .context("SSH tunnel: could not connect to ssh-agent (SSH_AUTH_SOCK)")?; + try_agent_identities(session, user, &mut agent).await } #[cfg(windows)] { - // `connect_env` is Unix-only (SSH_AUTH_SOCK). On Windows prefer the - // OpenSSH agent named pipe, then Pageant. + // `connect_env` is Unix-only (SSH_AUTH_SOCK). Prefer OpenSSH's agent + // named pipe, then Pageant. match AgentClient::connect_named_pipe(r"\\.\pipe\openssh-ssh-agent").await { - Ok(client) => Ok(client.dynamic()), - Err(openssh_err) => AgentClient::connect_pageant() - .await - .map(AgentClient::dynamic) - .with_context(|| { + Ok(mut agent) => try_agent_identities(session, user, &mut agent).await, + Err(openssh_err) => { + let mut agent = AgentClient::connect_pageant().await.with_context(|| { format!( "SSH tunnel: could not connect to OpenSSH agent \ - (\\\\.\\pipe\\openssh-ssh-agent: {openssh_err}) or Pageant" + (\\\\.\\pipe\\openssh-ssh-agent: {openssh_err}) or Pageant" ) - }), + })?; + try_agent_identities(session, user, &mut agent).await + } } } } -async fn authenticate_with_agent( +async fn try_agent_identities( session: &mut client::Handle, user: &str, -) -> Result<()> { - let mut agent = connect_ssh_agent().await?; + agent: &mut AgentClient, +) -> Result<()> +where + // Matches russh's `Signer` impl on `AgentClient` (private `auth` module). + S: AgentStream + Unpin + Send + 'static, +{ let identities = agent .request_identities() .await @@ -220,7 +230,7 @@ async fn authenticate_with_agent( let mut last_err = None; for public in identities { match session - .authenticate_publickey_with(user, public, hash, &mut agent) + .authenticate_publickey_with(user, public, hash, agent) .await { Ok(result) if result.success() => return Ok(()),