1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use serde_json::json;
4
5pub type CiabResult<T> = Result<T, CiabError>;
6
7#[derive(Debug, thiserror::Error)]
8pub enum CiabError {
9 #[error("sandbox not found: {0}")]
11 SandboxNotFound(String),
12 #[error("sandbox already exists: {0}")]
13 SandboxAlreadyExists(String),
14 #[error("sandbox in invalid state: {current}, expected: {expected}")]
15 SandboxInvalidState { current: String, expected: String },
16 #[error("sandbox creation failed: {0}")]
17 SandboxCreationFailed(String),
18 #[error("sandbox timeout: {0}")]
19 SandboxTimeout(String),
20
21 #[error("agent provider not found: {0}")]
23 AgentProviderNotFound(String),
24 #[error("agent not running")]
25 AgentNotRunning,
26 #[error("agent communication error: {0}")]
27 AgentCommunicationError(String),
28
29 #[error("session not found: {0}")]
31 SessionNotFound(String),
32 #[error("session in invalid state: {0}")]
33 SessionInvalidState(String),
34
35 #[error("workspace not found: {0}")]
37 WorkspaceNotFound(String),
38 #[error("workspace already exists: {0}")]
39 WorkspaceAlreadyExists(String),
40 #[error("workspace validation error: {0}")]
41 WorkspaceValidationError(String),
42
43 #[error("template source not found: {0}")]
45 TemplateSourceNotFound(String),
46 #[error("template sync failed: {0}")]
47 TemplateSyncFailed(String),
48
49 #[error("credential not found: {0}")]
51 CredentialNotFound(String),
52 #[error("decryption failed: {0}")]
53 DecryptionFailed(String),
54 #[error("OAuth flow failed: {0}")]
55 OAuthFlowFailed(String),
56 #[error("OAuth token expired")]
57 OAuthTokenExpired,
58
59 #[error("runtime unavailable: {0}")]
61 RuntimeUnavailable(String),
62 #[error("OpenSandbox error: {0}")]
63 OpenSandboxError(String),
64 #[error("kubernetes error: {0}")]
65 KubernetesError(String),
66 #[error("kubernetes pod not found: {0}")]
67 KubernetesPodNotFound(String),
68
69 #[error("EC2 error: {0}")]
70 Ec2Error(String),
71
72 #[error("SSH error: {0}")]
73 SshError(String),
74
75 #[error("Packer error: {0}")]
76 PackerError(String),
77
78 #[error("Image build error: {0}")]
79 ImageBuildError(String),
80
81 #[error("Resource resolution error: {0}")]
82 ResourceResolutionError(String),
83
84 #[error("Unsupported operation: {0}")]
85 Unsupported(String),
86
87 #[error("provisioning failed: {0}")]
89 ProvisioningFailed(String),
90 #[error("git clone failed: {0}")]
91 GitCloneFailed(String),
92 #[error("script execution failed: {0}")]
93 ScriptExecutionFailed(String),
94 #[error("local mount failed: {0}")]
95 LocalMountFailed(String),
96 #[error("git worktree failed: {0}")]
97 GitWorktreeFailed(String),
98 #[error("agentfs error: {0}")]
99 AgentFsError(String),
100
101 #[error("stream buffer overflow")]
103 StreamBufferOverflow,
104 #[error("stream connection lost")]
105 StreamConnectionLost,
106
107 #[error("file not found: {0}")]
109 FileNotFound(String),
110 #[error("exec failed: {0}")]
111 ExecFailed(String),
112
113 #[error("configuration error: {0}")]
115 ConfigError(String),
116 #[error("configuration validation error: {0}")]
117 ConfigValidationError(String),
118
119 #[error("unauthorized: {0}")]
121 Unauthorized(String),
122 #[error("forbidden: {0}")]
123 Forbidden(String),
124 #[error("rate limited")]
125 RateLimited,
126
127 #[error("tunnel not found: {0}")]
129 TunnelNotFound(String),
130 #[error("tunnel creation failed: {0}")]
131 TunnelCreationFailed(String),
132 #[error("client token not found: {0}")]
133 ClientTokenNotFound(String),
134 #[error("client token expired")]
135 ClientTokenExpired,
136 #[error("client token revoked")]
137 ClientTokenRevoked,
138 #[error("insufficient scope: {0}")]
139 InsufficientScope(String),
140 #[error("gateway not enabled")]
141 GatewayNotEnabled,
142 #[error("FRP error: {0}")]
143 FrpError(String),
144 #[error("tunnel provider error: {0}")]
145 TunnelProviderError(String),
146 #[error("tunnel provider not ready: {0}")]
147 TunnelProviderNotReady(String),
148
149 #[error("channel not found: {0}")]
151 ChannelNotFound(String),
152 #[error("channel adapter error: {0}")]
153 ChannelAdapterError(String),
154 #[error("channel sender not allowed: {0}")]
155 ChannelSenderNotAllowed(String),
156
157 #[error("internal error: {0}")]
159 Internal(String),
160 #[error("external error: {0}")]
161 External(String),
162 #[error("timeout: {0}")]
163 Timeout(String),
164
165 #[error("database error: {0}")]
167 Database(String),
168 #[error("serialization error: {0}")]
169 Serialization(#[from] serde_json::Error),
170}
171
172impl CiabError {
173 pub fn status_code(&self) -> u16 {
174 match self {
175 Self::SandboxNotFound(_)
176 | Self::SessionNotFound(_)
177 | Self::WorkspaceNotFound(_)
178 | Self::CredentialNotFound(_)
179 | Self::FileNotFound(_)
180 | Self::AgentProviderNotFound(_)
181 | Self::TemplateSourceNotFound(_)
182 | Self::TunnelNotFound(_)
183 | Self::ClientTokenNotFound(_)
184 | Self::ChannelNotFound(_) => 404,
185 Self::SandboxAlreadyExists(_) | Self::WorkspaceAlreadyExists(_) => 409,
186 Self::SandboxInvalidState { .. } | Self::SessionInvalidState(_) => 409,
187 Self::Unauthorized(_) => 401,
188 Self::Forbidden(_)
189 | Self::ClientTokenExpired
190 | Self::ClientTokenRevoked
191 | Self::InsufficientScope(_)
192 | Self::ChannelSenderNotAllowed(_) => 403,
193 Self::GatewayNotEnabled | Self::TunnelProviderNotReady(_) => 503,
194 Self::RateLimited => 429,
195 Self::ConfigError(_)
196 | Self::ConfigValidationError(_)
197 | Self::WorkspaceValidationError(_)
198 | Self::AgentCommunicationError(_)
199 | Self::TunnelProviderError(_) => 400,
200 Self::SandboxTimeout(_) | Self::Timeout(_) => 504,
201 Self::Ec2Error(_) | Self::SshError(_) => 502,
202 Self::PackerError(_) | Self::ImageBuildError(_) => 500,
203 Self::ResourceResolutionError(_) => 400,
204 Self::Unsupported(_) => 501,
205 _ => 500,
206 }
207 }
208
209 pub fn error_code(&self) -> &str {
210 match self {
211 Self::SandboxNotFound(_) => "sandbox_not_found",
212 Self::SandboxAlreadyExists(_) => "sandbox_already_exists",
213 Self::SandboxInvalidState { .. } => "sandbox_invalid_state",
214 Self::SandboxCreationFailed(_) => "sandbox_creation_failed",
215 Self::SandboxTimeout(_) => "sandbox_timeout",
216 Self::AgentProviderNotFound(_) => "agent_provider_not_found",
217 Self::AgentNotRunning => "agent_not_running",
218 Self::AgentCommunicationError(_) => "agent_communication_error",
219 Self::SessionNotFound(_) => "session_not_found",
220 Self::SessionInvalidState(_) => "session_invalid_state",
221 Self::WorkspaceNotFound(_) => "workspace_not_found",
222 Self::WorkspaceAlreadyExists(_) => "workspace_already_exists",
223 Self::WorkspaceValidationError(_) => "workspace_validation_error",
224 Self::TemplateSourceNotFound(_) => "template_source_not_found",
225 Self::TemplateSyncFailed(_) => "template_sync_failed",
226 Self::CredentialNotFound(_) => "credential_not_found",
227 Self::DecryptionFailed(_) => "decryption_failed",
228 Self::OAuthFlowFailed(_) => "oauth_flow_failed",
229 Self::OAuthTokenExpired => "oauth_token_expired",
230 Self::RuntimeUnavailable(_) => "runtime_unavailable",
231 Self::OpenSandboxError(_) => "opensandbox_error",
232 Self::KubernetesError(_) => "kubernetes_error",
233 Self::KubernetesPodNotFound(_) => "kubernetes_pod_not_found",
234 Self::Ec2Error(_) => "EC2_ERROR",
235 Self::SshError(_) => "SSH_ERROR",
236 Self::PackerError(_) => "PACKER_ERROR",
237 Self::ImageBuildError(_) => "IMAGE_BUILD_ERROR",
238 Self::ResourceResolutionError(_) => "RESOURCE_RESOLUTION_ERROR",
239 Self::Unsupported(_) => "UNSUPPORTED",
240 Self::ProvisioningFailed(_) => "provisioning_failed",
241 Self::GitCloneFailed(_) => "git_clone_failed",
242 Self::ScriptExecutionFailed(_) => "script_execution_failed",
243 Self::LocalMountFailed(_) => "local_mount_failed",
244 Self::GitWorktreeFailed(_) => "git_worktree_failed",
245 Self::AgentFsError(_) => "agentfs_error",
246 Self::StreamBufferOverflow => "stream_buffer_overflow",
247 Self::StreamConnectionLost => "stream_connection_lost",
248 Self::FileNotFound(_) => "file_not_found",
249 Self::ExecFailed(_) => "exec_failed",
250 Self::ConfigError(_) => "config_error",
251 Self::ConfigValidationError(_) => "config_validation_error",
252 Self::Unauthorized(_) => "unauthorized",
253 Self::Forbidden(_) => "forbidden",
254 Self::RateLimited => "rate_limited",
255 Self::TunnelNotFound(_) => "tunnel_not_found",
256 Self::TunnelCreationFailed(_) => "tunnel_creation_failed",
257 Self::ClientTokenNotFound(_) => "client_token_not_found",
258 Self::ClientTokenExpired => "client_token_expired",
259 Self::ClientTokenRevoked => "client_token_revoked",
260 Self::InsufficientScope(_) => "insufficient_scope",
261 Self::GatewayNotEnabled => "gateway_not_enabled",
262 Self::FrpError(_) => "frp_error",
263 Self::TunnelProviderError(_) => "tunnel_provider_error",
264 Self::TunnelProviderNotReady(_) => "tunnel_provider_not_ready",
265 Self::ChannelNotFound(_) => "channel_not_found",
266 Self::ChannelAdapterError(_) => "channel_adapter_error",
267 Self::ChannelSenderNotAllowed(_) => "channel_sender_not_allowed",
268 Self::Internal(_) => "internal_error",
269 Self::External(_) => "external_error",
270 Self::Timeout(_) => "timeout",
271 Self::Database(_) => "database_error",
272 Self::Serialization(_) => "serialization_error",
273 }
274 }
275}
276
277impl IntoResponse for CiabError {
278 fn into_response(self) -> Response {
279 let status =
280 StatusCode::from_u16(self.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
281 let body = json!({
282 "error": {
283 "code": self.error_code(),
284 "message": self.to_string(),
285 }
286 });
287 (status, axum::Json(body)).into_response()
288 }
289}