mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 01:04:42 +00:00
feat: expand api snapshots and schema tooling
This commit is contained in:
parent
ee014b0838
commit
011ca27287
72 changed files with 29480 additions and 1081 deletions
|
|
@ -1,9 +1,17 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
|
||||
use reqwest::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::agents::AgentId;
|
||||
use crate::credentials::{AuthType, ExtractedCredentials, ProviderCredentials};
|
||||
use crate::credentials::{
|
||||
extract_all_credentials, AuthType, CredentialExtractionOptions, ExtractedCredentials,
|
||||
ProviderCredentials,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestAgentConfig {
|
||||
|
|
@ -13,76 +21,121 @@ pub struct TestAgentConfig {
|
|||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TestAgentConfigError {
|
||||
#[error("no test agents configured (set SANDBOX_TEST_AGENTS)")]
|
||||
#[error("no test agents detected (install agents or set SANDBOX_TEST_AGENTS)")]
|
||||
NoAgentsConfigured,
|
||||
#[error("unknown agent name: {0}")]
|
||||
UnknownAgent(String),
|
||||
#[error("missing credentials for {agent}: {missing}")]
|
||||
MissingCredentials { agent: AgentId, missing: String },
|
||||
#[error("invalid credentials for {provider} (status {status})")]
|
||||
InvalidCredentials { provider: String, status: u16 },
|
||||
#[error("credential health check failed for {provider}: {message}")]
|
||||
HealthCheckFailed { provider: String, message: String },
|
||||
}
|
||||
|
||||
const AGENTS_ENV: &str = "SANDBOX_TEST_AGENTS";
|
||||
const ANTHROPIC_ENV: &str = "SANDBOX_TEST_ANTHROPIC_API_KEY";
|
||||
const OPENAI_ENV: &str = "SANDBOX_TEST_OPENAI_API_KEY";
|
||||
const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models";
|
||||
const OPENAI_MODELS_URL: &str = "https://api.openai.com/v1/models";
|
||||
const ANTHROPIC_VERSION: &str = "2023-06-01";
|
||||
|
||||
#[derive(Default)]
|
||||
struct HealthCheckCache {
|
||||
anthropic_ok: bool,
|
||||
openai_ok: bool,
|
||||
}
|
||||
|
||||
pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigError> {
|
||||
let raw_agents = env::var(AGENTS_ENV).unwrap_or_default();
|
||||
let mut agents = Vec::new();
|
||||
for entry in raw_agents.split(',') {
|
||||
let trimmed = entry.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
let mut agents = if raw_agents.trim().is_empty() {
|
||||
detect_system_agents()
|
||||
} else {
|
||||
let mut agents = Vec::new();
|
||||
for entry in raw_agents.split(',') {
|
||||
let trimmed = entry.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if trimmed == "all" {
|
||||
agents.extend([
|
||||
AgentId::Claude,
|
||||
AgentId::Codex,
|
||||
AgentId::Opencode,
|
||||
AgentId::Amp,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
let agent = AgentId::parse(trimmed)
|
||||
.ok_or_else(|| TestAgentConfigError::UnknownAgent(trimmed.to_string()))?;
|
||||
agents.push(agent);
|
||||
}
|
||||
if trimmed == "all" {
|
||||
agents.extend([
|
||||
AgentId::Claude,
|
||||
AgentId::Codex,
|
||||
AgentId::Opencode,
|
||||
AgentId::Amp,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
let agent = AgentId::parse(trimmed)
|
||||
.ok_or_else(|| TestAgentConfigError::UnknownAgent(trimmed.to_string()))?;
|
||||
agents.push(agent);
|
||||
}
|
||||
agents
|
||||
};
|
||||
|
||||
agents.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
||||
agents.dedup();
|
||||
|
||||
if agents.is_empty() {
|
||||
return Err(TestAgentConfigError::NoAgentsConfigured);
|
||||
}
|
||||
|
||||
let anthropic_key = read_env_key(ANTHROPIC_ENV);
|
||||
let openai_key = read_env_key(OPENAI_ENV);
|
||||
let extracted = extract_all_credentials(&CredentialExtractionOptions::new());
|
||||
let anthropic_cred = read_env_key(ANTHROPIC_ENV)
|
||||
.map(|key| ProviderCredentials {
|
||||
api_key: key,
|
||||
source: "sandbox-test-env".to_string(),
|
||||
auth_type: AuthType::ApiKey,
|
||||
provider: "anthropic".to_string(),
|
||||
})
|
||||
.or_else(|| extracted.anthropic.clone());
|
||||
let openai_cred = read_env_key(OPENAI_ENV)
|
||||
.map(|key| ProviderCredentials {
|
||||
api_key: key,
|
||||
source: "sandbox-test-env".to_string(),
|
||||
auth_type: AuthType::ApiKey,
|
||||
provider: "openai".to_string(),
|
||||
})
|
||||
.or_else(|| extracted.openai.clone());
|
||||
let mut health_cache = HealthCheckCache::default();
|
||||
|
||||
let mut configs = Vec::new();
|
||||
for agent in agents {
|
||||
let credentials = match agent {
|
||||
AgentId::Claude | AgentId::Amp => {
|
||||
let anthropic_key = anthropic_key.clone().ok_or_else(|| {
|
||||
let anthropic_cred = anthropic_cred.clone().ok_or_else(|| {
|
||||
TestAgentConfigError::MissingCredentials {
|
||||
agent,
|
||||
missing: ANTHROPIC_ENV.to_string(),
|
||||
}
|
||||
})?;
|
||||
credentials_with(anthropic_key, None)
|
||||
ensure_anthropic_ok(&mut health_cache, &anthropic_cred)?;
|
||||
credentials_with(Some(anthropic_cred), None)
|
||||
}
|
||||
AgentId::Codex => {
|
||||
let openai_key = openai_key.clone().ok_or_else(|| {
|
||||
let openai_cred = openai_cred.clone().ok_or_else(|| {
|
||||
TestAgentConfigError::MissingCredentials {
|
||||
agent,
|
||||
missing: OPENAI_ENV.to_string(),
|
||||
}
|
||||
})?;
|
||||
credentials_with(None, Some(openai_key))
|
||||
ensure_openai_ok(&mut health_cache, &openai_cred)?;
|
||||
credentials_with(None, Some(openai_cred))
|
||||
}
|
||||
AgentId::Opencode => {
|
||||
if anthropic_key.is_none() && openai_key.is_none() {
|
||||
if anthropic_cred.is_none() && openai_cred.is_none() {
|
||||
return Err(TestAgentConfigError::MissingCredentials {
|
||||
agent,
|
||||
missing: format!("{ANTHROPIC_ENV} or {OPENAI_ENV}"),
|
||||
});
|
||||
}
|
||||
credentials_with(anthropic_key.clone(), openai_key.clone())
|
||||
if let Some(cred) = anthropic_cred.as_ref() {
|
||||
ensure_anthropic_ok(&mut health_cache, cred)?;
|
||||
}
|
||||
if let Some(cred) = openai_cred.as_ref() {
|
||||
ensure_openai_ok(&mut health_cache, cred)?;
|
||||
}
|
||||
credentials_with(anthropic_cred.clone(), openai_cred.clone())
|
||||
}
|
||||
};
|
||||
configs.push(TestAgentConfig { agent, credentials });
|
||||
|
|
@ -91,6 +144,178 @@ pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigErr
|
|||
Ok(configs)
|
||||
}
|
||||
|
||||
fn ensure_anthropic_ok(
|
||||
cache: &mut HealthCheckCache,
|
||||
credentials: &ProviderCredentials,
|
||||
) -> Result<(), TestAgentConfigError> {
|
||||
if cache.anthropic_ok {
|
||||
return Ok(());
|
||||
}
|
||||
health_check_anthropic(credentials)?;
|
||||
cache.anthropic_ok = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_openai_ok(
|
||||
cache: &mut HealthCheckCache,
|
||||
credentials: &ProviderCredentials,
|
||||
) -> Result<(), TestAgentConfigError> {
|
||||
if cache.openai_ok {
|
||||
return Ok(());
|
||||
}
|
||||
health_check_openai(credentials)?;
|
||||
cache.openai_ok = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn health_check_anthropic(credentials: &ProviderCredentials) -> Result<(), TestAgentConfigError> {
|
||||
let credentials = credentials.clone();
|
||||
run_blocking_check("anthropic", move || {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||
provider: "anthropic".to_string(),
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let mut headers = HeaderMap::new();
|
||||
match credentials.auth_type {
|
||||
AuthType::ApiKey => {
|
||||
headers.insert(
|
||||
"x-api-key",
|
||||
HeaderValue::from_str(&credentials.api_key).map_err(|_| {
|
||||
TestAgentConfigError::HealthCheckFailed {
|
||||
provider: "anthropic".to_string(),
|
||||
message: "invalid anthropic api key header value".to_string(),
|
||||
}
|
||||
})?,
|
||||
);
|
||||
}
|
||||
AuthType::Oauth => {
|
||||
let value = format!("Bearer {}", credentials.api_key);
|
||||
headers.insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&value).map_err(|_| {
|
||||
TestAgentConfigError::HealthCheckFailed {
|
||||
provider: "anthropic".to_string(),
|
||||
message: "invalid anthropic oauth header value".to_string(),
|
||||
}
|
||||
})?,
|
||||
);
|
||||
}
|
||||
}
|
||||
headers.insert(
|
||||
"anthropic-version",
|
||||
HeaderValue::from_static(ANTHROPIC_VERSION),
|
||||
);
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
|
||||
let response = client
|
||||
.get(ANTHROPIC_MODELS_URL)
|
||||
.headers(headers)
|
||||
.send()
|
||||
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||
provider: "anthropic".to_string(),
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
handle_health_response("anthropic", response)
|
||||
})
|
||||
}
|
||||
|
||||
fn health_check_openai(credentials: &ProviderCredentials) -> Result<(), TestAgentConfigError> {
|
||||
let credentials = credentials.clone();
|
||||
run_blocking_check("openai", move || {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||
provider: "openai".to_string(),
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let response = client
|
||||
.get(OPENAI_MODELS_URL)
|
||||
.bearer_auth(&credentials.api_key)
|
||||
.send()
|
||||
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||
provider: "openai".to_string(),
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
handle_health_response("openai", response)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_health_response(
|
||||
provider: &str,
|
||||
response: reqwest::blocking::Response,
|
||||
) -> Result<(), TestAgentConfigError> {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
|
||||
return Err(TestAgentConfigError::InvalidCredentials {
|
||||
provider: provider.to_string(),
|
||||
status: status.as_u16(),
|
||||
});
|
||||
}
|
||||
let body = response.text().unwrap_or_default();
|
||||
let mut summary = body.trim().to_string();
|
||||
if summary.len() > 200 {
|
||||
summary.truncate(200);
|
||||
}
|
||||
Err(TestAgentConfigError::HealthCheckFailed {
|
||||
provider: provider.to_string(),
|
||||
message: format!("status {}: {}", status.as_u16(), summary),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_blocking_check<F>(
|
||||
provider: &str,
|
||||
check: F,
|
||||
) -> Result<(), TestAgentConfigError>
|
||||
where
|
||||
F: FnOnce() -> Result<(), TestAgentConfigError> + Send + 'static,
|
||||
{
|
||||
std::thread::spawn(check).join().unwrap_or_else(|_| {
|
||||
Err(TestAgentConfigError::HealthCheckFailed {
|
||||
provider: provider.to_string(),
|
||||
message: "health check panicked".to_string(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn detect_system_agents() -> Vec<AgentId> {
|
||||
let candidates = [AgentId::Claude, AgentId::Codex, AgentId::Opencode, AgentId::Amp];
|
||||
let install_dir = default_install_dir();
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|agent| {
|
||||
let binary = agent.binary_name();
|
||||
find_in_path(binary) || install_dir.join(binary).exists()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_install_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.map(|dir| dir.join("sandbox-agent").join("bin"))
|
||||
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("bin"))
|
||||
}
|
||||
|
||||
fn find_in_path(binary_name: &str) -> bool {
|
||||
let path_var = match env::var_os("PATH") {
|
||||
Some(path) => path,
|
||||
None => return false,
|
||||
};
|
||||
for path in env::split_paths(&path_var) {
|
||||
let candidate = path.join(binary_name);
|
||||
if candidate.exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn read_env_key(name: &str) -> Option<String> {
|
||||
env::var(name).ok().and_then(|value| {
|
||||
let trimmed = value.trim().to_string();
|
||||
|
|
@ -103,25 +328,11 @@ fn read_env_key(name: &str) -> Option<String> {
|
|||
}
|
||||
|
||||
fn credentials_with(
|
||||
anthropic_key: Option<String>,
|
||||
openai_key: Option<String>,
|
||||
anthropic_cred: Option<ProviderCredentials>,
|
||||
openai_cred: Option<ProviderCredentials>,
|
||||
) -> ExtractedCredentials {
|
||||
let mut credentials = ExtractedCredentials::default();
|
||||
if let Some(key) = anthropic_key {
|
||||
credentials.anthropic = Some(ProviderCredentials {
|
||||
api_key: key,
|
||||
source: "sandbox-test-env".to_string(),
|
||||
auth_type: AuthType::ApiKey,
|
||||
provider: "anthropic".to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(key) = openai_key {
|
||||
credentials.openai = Some(ProviderCredentials {
|
||||
api_key: key,
|
||||
source: "sandbox-test-env".to_string(),
|
||||
auth_type: AuthType::ApiKey,
|
||||
provider: "openai".to_string(),
|
||||
});
|
||||
}
|
||||
credentials.anthropic = anthropic_cred;
|
||||
credentials.openai = openai_cred;
|
||||
credentials
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue