chore: sync workspace changes

This commit is contained in:
Nathan Flurry 2026-01-27 05:06:33 -08:00
parent d24f983e2c
commit bf58891edf
139 changed files with 5454 additions and 8986 deletions

View file

@ -29,6 +29,7 @@ pub enum AgentId {
Codex,
Opencode,
Amp,
Mock,
}
impl AgentId {
@ -38,6 +39,7 @@ impl AgentId {
AgentId::Codex => "codex",
AgentId::Opencode => "opencode",
AgentId::Amp => "amp",
AgentId::Mock => "mock",
}
}
@ -47,6 +49,7 @@ impl AgentId {
AgentId::Codex => "codex",
AgentId::Opencode => "opencode",
AgentId::Amp => "amp",
AgentId::Mock => "mock",
}
}
@ -56,6 +59,7 @@ impl AgentId {
"codex" => Some(AgentId::Codex),
"opencode" => Some(AgentId::Opencode),
"amp" => Some(AgentId::Amp),
"mock" => Some(AgentId::Mock),
_ => None,
}
}
@ -138,6 +142,11 @@ impl AgentManager {
AgentId::Codex => install_codex(&install_path, self.platform, options.version.as_deref())?,
AgentId::Opencode => install_opencode(&install_path, self.platform, options.version.as_deref())?,
AgentId::Amp => install_amp(&install_path, self.platform, options.version.as_deref())?,
AgentId::Mock => {
if !install_path.exists() {
fs::write(&install_path, b"mock")?;
}
}
}
Ok(InstallResult {
@ -147,6 +156,9 @@ impl AgentManager {
}
pub fn is_installed(&self, agent: AgentId) -> bool {
if agent == AgentId::Mock {
return true;
}
self.binary_path(agent).exists()
|| find_in_path(agent.binary_name()).is_some()
|| default_install_dir().join(agent.binary_name()).exists()
@ -157,6 +169,9 @@ impl AgentManager {
}
pub fn version(&self, agent: AgentId) -> Result<Option<String>, AgentError> {
if agent == AgentId::Mock {
return Ok(Some("builtin".to_string()));
}
let path = self.resolve_binary(agent)?;
let attempts = [vec!["--version"], vec!["version"], vec!["-V"]];
for args in attempts {
@ -173,6 +188,11 @@ impl AgentManager {
}
pub fn spawn(&self, agent: AgentId, options: SpawnOptions) -> Result<SpawnResult, AgentError> {
if agent == AgentId::Mock {
return Err(AgentError::UnsupportedAgent {
agent: agent.as_str().to_string(),
});
}
if agent == AgentId::Codex {
return self.spawn_codex_app_server(options);
}
@ -247,6 +267,11 @@ impl AgentManager {
events,
});
}
AgentId::Mock => {
return Err(AgentError::UnsupportedAgent {
agent: agent.as_str().to_string(),
});
}
}
for (key, value) in options.env {
@ -550,6 +575,11 @@ impl AgentManager {
AgentId::Amp => {
return Ok(build_amp_command(&path, &working_dir, options));
}
AgentId::Mock => {
return Err(AgentError::UnsupportedAgent {
agent: agent.as_str().to_string(),
});
}
}
for (key, value) in &options.env {
@ -844,6 +874,7 @@ fn extract_session_id(agent: AgentId, events: &[Value]) -> Option<String> {
return Some(id);
}
}
AgentId::Mock => {}
}
}
None
@ -921,6 +952,7 @@ fn extract_result_text(agent: AgentId, events: &[Value]) -> Option<String> {
Some(buffer)
}
}
AgentId::Mock => None,
}
}

View file

@ -137,6 +137,7 @@ pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigErr
}
credentials_with(anthropic_cred.clone(), openai_cred.clone())
}
AgentId::Mock => credentials_with(None, None),
};
configs.push(TestAgentConfig { agent, credentials });
}

View file

@ -1,5 +1,6 @@
//! Sandbox daemon core utilities.
//! Sandbox agent core utilities.
pub mod credentials;
pub mod router;
pub mod telemetry;
pub mod ui;

View file

