Skip to content

Commit 04eebb8

Browse files
phodalcodex
andcommitted
fix(desktop): proxy ACP binary downloads
Pass the desktop proxy-aware reqwest client into binary archive downloads and support SOCKS-only macOS system proxy configurations. Co-authored-by: Codex (GPT 5.5) <codex@openai.com>
1 parent f9dc38b commit 04eebb8

4 files changed

Lines changed: 130 additions & 9 deletions

File tree

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ routa-server = { path = "../../../crates/routa-server" }
3737

3838
# Async runtime (needed for Tauri integration)
3939
tokio = { version = "1", features = ["full"] }
40-
reqwest = { version = "0.12", features = ["json"] }
40+
reqwest = { version = "0.12", features = ["json", "socks"] }
4141

4242
[dev-dependencies]
4343
axum = "0.8.8"

apps/desktop/src-tauri/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,10 @@ async fn install_acp_agent(
277277
.get_binary_info(&platform)
278278
.ok_or_else(|| format!("No binary available for platform: {platform}"))?;
279279

280+
let http_client = build_http_client()?;
280281
let exe_path = state
281282
.binary_manager
282-
.install_binary(&agent_id, &version, binary_info)
283+
.install_binary_with_client(&http_client, &agent_id, &version, binary_info)
283284
.await?;
284285

285286
state

apps/desktop/src-tauri/src/system_proxy.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct ProxySettings {
2424
}
2525

2626
impl ProxySettings {
27-
fn url(&self) -> Option<String> {
27+
fn url(&self, scheme: &str) -> Option<String> {
2828
if !self.enabled {
2929
return None;
3030
}
@@ -36,14 +36,15 @@ impl ProxySettings {
3636
} else {
3737
host.to_string()
3838
};
39-
Some(format!("http://{authority}:{port}"))
39+
Some(format!("{scheme}://{authority}:{port}"))
4040
}
4141
}
4242

43-
/// Parse the output of `scutil --proxy`, preferring HTTPS over HTTP settings.
43+
/// Parse `scutil --proxy`, preferring HTTPS, then HTTP, then SOCKS settings.
4444
fn parse_macos_system_proxy(output: &str) -> Option<String> {
4545
let mut https = ProxySettings::default();
4646
let mut http = ProxySettings::default();
47+
let mut socks = ProxySettings::default();
4748

4849
for line in output.lines() {
4950
let Some((key, value)) = line.trim().split_once(" : ") else {
@@ -58,11 +59,17 @@ fn parse_macos_system_proxy(output: &str) -> Option<String> {
5859
"HTTPEnable" => http.enabled = value == "1",
5960
"HTTPProxy" => http.host = Some(value.to_string()),
6061
"HTTPPort" => http.port = value.parse().ok(),
62+
"SOCKSEnable" => socks.enabled = value == "1",
63+
"SOCKSProxy" => socks.host = Some(value.to_string()),
64+
"SOCKSPort" => socks.port = value.parse().ok(),
6165
_ => {}
6266
}
6367
}
6468

65-
https.url().or_else(|| http.url())
69+
https
70+
.url("http")
71+
.or_else(|| http.url("http"))
72+
.or_else(|| socks.url("socks5h"))
6673
}
6774

6875
#[cfg(target_os = "macos")]
@@ -150,4 +157,21 @@ mod tests {
150157
Some("http://[::1]:6152".to_string())
151158
);
152159
}
160+
161+
#[test]
162+
fn falls_back_to_enabled_socks_proxy() {
163+
let output = r#"
164+
<dictionary> {
165+
HTTPEnable : 0
166+
HTTPSEnable : 0
167+
SOCKSEnable : 1
168+
SOCKSPort : 1080
169+
SOCKSProxy : 127.0.0.1
170+
}
171+
"#;
172+
173+
let proxy_url = parse_macos_system_proxy(output).expect("parse SOCKS proxy");
174+
assert_eq!(proxy_url, "socks5h://127.0.0.1:1080");
175+
reqwest::Proxy::all(proxy_url).expect("SOCKS proxy feature is enabled");
176+
}
153177
}

crates/routa-core/src/acp/binary_manager.rs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ impl AcpBinaryManager {
3737
agent_id: &str,
3838
version: &str,
3939
binary_info: &BinaryInfo,
40+
) -> Result<PathBuf, String> {
41+
let http_client = reqwest::Client::new();
42+
self.install_binary_with_client(&http_client, agent_id, version, binary_info)
43+
.await
44+
}
45+
46+
/// Download and install a binary agent with a caller-provided HTTP client.
47+
///
48+
/// Runtime adapters can use this to preserve transport concerns such as
49+
/// system proxy configuration without coupling them into the core domain.
50+
pub async fn install_binary_with_client(
51+
&self,
52+
http_client: &reqwest::Client,
53+
agent_id: &str,
54+
version: &str,
55+
binary_info: &BinaryInfo,
4056
) -> Result<PathBuf, String> {
4157
// Get or create a lock for this agent
4258
let lock = {
@@ -75,7 +91,7 @@ impl AcpBinaryManager {
7591

7692
// Download the archive
7793
let archive_path = self
78-
.download_archive(&binary_info.archive, &download_dir)
94+
.download_archive_with_client(http_client, &binary_info.archive, &download_dir)
7995
.await?;
8096

8197
// Extract the archive
@@ -103,10 +119,17 @@ impl AcpBinaryManager {
103119
}
104120

105121
/// Download an archive from a URL.
106-
async fn download_archive(&self, url: &str, download_dir: &Path) -> Result<PathBuf, String> {
122+
async fn download_archive_with_client(
123+
&self,
124+
http_client: &reqwest::Client,
125+
url: &str,
126+
download_dir: &Path,
127+
) -> Result<PathBuf, String> {
107128
tracing::info!("[AcpBinaryManager] Downloading from {}", url);
108129

109-
let response = reqwest::get(url)
130+
let response = http_client
131+
.get(url)
132+
.send()
110133
.await
111134
.map_err(|e| format!("Failed to download: {e}"))?;
112135

@@ -331,3 +354,76 @@ impl AcpBinaryManager {
331354
Ok(())
332355
}
333356
}
357+
358+
#[cfg(test)]
359+
mod tests {
360+
use reqwest::header::{HeaderMap, HeaderValue};
361+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
362+
363+
use super::AcpBinaryManager;
364+
use crate::acp::AcpPaths;
365+
366+
#[tokio::test]
367+
async fn download_archive_uses_caller_provided_http_client() {
368+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
369+
.await
370+
.expect("bind test server");
371+
let address = listener.local_addr().expect("read test server address");
372+
let server = tokio::spawn(async move {
373+
let (mut socket, _) = listener.accept().await.expect("accept request");
374+
let mut request = Vec::new();
375+
let mut chunk = [0; 1024];
376+
while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
377+
let read = socket.read(&mut chunk).await.expect("read request");
378+
if read == 0 {
379+
break;
380+
}
381+
request.extend_from_slice(&chunk[..read]);
382+
}
383+
let request = String::from_utf8_lossy(&request);
384+
let (status, body) = if request
385+
.to_ascii_lowercase()
386+
.contains("x-routa-test-client: configured")
387+
{
388+
("200 OK", "binary")
389+
} else {
390+
("403 Forbidden", "missing client header")
391+
};
392+
let response = format!(
393+
"HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
394+
body.len()
395+
);
396+
socket
397+
.write_all(response.as_bytes())
398+
.await
399+
.expect("write response");
400+
});
401+
402+
let mut headers = HeaderMap::new();
403+
headers.insert(
404+
"x-routa-test-client",
405+
HeaderValue::from_static("configured"),
406+
);
407+
let http_client = reqwest::Client::builder()
408+
.default_headers(headers)
409+
.build()
410+
.expect("build configured client");
411+
let temp_dir = tempfile::tempdir().expect("create download directory");
412+
let manager = AcpBinaryManager::new(AcpPaths::new());
413+
414+
let archive = manager
415+
.download_archive_with_client(
416+
&http_client,
417+
&format!("http://{address}/agent.bin"),
418+
temp_dir.path(),
419+
)
420+
.await
421+
.expect("download archive");
422+
423+
assert_eq!(
424+
tokio::fs::read(archive).await.expect("read archive"),
425+
b"binary"
426+
);
427+
server.await.expect("join test server");
428+
}
429+
}

0 commit comments

Comments
 (0)