1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4use thiserror::Error;
5use utoipa::ToSchema;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema)]
8#[serde(rename_all = "snake_case")]
9pub enum ErrorType {
10 InvalidRequest,
11 Conflict,
12 UnsupportedAgent,
13 AgentNotInstalled,
14 InstallFailed,
15 AgentProcessExited,
16 TokenInvalid,
17 PermissionDenied,
18 NotAcceptable,
19 UnsupportedMediaType,
20 NotFound,
21 SessionNotFound,
22 SessionAlreadyExists,
23 ModeNotSupported,
24 StreamError,
25 Timeout,
26}
27
28impl ErrorType {
29 pub fn as_urn(&self) -> &'static str {
30 match self {
31 Self::InvalidRequest => "urn:sandbox-agent:error:invalid_request",
32 Self::Conflict => "urn:sandbox-agent:error:conflict",
33 Self::UnsupportedAgent => "urn:sandbox-agent:error:unsupported_agent",
34 Self::AgentNotInstalled => "urn:sandbox-agent:error:agent_not_installed",
35 Self::InstallFailed => "urn:sandbox-agent:error:install_failed",
36 Self::AgentProcessExited => "urn:sandbox-agent:error:agent_process_exited",
37 Self::TokenInvalid => "urn:sandbox-agent:error:token_invalid",
38 Self::PermissionDenied => "urn:sandbox-agent:error:permission_denied",
39 Self::NotAcceptable => "urn:sandbox-agent:error:not_acceptable",
40 Self::UnsupportedMediaType => "urn:sandbox-agent:error:unsupported_media_type",
41 Self::NotFound => "urn:sandbox-agent:error:not_found",
42 Self::SessionNotFound => "urn:sandbox-agent:error:session_not_found",
43 Self::SessionAlreadyExists => "urn:sandbox-agent:error:session_already_exists",
44 Self::ModeNotSupported => "urn:sandbox-agent:error:mode_not_supported",
45 Self::StreamError => "urn:sandbox-agent:error:stream_error",
46 Self::Timeout => "urn:sandbox-agent:error:timeout",
47 }
48 }
49
50 pub fn title(&self) -> &'static str {
51 match self {
52 Self::InvalidRequest => "Invalid Request",
53 Self::Conflict => "Conflict",
54 Self::UnsupportedAgent => "Unsupported Agent",
55 Self::AgentNotInstalled => "Agent Not Installed",
56 Self::InstallFailed => "Install Failed",
57 Self::AgentProcessExited => "Agent Process Exited",
58 Self::TokenInvalid => "Token Invalid",
59 Self::PermissionDenied => "Permission Denied",
60 Self::NotAcceptable => "Not Acceptable",
61 Self::UnsupportedMediaType => "Unsupported Media Type",
62 Self::NotFound => "Not Found",
63 Self::SessionNotFound => "Session Not Found",
64 Self::SessionAlreadyExists => "Session Already Exists",
65 Self::ModeNotSupported => "Mode Not Supported",
66 Self::StreamError => "Stream Error",
67 Self::Timeout => "Timeout",
68 }
69 }
70
71 pub fn status_code(&self) -> u16 {
72 match self {
73 Self::InvalidRequest => 400,
74 Self::Conflict => 409,
75 Self::UnsupportedAgent => 400,
76 Self::AgentNotInstalled => 404,
77 Self::InstallFailed => 500,
78 Self::AgentProcessExited => 500,
79 Self::TokenInvalid => 401,
80 Self::PermissionDenied => 403,
81 Self::NotAcceptable => 406,
82 Self::UnsupportedMediaType => 415,
83 Self::NotFound => 404,
84 Self::SessionNotFound => 404,
85 Self::SessionAlreadyExists => 409,
86 Self::ModeNotSupported => 400,
87 Self::StreamError => 502,
88 Self::Timeout => 504,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
94pub struct ProblemDetails {
95 #[serde(rename = "type")]
96 pub type_: String,
97 pub title: String,
98 pub status: u16,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub detail: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub instance: Option<String>,
103 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
104 pub extensions: Map<String, Value>,
105}
106
107impl ProblemDetails {
108 pub fn new(error_type: ErrorType, detail: Option<String>) -> Self {
109 Self {
110 type_: error_type.as_urn().to_string(),
111 title: error_type.title().to_string(),
112 status: error_type.status_code(),
113 detail,
114 instance: None,
115 extensions: Map::new(),
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
121pub struct AgentError {
122 #[serde(rename = "type")]
123 pub type_: ErrorType,
124 pub message: String,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub agent: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub session_id: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub details: Option<Value>,
131}
132
133#[derive(Debug, Error)]
134pub enum SandboxError {
135 #[error("invalid request: {message}")]
136 InvalidRequest { message: String },
137 #[error("conflict: {message}")]
138 Conflict { message: String },
139 #[error("unsupported agent: {agent}")]
140 UnsupportedAgent { agent: String },
141 #[error("agent not installed: {agent}")]
142 AgentNotInstalled { agent: String },
143 #[error("install failed: {agent}")]
144 InstallFailed {
145 agent: String,
146 stderr: Option<String>,
147 },
148 #[error("agent process exited: {agent}")]
149 AgentProcessExited {
150 agent: String,
151 exit_code: Option<i32>,
152 stderr: Option<String>,
153 },
154 #[error("token invalid")]
155 TokenInvalid { message: Option<String> },
156 #[error("permission denied")]
157 PermissionDenied { message: Option<String> },
158 #[error("not acceptable: {message}")]
159 NotAcceptable { message: String },
160 #[error("unsupported media type: {message}")]
161 UnsupportedMediaType { message: String },
162 #[error("not found: {resource} {id}")]
163 NotFound { resource: String, id: String },
164 #[error("session not found: {session_id}")]
165 SessionNotFound { session_id: String },
166 #[error("session already exists: {session_id}")]
167 SessionAlreadyExists { session_id: String },
168 #[error("mode not supported: {agent} {mode}")]
169 ModeNotSupported { agent: String, mode: String },
170 #[error("stream error: {message}")]
171 StreamError { message: String },
172 #[error("timeout")]
173 Timeout { message: Option<String> },
174}
175
176impl SandboxError {
177 pub fn error_type(&self) -> ErrorType {
178 match self {
179 Self::InvalidRequest { .. } => ErrorType::InvalidRequest,
180 Self::Conflict { .. } => ErrorType::Conflict,
181 Self::UnsupportedAgent { .. } => ErrorType::UnsupportedAgent,
182 Self::AgentNotInstalled { .. } => ErrorType::AgentNotInstalled,
183 Self::InstallFailed { .. } => ErrorType::InstallFailed,
184 Self::AgentProcessExited { .. } => ErrorType::AgentProcessExited,
185 Self::TokenInvalid { .. } => ErrorType::TokenInvalid,
186 Self::PermissionDenied { .. } => ErrorType::PermissionDenied,
187 Self::NotAcceptable { .. } => ErrorType::NotAcceptable,
188 Self::UnsupportedMediaType { .. } => ErrorType::UnsupportedMediaType,
189 Self::NotFound { .. } => ErrorType::NotFound,
190 Self::SessionNotFound { .. } => ErrorType::SessionNotFound,
191 Self::SessionAlreadyExists { .. } => ErrorType::SessionAlreadyExists,
192 Self::ModeNotSupported { .. } => ErrorType::ModeNotSupported,
193 Self::StreamError { .. } => ErrorType::StreamError,
194 Self::Timeout { .. } => ErrorType::Timeout,
195 }
196 }
197
198 pub fn to_agent_error(&self) -> AgentError {
199 let (agent, session_id, details) = match self {
200 Self::InvalidRequest { .. } => (None, None, None),
201 Self::Conflict { message } => {
202 let mut map = Map::new();
203 map.insert("message".to_string(), Value::String(message.clone()));
204 (None, None, Some(Value::Object(map)))
205 }
206 Self::UnsupportedAgent { agent } => (Some(agent.clone()), None, None),
207 Self::AgentNotInstalled { agent } => (Some(agent.clone()), None, None),
208 Self::InstallFailed { agent, stderr } => {
209 let mut map = Map::new();
210 if let Some(stderr) = stderr {
211 map.insert("stderr".to_string(), Value::String(stderr.clone()));
212 }
213 (
214 Some(agent.clone()),
215 None,
216 if map.is_empty() {
217 None
218 } else {
219 Some(Value::Object(map))
220 },
221 )
222 }
223 Self::AgentProcessExited {
224 agent,
225 exit_code,
226 stderr,
227 } => {
228 let mut map = Map::new();
229 if let Some(code) = exit_code {
230 map.insert(
231 "exitCode".to_string(),
232 Value::Number(serde_json::Number::from(*code as i64)),
233 );
234 }
235 if let Some(stderr) = stderr {
236 map.insert("stderr".to_string(), Value::String(stderr.clone()));
237 }
238 (
239 Some(agent.clone()),
240 None,
241 if map.is_empty() {
242 None
243 } else {
244 Some(Value::Object(map))
245 },
246 )
247 }
248 Self::TokenInvalid { message } => {
249 let details = message.as_ref().map(|msg| {
250 let mut map = Map::new();
251 map.insert("message".to_string(), Value::String(msg.clone()));
252 Value::Object(map)
253 });
254 (None, None, details)
255 }
256 Self::PermissionDenied { message } => {
257 let details = message.as_ref().map(|msg| {
258 let mut map = Map::new();
259 map.insert("message".to_string(), Value::String(msg.clone()));
260 Value::Object(map)
261 });
262 (None, None, details)
263 }
264 Self::NotAcceptable { message } => {
265 let mut map = Map::new();
266 map.insert("message".to_string(), Value::String(message.clone()));
267 (None, None, Some(Value::Object(map)))
268 }
269 Self::UnsupportedMediaType { message } => {
270 let mut map = Map::new();
271 map.insert("message".to_string(), Value::String(message.clone()));
272 (None, None, Some(Value::Object(map)))
273 }
274 Self::NotFound { resource, id } => {
275 let mut map = Map::new();
276 map.insert("resource".to_string(), Value::String(resource.clone()));
277 map.insert("id".to_string(), Value::String(id.clone()));
278 (None, None, Some(Value::Object(map)))
279 }
280 Self::SessionNotFound { session_id } => (None, Some(session_id.clone()), None),
281 Self::SessionAlreadyExists { session_id } => (None, Some(session_id.clone()), None),
282 Self::ModeNotSupported { agent, mode } => {
283 let mut map = Map::new();
284 map.insert("mode".to_string(), Value::String(mode.clone()));
285 (Some(agent.clone()), None, Some(Value::Object(map)))
286 }
287 Self::StreamError { message } => {
288 let mut map = Map::new();
289 map.insert("message".to_string(), Value::String(message.clone()));
290 (None, None, Some(Value::Object(map)))
291 }
292 Self::Timeout { message } => {
293 let details = message.as_ref().map(|msg| {
294 let mut map = Map::new();
295 map.insert("message".to_string(), Value::String(msg.clone()));
296 Value::Object(map)
297 });
298 (None, None, details)
299 }
300 };
301
302 AgentError {
303 type_: self.error_type(),
304 message: self.to_string(),
305 agent,
306 session_id,
307 details,
308 }
309 }
310
311 pub fn to_problem_details(&self) -> ProblemDetails {
312 let mut problem = ProblemDetails::new(self.error_type(), Some(self.to_string()));
313 let agent_error = self.to_agent_error();
314
315 let mut extensions = Map::new();
316 if let Some(agent) = agent_error.agent {
317 extensions.insert("agent".to_string(), Value::String(agent));
318 }
319 if let Some(session_id) = agent_error.session_id {
320 extensions.insert("sessionId".to_string(), Value::String(session_id));
321 }
322 if let Some(details) = agent_error.details {
323 extensions.insert("details".to_string(), details);
324 }
325 problem.extensions = extensions;
326 problem
327 }
328}
329
330impl From<SandboxError> for ProblemDetails {
331 fn from(value: SandboxError) -> Self {
332 value.to_problem_details()
333 }
334}
335
336impl From<&SandboxError> for ProblemDetails {
337 fn from(value: &SandboxError) -> Self {
338 value.to_problem_details()
339 }
340}
341
342impl From<SandboxError> for AgentError {
343 fn from(value: SandboxError) -> Self {
344 value.to_agent_error()
345 }
346}
347
348impl From<&SandboxError> for AgentError {
349 fn from(value: &SandboxError) -> Self {
350 value.to_agent_error()
351 }
352}