agent_servers: Set proxy env for all ACP agents (#38247)

Ben Brandt created

- Use ProxySettings::proxy_url to read from settings or env 
- Export HTTP(S)_PROXY and NO_PROXY for agent CLIs 
- Add read_no_proxy_from_env and move parsing from main

Closes https://github.com/zed-industries/claude-code-acp/issues/46

Release Notes:

- acp: Pass proxy settings through to all ACP agents

Change summary

Cargo.lock                                |  1 
crates/agent_servers/Cargo.toml           |  1 
crates/agent_servers/src/agent_servers.rs | 28 ++++++++++++++++++++++++
crates/agent_servers/src/claude.rs        |  5 ++-
crates/agent_servers/src/custom.rs        |  5 ++-
crates/agent_servers/src/gemini.rs        | 23 ++++----------------
crates/client/src/client.rs               | 16 +++++++++++++
crates/http_client/src/http_client.rs     |  6 +++++
crates/zed/src/main.rs                    | 12 ---------
9 files changed, 62 insertions(+), 35 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -300,6 +300,7 @@ dependencies = [
  "futures 0.3.31",
  "gpui",
  "gpui_tokio",
+ "http_client",
  "indoc",
  "language",
  "language_model",

crates/agent_servers/Cargo.toml 🔗

@@ -30,6 +30,7 @@ fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
 gpui_tokio = { workspace = true, optional = true }
+http_client.workspace = true
 indoc.workspace = true
 language.workspace = true
 language_model.workspace = true

crates/agent_servers/src/agent_servers.rs 🔗

@@ -7,15 +7,19 @@ mod gemini;
 pub mod e2e_tests;
 
 pub use claude::*;
+use client::ProxySettings;
+use collections::HashMap;
 pub use custom::*;
 use fs::Fs;
 pub use gemini::*;
+use http_client::read_no_proxy_from_env;
 use project::agent_server_store::AgentServerStore;
 
 use acp_thread::AgentConnection;
 use anyhow::Result;
-use gpui::{App, Entity, SharedString, Task};
+use gpui::{App, AppContext, Entity, SharedString, Task};
 use project::Project;
+use settings::SettingsStore;
 use std::{any::Any, path::Path, rc::Rc, sync::Arc};
 
 pub use acp::AcpConnection;
@@ -77,3 +81,25 @@ impl dyn AgentServer {
         self.into_any().downcast().ok()
     }
 }
+
+/// Load the default proxy environment variables to pass through to the agent
+pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
+    let proxy_url = cx
+        .read_global(|settings: &SettingsStore, _| settings.get::<ProxySettings>(None).proxy_url());
+    let mut env = HashMap::default();
+
+    if let Some(proxy_url) = &proxy_url {
+        let env_var = if proxy_url.scheme() == "https" {
+            "HTTPS_PROXY"
+        } else {
+            "HTTP_PROXY"
+        };
+        env.insert(env_var.to_owned(), proxy_url.to_string());
+    }
+
+    if let Some(no_proxy) = read_no_proxy_from_env() {
+        env.insert("NO_PROXY".to_owned(), no_proxy);
+    }
+
+    env
+}

crates/agent_servers/src/claude.rs 🔗

@@ -10,7 +10,7 @@ use anyhow::{Context as _, Result};
 use gpui::{App, AppContext as _, SharedString, Task};
 use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
 
-use crate::{AgentServer, AgentServerDelegate};
+use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
 use acp_thread::AgentConnection;
 
 #[derive(Clone)]
