1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! Typed error surface — `ToolError` for tool/hook failures,
//! `Error` for SDK runtime / dispatch loop failures.
use thiserror::Error;
/// Result alias used by `run_stdio` / `run_with` and other
/// dispatch-loop entry points.
pub type Result<T> = std::result::Result<T, Error>;
/// SDK runtime / dispatch-loop errors. Distinct from
/// [`ToolError`] (which is the per-tool / per-hook failure
/// surface). Operators see this when the loop itself fails to
/// boot or read/write — `ToolError` flows back over JSON-RPC to
/// the daemon.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum Error {
/// I/O on stdin / stdout failed (closed, permission denied,
/// etc.). Usually fatal to the loop.
#[error("io error: {0}")]
Io(#[from] std::io::Error),
/// Generic boxed error escape hatch — used by handlers that
/// don't fit one of the above and don't have their own typed
/// shape.
#[error("internal: {0}")]
Internal(String),
}
/// Per-tool / per-hook failure. Returned by handler bodies and
/// translated into JSON-RPC error frames by the dispatch loop.
///
/// Operator-facing diagnostic — `#[non_exhaustive]` so a future
/// failure mode (e.g. `RateLimited`) lands as semver-minor without
/// breaking downstream pattern matches.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum ToolError {
/// The handler couldn't parse / validate its arguments.
/// Translates to JSON-RPC code `-32602` (Invalid params).
#[error("invalid arguments: {0}")]
InvalidArguments(String),
/// The handler isn't implemented yet (scaffold stage).
/// Translates to JSON-RPC code `-32601` (Method not found).
#[error("not implemented")]
NotImplemented,
/// Catch-all for handler-internal failures (DB error, network,
/// unexpected state). Translates to JSON-RPC code `-32000`.
#[error("internal: {0}")]
Internal(String),
/// The caller doesn't have permission to invoke this tool with
/// these arguments. Translates to JSON-RPC code `-32000` with
/// `data.code = "unauthorized"`.
#[error("unauthorized: {0}")]
Unauthorized(String),
}
impl ToolError {
/// JSON-RPC error code for this variant.
pub fn code(&self) -> i32 {
match self {
ToolError::InvalidArguments(_) => -32602,
ToolError::NotImplemented => -32601,
ToolError::Internal(_) => -32000,
ToolError::Unauthorized(_) => -32000,
}
}
/// Symbolic code surfaced under JSON-RPC error `data.code` so
/// callers can pattern-match without parsing the message.
pub fn symbolic(&self) -> &'static str {
match self {
ToolError::InvalidArguments(_) => "invalid_arguments",
ToolError::NotImplemented => "not_implemented",
ToolError::Internal(_) => "internal",
ToolError::Unauthorized(_) => "unauthorized",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_arguments_maps_to_minus_32602() {
let e = ToolError::InvalidArguments("missing field".into());
assert_eq!(e.code(), -32602);
assert_eq!(e.symbolic(), "invalid_arguments");
}
#[test]
fn not_implemented_maps_to_minus_32601() {
assert_eq!(ToolError::NotImplemented.code(), -32601);
assert_eq!(ToolError::NotImplemented.symbolic(), "not_implemented");
}
#[test]
fn internal_maps_to_minus_32000() {
assert_eq!(ToolError::Internal("x".into()).code(), -32000);
assert_eq!(ToolError::Internal("x".into()).symbolic(), "internal");
}
#[test]
fn unauthorized_maps_to_minus_32000_with_symbolic() {
let e = ToolError::Unauthorized("no binding".into());
assert_eq!(e.code(), -32000);
assert_eq!(e.symbolic(), "unauthorized");
}
#[test]
fn error_io_round_trip() {
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe closed");
let e: Error = io_err.into();
assert!(format!("{e}").contains("pipe closed"));
}
}