use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use thiserror::Error; use utoipa::ToSchema; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ErrorType { InvalidRequest, UnsupportedAgent, AgentNotInstalled, InstallFailed, AgentProcessExited, TokenInvalid, PermissionDenied, SessionNotFound, SessionAlreadyExists, ModeNotSupported, StreamError, Timeout, } impl ErrorType { pub fn as_urn(&self) -> &'static str { match self { Self::InvalidRequest => "urn:sandbox-agent:error:invalid_request", Self::UnsupportedAgent => "urn:sandbox-agent:error:unsupported_agent", Self::AgentNotInstalled => "urn:sandbox-agent:error:agent_not_installed", Self::InstallFailed => "urn:sandbox-agent:error:install_failed", Self::AgentProcessExited => "urn:sandbox-agent:error:agent_process_exited", Self::TokenInvalid => "urn:sandbox-agent:error:token_invalid", Self::PermissionDenied => "urn:sandbox-agent:error:permission_denied", Self::SessionNotFound => "urn:sandbox-agent:error:session_not_found", Self::SessionAlreadyExists => "urn:sandbox-agent:error:session_already_exists", Self::ModeNotSupported => "urn:sandbox-agent:error:mode_not_supported", Self::StreamError => "urn:sandbox-agent:error:stream_error", Self::Timeout => "urn:sandbox-agent:error:timeout", } } pub fn title(&self) -> &'static str { match self { Self::InvalidRequest => "Invalid Request", Self::UnsupportedAgent => "Unsupported Agent", Self::AgentNotInstalled => "Agent Not Installed", Self::InstallFailed => "Install Failed", Self::AgentProcessExited => "Agent Process Exited", Self::TokenInvalid => "Token Invalid", Self::PermissionDenied => "Permission Denied", Self::SessionNotFound => "Session Not Found", Self::SessionAlreadyExists => "Session Already Exists", Self::ModeNotSupported => "Mode Not Supported", Self::StreamError => "Stream Error", Self::Timeout => "Timeout", } } pub fn status_code(&self) -> u16 { match self { Self::InvalidRequest => 400, Self::UnsupportedAgent => 400, Self::AgentNotInstalled => 404, Self::InstallFailed => 500, Self::AgentProcessExited => 500, Self::TokenInvalid => 401, Self::PermissionDenied => 403, Self::SessionNotFound => 404, Self::SessionAlreadyExists => 409, Self::ModeNotSupported => 400, Self::StreamError => 502, Self::Timeout => 504, } } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct ProblemDetails { #[serde(rename = "type")] pub type_: String, pub title: String, pub status: u16, #[serde(default, skip_serializing_if = "Option::is_none")] pub detail: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub instance: Option, #[serde(flatten, default, skip_serializing_if = "Map::is_empty")] pub extensions: Map, } impl ProblemDetails { pub fn new(error_type: ErrorType, detail: Option) -> Self { Self { type_: error_type.as_urn().to_string(), title: error_type.title().to_string(), status: error_type.status_code(), detail, instance: None, extensions: Map::new(), } } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct AgentError { #[serde(rename = "type")] pub type_: ErrorType, pub message: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub details: Option, } #[derive(Debug, Error)] pub enum SandboxError { #[error("invalid request: {message}")] InvalidRequest { message: String }, #[error("unsupported agent: {agent}")] UnsupportedAgent { agent: String }, #[error("agent not installed: {agent}")] AgentNotInstalled { agent: String }, #[error("install failed: {agent}")] InstallFailed { agent: String, stderr: Option }, #[error("agent process exited: {agent}")] AgentProcessExited { agent: String, exit_code: Option, stderr: Option, }, #[error("token invalid")] TokenInvalid { message: Option }, #[error("permission denied")] PermissionDenied { message: Option }, #[error("session not found: {session_id}")] SessionNotFound { session_id: String }, #[error("session already exists: {session_id}")] SessionAlreadyExists { session_id: String }, #[error("mode not supported: {agent} {mode}")] ModeNotSupported { agent: String, mode: String }, #[error("stream error: {message}")] StreamError { message: String }, #[error("timeout")] Timeout { message: Option }, } impl SandboxError { pub fn error_type(&self) -> ErrorType { match self { Self::InvalidRequest { .. } => ErrorType::InvalidRequest, Self::UnsupportedAgent { .. } => ErrorType::UnsupportedAgent, Self::AgentNotInstalled { .. } => ErrorType::AgentNotInstalled, Self::InstallFailed { .. } => ErrorType::InstallFailed, Self::AgentProcessExited { .. } => ErrorType::AgentProcessExited, Self::TokenInvalid { .. } => ErrorType::TokenInvalid, Self::PermissionDenied { .. } => ErrorType::PermissionDenied, Self::SessionNotFound { .. } => ErrorType::SessionNotFound, Self::SessionAlreadyExists { .. } => ErrorType::SessionAlreadyExists, Self::ModeNotSupported { .. } => ErrorType::ModeNotSupported, Self::StreamError { .. } => ErrorType::StreamError, Self::Timeout { .. } => ErrorType::Timeout, } } pub fn to_agent_error(&self) -> AgentError { let (agent, session_id, details) = match self { Self::InvalidRequest { .. } => (None, None, None), Self::UnsupportedAgent { agent } => { (Some(agent.clone()), None, None) } Self::AgentNotInstalled { agent } => (Some(agent.clone()), None, None), Self::InstallFailed { agent, stderr } => { let mut map = Map::new(); if let Some(stderr) = stderr { map.insert("stderr".to_string(), Value::String(stderr.clone())); } ( Some(agent.clone()), None, if map.is_empty() { None } else { Some(Value::Object(map)) }, ) } Self::AgentProcessExited { agent, exit_code, stderr, } => { let mut map = Map::new(); if let Some(code) = exit_code { map.insert( "exitCode".to_string(), Value::Number(serde_json::Number::from(*code as i64)), ); } if let Some(stderr) = stderr { map.insert("stderr".to_string(), Value::String(stderr.clone())); } ( Some(agent.clone()), None, if map.is_empty() { None } else { Some(Value::Object(map)) }, ) } Self::TokenInvalid { message } => { let details = message.as_ref().map(|msg| { let mut map = Map::new(); map.insert("message".to_string(), Value::String(msg.clone())); Value::Object(map) }); (None, None, details) } Self::PermissionDenied { message } => { let details = message.as_ref().map(|msg| { let mut map = Map::new(); map.insert("message".to_string(), Value::String(msg.clone())); Value::Object(map) }); (None, None, details) } Self::SessionNotFound { session_id } => { (None, Some(session_id.clone()), None) } Self::SessionAlreadyExists { session_id } => { (None, Some(session_id.clone()), None) } Self::ModeNotSupported { agent, mode } => { let mut map = Map::new(); map.insert("mode".to_string(), Value::String(mode.clone())); ( Some(agent.clone()), None, Some(Value::Object(map)), ) } Self::StreamError { message } => { let mut map = Map::new(); map.insert("message".to_string(), Value::String(message.clone())); (None, None, Some(Value::Object(map))) } Self::Timeout { message } => { let details = message.as_ref().map(|msg| { let mut map = Map::new(); map.insert("message".to_string(), Value::String(msg.clone())); Value::Object(map) }); (None, None, details) } }; AgentError { type_: self.error_type(), message: self.to_string(), agent, session_id, details, } } pub fn to_problem_details(&self) -> ProblemDetails { let mut problem = ProblemDetails::new(self.error_type(), Some(self.to_string())); let agent_error = self.to_agent_error(); let mut extensions = Map::new(); if let Some(agent) = agent_error.agent { extensions.insert("agent".to_string(), Value::String(agent)); } if let Some(session_id) = agent_error.session_id { extensions.insert("sessionId".to_string(), Value::String(session_id)); } if let Some(details) = agent_error.details { extensions.insert("details".to_string(), details); } problem.extensions = extensions; problem } } impl From for ProblemDetails { fn from(value: SandboxError) -> Self { value.to_problem_details() } } impl From<&SandboxError> for ProblemDetails { fn from(value: &SandboxError) -> Self { value.to_problem_details() } } impl From for AgentError { fn from(value: SandboxError) -> Self { value.to_agent_error() } } impl From<&SandboxError> for AgentError { fn from(value: &SandboxError) -> Self { value.to_agent_error() } }