Skip to main content

statespace_tool_runtime/
error.rs

1//! Error types for the tool runtime.
2
3use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum Error {
10    #[error("invalid command: {0}")]
11    InvalidCommand(String),
12
13    #[error("command not found in frontmatter: {command}")]
14    CommandNotFound { command: String },
15
16    #[error("no frontmatter found in file")]
17    NoFrontmatter,
18
19    #[error("frontmatter parse error: {0}")]
20    FrontmatterParse(String),
21
22    #[error("tool execution timeout")]
23    Timeout,
24
25    #[error("output too large: {size} bytes (limit: {limit})")]
26    OutputTooLarge { size: usize, limit: usize },
27
28    #[error("path traversal attempt: tried to access {attempted} outside boundary {boundary}")]
29    PathTraversal { attempted: String, boundary: String },
30
31    #[error("file not found: {0}")]
32    NotFound(String),
33
34    #[error("security violation: {0}")]
35    Security(String),
36
37    #[error("network error: {0}")]
38    Network(String),
39
40    #[error("io error: {0}")]
41    Io(#[from] std::io::Error),
42
43    #[error("internal error: {0}")]
44    Internal(String),
45}
46
47impl Error {
48    #[must_use]
49    pub fn user_message(&self) -> String {
50        match self {
51            Self::InvalidCommand(msg) => format!("Invalid command: {msg}"),
52            Self::CommandNotFound { command } => {
53                format!("Command '{command}' not allowed by frontmatter")
54            }
55            Self::NoFrontmatter => {
56                "No frontmatter found. Tools must be declared in YAML/TOML frontmatter.".to_string()
57            }
58            Self::FrontmatterParse(msg) => format!("Frontmatter parse error: {msg}"),
59            Self::Timeout => "Tool execution timeout".to_string(),
60            Self::OutputTooLarge { size, limit } => {
61                format!("Output too large: {size} bytes (limit: {limit} bytes)")
62            }
63            Self::PathTraversal { attempted, .. } => {
64                format!("Access denied: cannot access '{attempted}'")
65            }
66            Self::NotFound(path) => format!("File not found: {path}"),
67            Self::Security(msg) => format!("Security violation: {msg}"),
68            Self::Network(msg) => format!("Network error: {msg}"),
69            Self::Io(e) => format!("IO error: {e}"),
70            Self::Internal(_) => "Internal server error".to_string(),
71        }
72    }
73
74    /// Returns the appropriate HTTP status code for this error as a `u16`.
75    #[must_use]
76    pub const fn http_status_code(&self) -> u16 {
77        match self {
78            Self::InvalidCommand(_)
79            | Self::CommandNotFound { .. }
80            | Self::NoFrontmatter
81            | Self::FrontmatterParse(_) => 400,
82
83            Self::PathTraversal { .. } | Self::Security(_) => 403,
84
85            Self::NotFound(_) => 404,
86            Self::Timeout => 504,
87            Self::OutputTooLarge { .. } => 413,
88
89            Self::Io(_) | Self::Internal(_) => 500,
90            Self::Network(_) => 502,
91        }
92    }
93}