use std::path::PathBuf;
use std::time::Duration;
pub const HOST_MANAGEMENT_METHODS: &[&str] = &[
"agent_permissions.evaluate_tool",
"agent_permissions.reset",
"agent_permissions.reset_tool",
"agent_permissions.set",
"agent_permissions.set_default",
"agent_permissions.set_tool",
"agents.install",
"agents.remove",
"agents.upsert",
"assistant.identity.set",
"auth.accounts",
"auth.authority_hint",
"auth.complete",
"auth.completion_status",
"auth.logout",
"auth.remove_account",
"auth.snapshot",
"auth.start",
"auth.status",
"auth.switch_account",
"auth.switch_org",
"diagnostics.secret_store_activity",
"declagents.remove",
"declagents.set_enabled",
"messaging.config.get",
"messaging.config.set",
"messaging.pairing.start",
"messaging.pairing.status",
"messaging.status",
"messaging.test_send",
"models.adopt",
"models.install",
"models.pull",
"models.remove",
"models.resource_policy.set",
"models.storage_roots",
"openrouter.auth_cancel",
"openrouter.auth_start",
"openrouter.disconnect",
"openrouter.status",
"permission.approve",
"permission.reject",
"permission.set_tier",
"session.clear_halt",
"tasks.schedule",
"tasks.unschedule",
];
const AGENT_SELF_OR_HOST_METHODS: &[&str] = &["agents.restart", "agents.start", "agents.stop"];
pub fn method_accepts_host_authority(method: &str) -> bool {
HOST_MANAGEMENT_METHODS.contains(&method) || AGENT_SELF_OR_HOST_METHODS.contains(&method)
}
pub fn daemon_ws_url() -> String {
std::env::var("CAR_DAEMON_URL").unwrap_or_else(|_| "ws://127.0.0.1:9100".to_string())
}
pub fn daemon_port() -> u16 {
parse_port(&daemon_ws_url()).unwrap_or(9100)
}
pub fn parse_host_port(url: &str) -> Option<String> {
let after_scheme = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
let host_port = after_scheme.split('/').next().unwrap_or("");
if host_port.starts_with('[') {
if host_port.contains("]:") {
Some(host_port.to_string())
} else {
None
}
} else if host_port.contains(':') {
Some(host_port.to_string())
} else {
None
}
}
pub const LOOPBACK_CONNECT_TIMEOUT: Duration = Duration::from_millis(400);
pub const REMOTE_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
pub fn is_loopback_url(url: &str) -> bool {
let after_scheme = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
let authority = after_scheme.split('/').next().unwrap_or("");
let authority = authority.rsplit('@').next().unwrap_or(authority);
let host = if let Some(v6) = authority.strip_prefix('[') {
v6.split(']').next().unwrap_or("")
} else {
authority.split(':').next().unwrap_or("")
};
match host.parse::<std::net::IpAddr>() {
Ok(ip) => ip.is_loopback(),
Err(_) => host.eq_ignore_ascii_case("localhost"),
}
}
pub fn connect_deadline(url: &str) -> Duration {
if cfg!(windows) && is_loopback_url(url) {
LOOPBACK_CONNECT_TIMEOUT
} else {
REMOTE_CONNECT_TIMEOUT
}
}
pub fn parse_port(url: &str) -> Option<u16> {
let host_port = parse_host_port(url)?;
let port_str = if host_port.starts_with('[') {
host_port.rsplit("]:").next()?
} else {
host_port.rsplit(':').next()?
};
port_str.parse().ok()
}
pub fn probe_daemon_port(timeout: Duration) -> bool {
let url = daemon_ws_url();
let host_port = match parse_host_port(&url) {
Some(hp) => hp,
None => return false,
};
let addrs: Vec<_> = match std::net::ToSocketAddrs::to_socket_addrs(&host_port) {
Ok(it) => it.collect(),
Err(_) => return false,
};
addrs
.into_iter()
.any(|addr| std::net::TcpStream::connect_timeout(&addr, timeout).is_ok())
}
pub fn try_spawn_daemon() -> Result<(), String> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(dir.join("car-server"));
}
}
candidates.push(PathBuf::from("car-server"));
let port = daemon_port().to_string();
let mut last_err: Option<std::io::Error> = None;
for candidate in candidates {
let mut cmd = std::process::Command::new(&candidate);
cmd.args(["--port", &port])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
match cmd.spawn() {
Ok(_) => return Ok(()),
Err(e) => last_err = Some(e),
}
}
Err(match last_err {
Some(e) if e.kind() == std::io::ErrorKind::NotFound => {
"car-server binary not found".to_string()
}
Some(e) => format!("car-server spawn failed: {e}"),
None => unreachable!("candidates is never empty"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_host_port_basic() {
assert_eq!(
parse_host_port("ws://127.0.0.1:9100").as_deref(),
Some("127.0.0.1:9100")
);
assert_eq!(
parse_host_port("wss://other:1234/json-rpc").as_deref(),
Some("other:1234")
);
assert!(parse_host_port("ws://localhost").is_none());
assert_eq!(
parse_host_port("127.0.0.1:9100").as_deref(),
Some("127.0.0.1:9100")
);
assert_eq!(
parse_host_port("wss://[::1]:9100/").as_deref(),
Some("[::1]:9100")
);
assert!(parse_host_port("ws://[::1]").is_none());
}
#[test]
fn loopback_urls_get_the_short_connect_deadline() {
let loopback_deadline = if cfg!(windows) {
LOOPBACK_CONNECT_TIMEOUT
} else {
REMOTE_CONNECT_TIMEOUT
};
for url in [
"ws://127.0.0.1:9100",
"ws://127.0.0.1:9100/",
"ws://127.0.0.1",
"ws://localhost:9100",
"ws://LOCALHOST:9100",
"wss://127.0.0.1:9100/rpc",
"ws://[::1]:9100",
"ws://user:pw@127.0.0.1:9100",
"ws://127.0.0.53:9100",
] {
assert!(is_loopback_url(url), "expected loopback: {url}");
assert_eq!(connect_deadline(url), loopback_deadline, "{url}");
}
for url in [
"ws://10.0.0.4:9100",
"ws://car.internal:9100",
"wss://daemon.example.com/rpc",
"ws://[2001:db8::1]:9100",
"not a url",
"",
] {
assert!(!is_loopback_url(url), "expected non-loopback: {url}");
assert_eq!(connect_deadline(url), REMOTE_CONNECT_TIMEOUT, "{url}");
}
}
#[test]
fn loopback_deadline_sits_between_a_local_accept_and_windows_refusal() {
assert!(LOOPBACK_CONNECT_TIMEOUT > Duration::from_millis(100));
assert!(LOOPBACK_CONNECT_TIMEOUT < Duration::from_millis(2000));
assert!(LOOPBACK_CONNECT_TIMEOUT < REMOTE_CONNECT_TIMEOUT);
}
#[test]
fn parse_port_basic() {
assert_eq!(parse_port("ws://127.0.0.1:9100"), Some(9100));
assert_eq!(parse_port("wss://other:1234"), Some(1234));
assert_eq!(parse_port("ws://[::1]:9101"), Some(9101));
assert_eq!(parse_port("ws://localhost"), None);
assert_eq!(parse_port("ws://[::1]"), None);
}
#[test]
fn probe_dead_port_returns_false() {
let prev = std::env::var("CAR_DAEMON_URL").ok();
std::env::set_var("CAR_DAEMON_URL", "ws://127.0.0.1:1");
assert!(!probe_daemon_port(Duration::from_millis(100)));
match prev {
Some(v) => std::env::set_var("CAR_DAEMON_URL", v),
None => std::env::remove_var("CAR_DAEMON_URL"),
}
}
}