Skip to main content

agent_bridle_core/
error.rs

1//! Errors surfaced by the leash and by tools.
2
3use std::fmt;
4
5/// The result type used throughout `agent-bridle-core`.
6pub type ToolResult<T> = Result<T, ToolError>;
7
8/// Why a dispatch or a tool invocation failed.
9///
10/// The first three variants (`Denied`, `Budget`, `Generation`) are *leash*
11/// outcomes: the [`crate::Gate`] refused to mint a [`crate::ToolContext`], so
12/// the tool never ran. `NotFound` is a registry miss. `Exec` and `Other` are
13/// failures from inside a tool that *did* pass the leash.
14#[derive(Debug)]
15pub enum ToolError {
16    /// The requested authority is not within (or below) the granted caveats —
17    /// the tool's `required ⊑ granted` check failed, or a per-operation leash
18    /// check (`check_exec`, `check_path_*`, `check_net`) denied the operation.
19    Denied {
20        /// Human-readable reason (safe to surface to the agent).
21        reason: String,
22    },
23    /// No tool registered under the requested name.
24    NotFound {
25        /// The name that was looked up.
26        name: String,
27    },
28    /// The grant's `max_calls` budget is exhausted.
29    Budget,
30    /// The gate's generation is not in the grant's `valid_for_generation` set.
31    Generation,
32    /// A tool that passed the leash failed during execution (I/O, spawn, …).
33    Exec(std::io::Error),
34    /// Any other failure from inside a tool.
35    Other(anyhow::Error),
36}
37
38impl ToolError {
39    /// Convenience constructor for a denial with a formatted reason.
40    pub fn denied(reason: impl Into<String>) -> Self {
41        Self::Denied {
42            reason: reason.into(),
43        }
44    }
45
46    /// Convenience constructor for a registry miss.
47    pub fn not_found(name: impl Into<String>) -> Self {
48        Self::NotFound { name: name.into() }
49    }
50}
51
52impl fmt::Display for ToolError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Denied { reason } => write!(f, "denied: {reason}"),
56            Self::NotFound { name } => write!(f, "no such tool: {name}"),
57            Self::Budget => write!(f, "denied: call budget (max_calls) exhausted"),
58            Self::Generation => {
59                write!(f, "denied: grant is not valid for the gate's generation")
60            }
61            Self::Exec(e) => write!(f, "tool execution failed: {e}"),
62            Self::Other(e) => write!(f, "{e}"),
63        }
64    }
65}
66
67impl std::error::Error for ToolError {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Self::Exec(e) => Some(e),
71            Self::Other(e) => e.source(),
72            _ => None,
73        }
74    }
75}
76
77impl From<std::io::Error> for ToolError {
78    fn from(e: std::io::Error) -> Self {
79        Self::Exec(e)
80    }
81}
82
83impl From<anyhow::Error> for ToolError {
84    fn from(e: anyhow::Error) -> Self {
85        Self::Other(e)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn display_is_stable_and_safe() {
95        assert_eq!(
96            ToolError::denied("nope").to_string(),
97            "denied: nope".to_string()
98        );
99        assert_eq!(
100            ToolError::Budget.to_string(),
101            "denied: call budget (max_calls) exhausted"
102        );
103        assert!(ToolError::not_found("shell").to_string().contains("shell"));
104    }
105}