mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 08:03:46 +00:00
feat: add openapi generator and error schemas
This commit is contained in:
parent
55c45bfc12
commit
1ac5a0a23a
5 changed files with 348 additions and 0 deletions
11
engine/packages/error/Cargo.toml
Normal file
11
engine/packages/error/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "error"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
schemars = "0.8"
|
||||
utoipa = "4.2"
|
||||
302
engine/packages/error/src/lib.rs
Normal file
302
engine/packages/error/src/lib.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
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-daemon:error:invalid_request",
|
||||
Self::UnsupportedAgent => "urn:sandbox-daemon:error:unsupported_agent",
|
||||
Self::AgentNotInstalled => "urn:sandbox-daemon:error:agent_not_installed",
|
||||
Self::InstallFailed => "urn:sandbox-daemon:error:install_failed",
|
||||
Self::AgentProcessExited => "urn:sandbox-daemon:error:agent_process_exited",
|
||||
Self::TokenInvalid => "urn:sandbox-daemon:error:token_invalid",
|
||||
Self::PermissionDenied => "urn:sandbox-daemon:error:permission_denied",
|
||||
Self::SessionNotFound => "urn:sandbox-daemon:error:session_not_found",
|
||||
Self::SessionAlreadyExists => "urn:sandbox-daemon:error:session_already_exists",
|
||||
Self::ModeNotSupported => "urn:sandbox-daemon:error:mode_not_supported",
|
||||
Self::StreamError => "urn:sandbox-daemon:error:stream_error",
|
||||
Self::Timeout => "urn:sandbox-daemon: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<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub instance: Option<String>,
|
||||
#[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
|
||||
pub extensions: Map<String, Value>,
|
||||
}
|
||||
|
||||
impl ProblemDetails {
|
||||
pub fn new(error_type: ErrorType, detail: Option<String>) -> 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<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<Value>,
|
||||
}
|
||||
|
||||
#[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<String> },
|
||||
#[error("agent process exited: {agent}")]
|
||||
AgentProcessExited {
|
||||
agent: String,
|
||||
exit_code: Option<i32>,
|
||||
stderr: Option<String>,
|
||||
},
|
||||
#[error("token invalid")]
|
||||
TokenInvalid { message: Option<String> },
|
||||
#[error("permission denied")]
|
||||
PermissionDenied { message: Option<String> },
|
||||
#[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<String> },
|
||||
}
|
||||
|
||||
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<SandboxError> 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<SandboxError> 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()
|
||||
}
|
||||
}
|
||||
12
engine/packages/openapi-gen/Cargo.toml
Normal file
12
engine/packages/openapi-gen/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "openapi-gen"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[build-dependencies]
|
||||
sandbox-daemon = { path = "../sandbox-daemon" }
|
||||
serde_json = "1.0"
|
||||
utoipa = "4.2"
|
||||
20
engine/packages/openapi-gen/build.rs
Normal file
20
engine/packages/openapi-gen/build.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use sandbox_daemon::router::ApiDoc;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../sandbox-daemon/src/router.rs");
|
||||
println!("cargo:rerun-if-changed=../sandbox-daemon/src/lib.rs");
|
||||
|
||||
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||
let out_path = Path::new(&out_dir).join("openapi.json");
|
||||
|
||||
let openapi = ApiDoc::openapi();
|
||||
let json = serde_json::to_string_pretty(&openapi)
|
||||
.expect("Failed to serialize OpenAPI spec");
|
||||
|
||||
fs::write(&out_path, json).expect("Failed to write OpenAPI spec");
|
||||
println!("cargo:warning=Generated OpenAPI spec at {}", out_path.display());
|
||||
}
|
||||
3
engine/packages/openapi-gen/src/lib.rs
Normal file
3
engine/packages/openapi-gen/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
//! Generated OpenAPI schema output.
|
||||
|
||||
pub const OPENAPI_JSON: &str = include_str!(concat!(env!("OUT_DIR"), "/openapi.json"));
|
||||
Loading…
Add table
Add a link
Reference in a new issue