@@ -60,6 +60,7 @@ impl AgentServer for ClaudeCode {
         let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let store = delegate.store.downgrade();
+        let extra_env = load_proxy_env(cx);
         let default_mode = self.default_mode(cx);
 
         cx.spawn(async move |cx| {
@@ -70,7 +71,7 @@ impl AgentServer for ClaudeCode {
                         .context("Claude Code is not registered")?;
                     anyhow::Ok(agent.get_command(
                         root_dir.as_deref(),
-                        Default::default(),
+                        extra_env,
                         delegate.status_tx,
                         delegate.new_version_available,
                         &mut cx.to_async(),

crates/agent_servers/src/custom.rs 🔗

@@ -1,4 +1,4 @@
-use crate::AgentServerDelegate;
+use crate::{AgentServerDelegate, load_proxy_env};
 use acp_thread::AgentConnection;
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result};
@@ -65,6 +65,7 @@ impl crate::AgentServer for CustomAgentServer {
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let default_mode = self.default_mode(cx);
         let store = delegate.store.downgrade();
+        let extra_env = load_proxy_env(cx);
 
         cx.spawn(async move |cx| {
             let (command, root_dir, login) = store
@@ -76,7 +77,7 @@ impl crate::AgentServer for CustomAgentServer {
                         })?;
                     anyhow::Ok(agent.get_command(
                         root_dir.as_deref(),
-                        Default::default(),
+                        extra_env,
                         delegate.status_tx,
                         delegate.new_version_available,
                         &mut cx.to_async(),

crates/agent_servers/src/gemini.rs 🔗

@@ -1,15 +1,12 @@
 use std::rc::Rc;
 use std::{any::Any, path::Path};
 
-use crate::{AgentServer, AgentServerDelegate};
+use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
 use acp_thread::AgentConnection;
 use anyhow::{Context as _, Result};
-use client::ProxySettings;
-use collections::HashMap;
-use gpui::{App, AppContext, SharedString, Task};
+use gpui::{App, SharedString, Task};
 use language_models::provider::google::GoogleLanguageModelProvider;
 use project::agent_server_store::GEMINI_NAME;
-use settings::SettingsStore;
 
 #[derive(Clone)]
 pub struct Gemini;
@@ -37,14 +34,12 @@ impl AgentServer for Gemini {
         let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let store = delegate.store.downgrade();
-        let proxy_url = cx.read_global(|settings: &SettingsStore, _| {
-            settings.get::<ProxySettings>(None).proxy.clone()
-        });
+        let mut extra_env = load_proxy_env(cx);
         let default_mode = self.default_mode(cx);
 
         cx.spawn(async move |cx| {
-            let mut extra_env = HashMap::default();
             extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
+
             if let Some(api_key) = cx
                 .update(GoogleLanguageModelProvider::api_key_for_gemini_cli)?
                 .await
@@ -52,7 +47,7 @@ impl AgentServer for Gemini {
             {
                 extra_env.insert("GEMINI_API_KEY".into(), api_key);
             }
-            let (mut command, root_dir, login) = store
+            let (command, root_dir, login) = store
                 .update(cx, |store, cx| {
                     let agent = store
                         .get_external_agent(&GEMINI_NAME.into())
@@ -67,14 +62,6 @@ impl AgentServer for Gemini {
                 })??
                 .await?;
 
-            // Add proxy flag if proxy settings are configured in Zed and not in the args
-            if let Some(proxy_url_value) = &proxy_url
-                && !command.args.iter().any(|arg| arg.contains("--proxy"))
-            {
-                command.args.push("--proxy".into());
-                command.args.push(proxy_url_value.clone());
-            }
-
             let connection = crate::acp::connect(
                 name,
                 command,

crates/client/src/client.rs 🔗

@@ -22,7 +22,7 @@ use futures::{
     channel::oneshot, future::BoxFuture,
 };
 use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
-use http_client::{HttpClient, HttpClientWithUrl, http};
+use http_client::{HttpClient, HttpClientWithUrl, http, read_proxy_from_env};
 use parking_lot::RwLock;
 use postage::watch;
 use proxy::connect_proxy_stream;
@@ -132,6 +132,20 @@ pub struct ProxySettings {
     pub proxy: Option<String>,
 }
 
+impl ProxySettings {
+    pub fn proxy_url(&self) -> Option<Url> {
+        self.proxy
+            .as_ref()
+            .and_then(|input| {
+                input
+                    .parse::<Url>()
+                    .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
+                    .ok()
+            })
+            .or_else(read_proxy_from_env)
+    }
+}
+
 impl Settings for ProxySettings {
     type FileContent = ProxySettingsContent;
 

crates/http_client/src/http_client.rs 🔗

@@ -318,6 +318,12 @@ pub fn read_proxy_from_env() -> Option<Url> {
         .and_then(|env| env.parse().ok())
 }
 
+pub fn read_no_proxy_from_env() -> Option<String> {
+    const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
+
+    ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
+}
+
 pub struct BlockedHttpClient;
 
 impl BlockedHttpClient {

crates/zed/src/main.rs 🔗

@@ -19,7 +19,6 @@ use git::GitHostingProviderRegistry;
 use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
 
 use gpui_tokio::Tokio;
-use http_client::{Url, read_proxy_from_env};
 use language::LanguageRegistry;
 use onboarding::{FIRST_OPEN, show_onboarding_view};
 use prompt_store::PromptBuilder;
@@ -398,16 +397,7 @@ pub fn main() {
             std::env::consts::OS,
             std::env::consts::ARCH
         );
-        let proxy_str = ProxySettings::get_global(cx).proxy.to_owned();
-        let proxy_url = proxy_str
-            .as_ref()
-            .and_then(|input| {
-                input
-                    .parse::<Url>()
-                    .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
-                    .ok()
-            })
-            .or_else(read_proxy_from_env);
+        let proxy_url = ProxySettings::get_global(cx).proxy_url();
         let http = {
             let _guard = Tokio::handle(cx).enter();