@ -11,9 +11,10 @@ use sandbox_agent_agent_management::credentials::{
ProviderCredentials,
};
use sandbox_agent::router::{
AgentInstallRequest, AppState, AuthConfig, CreateSessionRequest, MessageRequest, MockConfig,
AgentInstallRequest, AppState, AuthConfig, CreateSessionRequest, MessageRequest,
PermissionReply, PermissionReplyRequest, QuestionReplyRequest,
};
use sandbox_agent::telemetry;
use sandbox_agent::router::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse};
use sandbox_agent::router::build_router;
use sandbox_agent::ui;
@ -28,8 +29,8 @@ const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 2468;
#[derive(Parser, Debug)]
#[command(name = "sandbox-daemon", bin_name = "sandbox-agent")]
#[command(about = "Sandbox daemon for managing coding agents", version)]
#[command(name = "sandbox-agent", bin_name = "sandbox-agent")]
#[command(about = "Sandbox agent server for managing coding agents", version)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
@ -43,7 +44,7 @@ struct Cli {
#[derive(Subcommand, Debug)]
enum Command {
/// Run the sandbox daemon HTTP server.
/// Run the sandbox agent HTTP server.
Server(ServerArgs),
/// Manage installed agents and their modes.
Agents(AgentsArgs),
@ -73,8 +74,8 @@ struct ServerArgs {
#[arg(long = "cors-allow-credentials", short = 'C')]
cors_allow_credentials: bool,
#[arg(long)]
mock: bool,
#[arg(long = "no-telemetry")]
no_telemetry: bool,
}
#[derive(Args, Debug)]
@ -280,7 +281,7 @@ struct CredentialsExtractEnvArgs {
#[derive(Debug, Error)]
enum CliError {
#[error("missing command: run `sandbox-daemon server` to start the daemon")]
#[error("missing command: run `sandbox-agent server` to start the server")]
MissingCommand,
#[error("missing --token or --no-token for server mode")]
MissingToken,
@ -337,12 +338,7 @@ fn run_server(cli: &Cli, server: &ServerArgs) -> Result<(), CliError> {
let agent_manager =
AgentManager::new(default_install_dir()).map_err(|err| CliError::Server(err.to_string()))?;
let mock = if server.mock {
MockConfig::enabled()
} else {
MockConfig::disabled()
};
let state = AppState::new(auth, agent_manager, mock);
let state = AppState::new(auth, agent_manager);
let mut router = build_router(state);
if let Some(cors) = build_cors_layer(server)? {
@ -360,7 +356,13 @@ fn run_server(cli: &Cli, server: &ServerArgs) -> Result<(), CliError> {
.build()
.map_err(|err| CliError::Server(err.to_string()))?;
let telemetry_enabled = telemetry::telemetry_enabled(server.no_telemetry);
runtime.block_on(async move {
if telemetry_enabled {
telemetry::log_enabled_message();
telemetry::spawn_telemetry_task();
}
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!(addr = %addr, "server listening");
if ui::is_enabled() {
@ -383,7 +385,7 @@ fn default_install_dir() -> PathBuf {
fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
match command {
Command::Server(_) => Err(CliError::Server(
"server subcommand must be invoked as `sandbox-daemon server`".to_string(),
"server subcommand must be invoked as `sandbox-agent server`".to_string(),
)),
Command::Agents(subcommand) => run_agents(&subcommand.command, cli),
Command::Sessions(subcommand) => run_sessions(&subcommand.command, cli),

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,373 @@
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::Client;
use serde::Serialize;
use time::OffsetDateTime;
const TELEMETRY_URL: &str = "https://tc.rivet.dev";
const TELEMETRY_ENV_DEBUG: &str = "SANDBOX_AGENT_TELEMETRY_DEBUG";
const TELEMETRY_ID_FILE: &str = "telemetry_id";
const TELEMETRY_TIMEOUT_MS: u64 = 800;
#[derive(Debug, Serialize)]
struct TelemetryEvent {
// p = project identifier
p: String,
// dt = unix timestamp (seconds)
dt: i64,
// et = entity type
et: String,
// eid = unique entity id
eid: String,
// ev = event name
ev: String,
// d = data payload
d: TelemetryData,
// v = schema version
v: u8,
}
#[derive(Debug, Serialize)]
struct TelemetryData {
version: String,
os: OsInfo,
provider: ProviderInfo,
}
#[derive(Debug, Serialize)]
struct OsInfo {
name: String,
arch: String,
family: String,
}
#[derive(Debug, Serialize)]
struct ProviderInfo {
name: String,
confidence: String,
#[serde(skip_serializing_if = "Option::is_none")]
method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<HashMap<String, String>>,
}
pub fn telemetry_enabled(no_telemetry: bool) -> bool {
if no_telemetry {
return false;
}
if cfg!(debug_assertions) {
return env::var(TELEMETRY_ENV_DEBUG)
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE"))
.unwrap_or(false);
}
true
}
pub fn log_enabled_message() {
tracing::info!("anonymous telemetry is enabled; disable with --no-telemetry");
}
pub fn spawn_telemetry_task() {
let event = build_event();
tokio::spawn(async move {
let client = match Client::builder()
.timeout(Duration::from_millis(TELEMETRY_TIMEOUT_MS))
.build()
{
Ok(client) => client,
Err(err) => {
tracing::debug!(error = %err, "failed to build telemetry client");
return;
}
};
if let Err(err) = client.post(TELEMETRY_URL).json(&event).send().await {
tracing::debug!(error = %err, "telemetry request failed");
}
});
}
fn build_event() -> TelemetryEvent {
let dt = OffsetDateTime::now_utc().unix_timestamp();
let eid = load_or_create_id();
TelemetryEvent {
p: "sandbox-agent".to_string(),
dt,
et: "sandbox".to_string(),
eid,
ev: "entity_snapshot".to_string(),
d: TelemetryData {
version: env!("CARGO_PKG_VERSION").to_string(),
os: OsInfo {
name: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
family: std::env::consts::FAMILY.to_string(),
},
provider: detect_provider(),
},
v: 1,
}
}
fn load_or_create_id() -> String {
let path = telemetry_id_path();
if let Ok(existing) = fs::read_to_string(&path) {
let trimmed = existing.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
let id = generate_id();
if let Some(parent) = path.parent() {
if let Err(err) = fs::create_dir_all(parent) {
tracing::debug!(error = %err, "failed to create telemetry directory");
return id;
}
}
if let Ok(mut file) = fs::OpenOptions::new().create(true).write(true).truncate(true).open(&path) {
let _ = file.write_all(id.as_bytes());
}
id
}
fn telemetry_id_path() -> PathBuf {
dirs::data_dir()
.map(|dir| dir.join("sandbox-agent").join(TELEMETRY_ID_FILE))
.unwrap_or_else(|| PathBuf::from(".sandbox-agent").join(TELEMETRY_ID_FILE))
}
fn generate_id() -> String {
let mut bytes = [0u8; 16];
if read_random_bytes(&mut bytes) {
return hex_encode(&bytes);
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id() as u128;
let mixed = now ^ (pid << 64);
bytes = mixed.to_le_bytes();
hex_encode(&bytes)
}
fn read_random_bytes(buf: &mut [u8]) -> bool {
let path = Path::new("/dev/urandom");
let mut file = match fs::File::open(path) {
Ok(file) => file,
Err(_) => return false,
};
file.read_exact(buf).is_ok()
}
fn hex_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push_str(&format!("{:02x}", byte));
}
out
}
fn detect_provider() -> ProviderInfo {
if env::var("E2B_SANDBOX").as_deref() == Ok("true") {
let metadata = metadata_or_none([
("sandboxId", env::var("E2B_SANDBOX_ID").ok()),
("teamId", env::var("E2B_TEAM_ID").ok()),
("templateId", env::var("E2B_TEMPLATE_ID").ok()),
]);
return ProviderInfo {
name: "e2b".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("VERCEL").as_deref() == Ok("1") {
let runtime = if env::var("VERCEL_SANDBOX").is_ok() {
"sandbox"
} else if env::var("LAMBDA_TASK_ROOT").is_ok() {
"serverless"
} else {
"static"
};
let metadata = metadata_or_none([
("env", env::var("VERCEL_ENV").ok()),
("region", env::var("VERCEL_REGION").ok()),
("runtime", Some(runtime.to_string())),
]);
return ProviderInfo {
name: "vercel".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("MODAL_IS_REMOTE").as_deref() == Ok("1") || env::var("MODAL_CLOUD_PROVIDER").is_ok() {
let metadata = metadata_or_none([
("cloudProvider", env::var("MODAL_CLOUD_PROVIDER").ok()),
("region", env::var("MODAL_REGION").ok()),
]);
return ProviderInfo {
name: "modal".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("FLY_APP_NAME").is_ok() || env::var("FLY_MACHINE_ID").is_ok() {
let metadata = metadata_or_none([
("appName", env::var("FLY_APP_NAME").ok()),
("region", env::var("FLY_REGION").ok()),
]);
return ProviderInfo {
name: "fly.io".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("REPL_ID").is_ok() || env::var("REPL_SLUG").is_ok() {
let metadata = metadata_or_none([
("replId", env::var("REPL_ID").ok()),
("owner", env::var("REPL_OWNER").ok()),
]);
return ProviderInfo {
name: "replit".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("CODESANDBOX_HOST").is_ok() || env::var("CSB_BASE_PREVIEW_HOST").is_ok() {
return ProviderInfo {
name: "codesandbox".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata: None,
};
}
if env::var("CODESPACES").as_deref() == Ok("true") {
let metadata = metadata_or_none([("name", env::var("CODESPACE_NAME").ok())]);
return ProviderInfo {
name: "github-codespaces".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("RAILWAY_ENVIRONMENT").is_ok() {
let metadata = metadata_or_none([("environment", env::var("RAILWAY_ENVIRONMENT").ok())]);
return ProviderInfo {
name: "railway".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if env::var("RENDER").as_deref() == Ok("true") {
let metadata = metadata_or_none([("serviceId", env::var("RENDER_SERVICE_ID").ok())]);
return ProviderInfo {
name: "render".to_string(),
confidence: "high".to_string(),
method: Some("env".to_string()),
metadata,
};
}
if detect_daytona() {
return ProviderInfo {
name: "daytona".to_string(),
confidence: "medium".to_string(),
method: Some("filesystem".to_string()),
metadata: None,
};
}
if detect_docker() {
return ProviderInfo {
name: "docker".to_string(),
confidence: "high".to_string(),
method: Some("filesystem".to_string()),
metadata: None,
};
}
ProviderInfo {
name: "unknown".to_string(),
confidence: "low".to_string(),
method: None,
metadata: None,
}
}
fn detect_daytona() -> bool {
let mut signals = 0;
let username = env::var("USER")
.or_else(|_| env::var("USERNAME"))
.unwrap_or_default();
if username == "daytona" {
signals += 1;
}
if Path::new("/home/daytona").exists() {
signals += 1;
}
if let Some(home) = dirs::home_dir() {
if home.join(".daytona").exists() {
signals += 1;
}
}
signals >= 2
}
fn detect_docker() -> bool {
if Path::new("/.dockerenv").exists() {
return true;
}
if Path::new("/run/.containerenv").exists() {
return true;
}
if let Ok(cgroup) = fs::read_to_string("/proc/1/cgroup") {
let lower = cgroup.to_lowercase();
if lower.contains("docker") || lower.contains("containerd") {
return true;
}
}
false
}
fn filter_metadata(pairs: impl IntoIterator<Item = (&'static str, Option<String>)>) -> HashMap<String, String> {
let mut map = HashMap::new();
for (key, value) in pairs {
if let Some(value) = value {
if !value.is_empty() {
map.insert(key.to_string(), value);
}
}
}
map
}
fn metadata_or_none(pairs: impl IntoIterator<Item = (&'static str, Option<String>)>) -> Option<HashMap<String, String>> {
let map = filter_metadata(pairs);
if map.is_empty() {
None
} else {
Some(map)
}
}

View file

@ -17,7 +17,6 @@ use sandbox_agent::router::{
AgentCapabilities,
AgentListResponse,
AuthConfig,
MockConfig,
};
const PROMPT: &str = "Reply with exactly the single word OK.";
@ -42,11 +41,7 @@ impl TestApp {
let install_dir = tempfile::tempdir().expect("create temp install dir");
let manager = AgentManager::new(install_dir.path())
.expect("create agent manager");
let state = sandbox_agent::router::AppState::new(
AuthConfig::disabled(),
manager,
MockConfig::disabled(),
);
let state = sandbox_agent::router::AppState::new(AuthConfig::disabled(), manager);
let app = build_router(state);
Self {
app,

View file

@ -12,7 +12,7 @@ use tempfile::TempDir;
use sandbox_agent_agent_management::agents::{AgentId, AgentManager};
use sandbox_agent_agent_management::testing::{test_agents_from_env, TestAgentConfig};
use sandbox_agent_agent_credentials::ExtractedCredentials;
use sandbox_agent::router::{build_router, AppState, AuthConfig, MockConfig};
use sandbox_agent::router::{build_router, AppState, AuthConfig};
use tower::util::ServiceExt;
use tower_http::cors::CorsLayer;
@ -39,7 +39,7 @@ impl TestApp {
let install_dir = tempfile::tempdir().expect("create temp install dir");
let manager = AgentManager::new(install_dir.path())
.expect("create agent manager");
let state = AppState::new(auth, manager, MockConfig::disabled());
let state = AppState::new(auth, manager);
let mut app = build_router(state);
if let Some(cors) = cors {
app = app.layer(cors);

View file

@ -2,7 +2,7 @@ use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use sandbox_agent_agent_management::agents::AgentManager;
use sandbox_agent::router::{build_router, AppState, AuthConfig, MockConfig};
use sandbox_agent::router::{build_router, AppState, AuthConfig};
use sandbox_agent::ui;
use tempfile::TempDir;
use tower::util::ServiceExt;
@ -15,7 +15,7 @@ async fn serves_inspector_ui() {
let install_dir = TempDir::new().expect("create temp install dir");
let manager = AgentManager::new(install_dir.path()).expect("create agent manager");
let state = AppState::new(AuthConfig::disabled(), manager, MockConfig::disabled());
let state = AppState::new(AuthConfig::disabled(), manager);
let app = build_router(state);
let request = Request::builder()