use std::time::Instant;
use async_trait::async_trait;
use super::config::{AgentCapabilities, AuthMethod, BackendDefinition};
use super::error::CodingAgentError;
use super::status::{AuthStatus, HealthStatus, InstallationStatus};
#[async_trait]
pub trait CodingAgentBackend: Send + Sync {
fn agent_type(&self) -> &str;
fn display_name(&self) -> &str;
async fn check_installation(&self) -> Result<InstallationStatus, CodingAgentError>;
async fn health_check(&self) -> Result<HealthStatus, CodingAgentError>;
fn auth_method(&self) -> AuthMethod;
async fn validate_auth(&self) -> Result<AuthStatus, CodingAgentError>;
fn capabilities(&self) -> &AgentCapabilities;
fn installation_instructions(&self) -> &str;
}
pub struct ConfigDrivenBackend {
definition: BackendDefinition,
endpoint: Option<String>,
}
impl ConfigDrivenBackend {
pub fn new(definition: BackendDefinition, endpoint: Option<String>) -> Self {
Self {
definition,
endpoint,
}
}
fn parse_version(output: &str) -> Option<String> {
let trimmed = output.trim();
if trimmed.is_empty() {
return None;
}
for word in trimmed.split_whitespace() {
let candidate = word.trim_start_matches('v').trim_end_matches(',');
if candidate.starts_with(|c: char| c.is_ascii_digit())
&& candidate.contains('.')
{
return Some(candidate.to_string());
}
}
trimmed.lines().next().map(|s| s.trim().to_string())
}
}
#[async_trait]
impl CodingAgentBackend for ConfigDrivenBackend {
fn agent_type(&self) -> &str {
&self.definition.agent_type
}
fn display_name(&self) -> &str {
&self.definition.display_name
}
async fn check_installation(&self) -> Result<InstallationStatus, CodingAgentError> {
let command_str = &self.definition.install_check_command;
if command_str.trim().is_empty() {
return Err(CodingAgentError::ConfigValidation(
"install_check_command is empty".to_string(),
));
}
let program = command_str.split_whitespace().next().unwrap_or(command_str);
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let extra_paths = format!(
"{}:{}/go/bin:{}/.local/bin:{}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin",
std::env::var("PATH").unwrap_or_default(),
home, home, home
);
let shell_cmd = format!("export PATH=\"{}\"; {}", extra_paths, command_str);
let output = tokio::process::Command::new("sh")
.args(["-c", &shell_cmd])
.output()
.await;
match output {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let version = Self::parse_version(&stdout);
let which_cmd = format!("export PATH=\"{}\"; which {}", extra_paths, program);
let path = tokio::process::Command::new("sh")
.args(["-c", &which_cmd])
.output()
.await
.ok()
.and_then(|o| {
if o.status.success() {
let p = String::from_utf8_lossy(&o.stdout).trim().to_string();
if p.is_empty() {
None
} else {
Some(std::path::PathBuf::from(p))
}
} else {
None
}
});
Ok(InstallationStatus {
installed: true,
version,
path,
})
}
Ok(_output) => {
Ok(InstallationStatus {
installed: false,
version: None,
path: None,
})
}
Err(_) => {
Ok(InstallationStatus {
installed: false,
version: None,
path: None,
})
}
}
}
async fn health_check(&self) -> Result<HealthStatus, CodingAgentError> {
let endpoint = match &self.endpoint {
Some(ep) => ep.clone(),
None => {
return Ok(HealthStatus {
reachable: false,
latency_ms: None,
version: None,
});
}
};
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| {
CodingAgentError::DelegationFailed(format!("failed to create HTTP client: {}", e))
})?;
let start = Instant::now();
let response = client.get(&endpoint).send().await;
let latency_ms = start.elapsed().as_millis() as u64;
match response {
Ok(resp) if resp.status().is_success() => Ok(HealthStatus {
reachable: true,
latency_ms: Some(latency_ms),
version: None,
}),
Ok(_resp) => {
Ok(HealthStatus {
reachable: true,
latency_ms: Some(latency_ms),
version: None,
})
}
Err(_) => Ok(HealthStatus {
reachable: false,
latency_ms: None,
version: None,
}),
}
}
fn auth_method(&self) -> AuthMethod {
self.definition.auth_method.clone()
}
async fn validate_auth(&self) -> Result<AuthStatus, CodingAgentError> {
match &self.definition.auth_method {
AuthMethod::ApiKey { env_var } => {
if std::env::var(env_var).is_ok() {
Ok(AuthStatus::Valid { expires_at: None })
} else {
Ok(AuthStatus::NotConfigured)
}
}
AuthMethod::None => Ok(AuthStatus::Valid { expires_at: None }),
AuthMethod::OAuth { .. } | AuthMethod::CliLogin { .. } => {
Ok(AuthStatus::NotConfigured)
}
}
}
fn capabilities(&self) -> &AgentCapabilities {
&self.definition.capabilities
}
fn installation_instructions(&self) -> &str {
&self.definition.install_instructions
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coding_agent::config::{AgentCapabilities, AuthMethod, BackendDefinition};
fn make_test_definition() -> BackendDefinition {
BackendDefinition {
agent_type: "test-agent".to_string(),
display_name: "Test Agent".to_string(),
cli_command: "test-cli".to_string(),
install_check_command: "echo v1.2.3".to_string(),
auth_method: AuthMethod::None,
capabilities: AgentCapabilities {
file_context: true,
streaming_output: false,
cost_reporting: true,
cancellation: false,
},
install_instructions: "Run: cargo install test-agent".to_string(),
install_instructions_windows: None,
install_instructions_linux: None,
}
}
#[test]
fn test_config_driven_backend_agent_type() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
assert_eq!(backend.agent_type(), "test-agent");
}
#[test]
fn test_config_driven_backend_display_name() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
assert_eq!(backend.display_name(), "Test Agent");
}
#[test]
fn test_config_driven_backend_capabilities() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
let caps = backend.capabilities();
assert!(caps.file_context);
assert!(!caps.streaming_output);
assert!(caps.cost_reporting);
assert!(!caps.cancellation);
}
#[test]
fn test_config_driven_backend_installation_instructions() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
assert_eq!(
backend.installation_instructions(),
"Run: cargo install test-agent"
);
}
#[test]
fn test_config_driven_backend_auth_method() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
assert!(matches!(backend.auth_method(), AuthMethod::None));
}
#[test]
fn test_parse_version_semver() {
assert_eq!(
ConfigDrivenBackend::parse_version("v1.2.3"),
Some("1.2.3".to_string())
);
}
#[test]
fn test_parse_version_with_prefix() {
assert_eq!(
ConfigDrivenBackend::parse_version("claude-code version 1.0.5"),
Some("1.0.5".to_string())
);
}
#[test]
fn test_parse_version_plain() {
assert_eq!(
ConfigDrivenBackend::parse_version("2.1.0"),
Some("2.1.0".to_string())
);
}
#[test]
fn test_parse_version_empty() {
assert_eq!(ConfigDrivenBackend::parse_version(""), None);
}
#[test]
fn test_parse_version_no_version_pattern() {
assert_eq!(
ConfigDrivenBackend::parse_version("hello world"),
Some("hello world".to_string())
);
}
#[tokio::test]
async fn test_check_installation_with_echo() {
let def = BackendDefinition {
agent_type: "echo-agent".to_string(),
display_name: "Echo Agent".to_string(),
cli_command: "echo".to_string(),
install_check_command: "echo v3.5.1".to_string(),
auth_method: AuthMethod::None,
capabilities: AgentCapabilities::default(),
install_instructions: "Already installed".to_string(),
install_instructions_windows: None,
install_instructions_linux: None,
};
let backend = ConfigDrivenBackend::new(def, None);
let status = backend.check_installation().await.unwrap();
assert!(status.installed);
assert_eq!(status.version, Some("3.5.1".to_string()));
}
#[tokio::test]
async fn test_check_installation_not_found() {
let def = BackendDefinition {
agent_type: "missing-agent".to_string(),
display_name: "Missing Agent".to_string(),
cli_command: "nonexistent_binary_xyz_12345".to_string(),
install_check_command: "nonexistent_binary_xyz_12345 --version".to_string(),
auth_method: AuthMethod::None,
capabilities: AgentCapabilities::default(),
install_instructions: "Install it somehow".to_string(),
install_instructions_windows: None,
install_instructions_linux: None,
};
let backend = ConfigDrivenBackend::new(def, None);
let status = backend.check_installation().await.unwrap();
assert!(!status.installed);
assert_eq!(status.version, None);
}
#[tokio::test]
async fn test_health_check_no_endpoint() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
let status = backend.health_check().await.unwrap();
assert!(!status.reachable);
assert_eq!(status.latency_ms, None);
}
#[tokio::test]
async fn test_health_check_unreachable_endpoint() {
let def = make_test_definition();
let backend =
ConfigDrivenBackend::new(def, Some("http://127.0.0.1:19999/health".to_string()));
let status = backend.health_check().await.unwrap();
assert!(!status.reachable);
}
#[tokio::test]
async fn test_validate_auth_none() {
let def = make_test_definition();
let backend = ConfigDrivenBackend::new(def, None);
let status = backend.validate_auth().await.unwrap();
assert!(matches!(status, AuthStatus::Valid { expires_at: None }));
}
#[tokio::test]
async fn test_validate_auth_api_key_not_set() {
let def = BackendDefinition {
agent_type: "api-agent".to_string(),
display_name: "API Agent".to_string(),
cli_command: "api-cli".to_string(),
install_check_command: "api-cli --version".to_string(),
auth_method: AuthMethod::ApiKey {
env_var: "NONEXISTENT_TEST_KEY_XYZ_98765".to_string(),
},
capabilities: AgentCapabilities::default(),
install_instructions: "".to_string(),
install_instructions_windows: None,
install_instructions_linux: None,
};
let backend = ConfigDrivenBackend::new(def, None);
let status = backend.validate_auth().await.unwrap();
assert!(matches!(status, AuthStatus::NotConfigured));
}
}