agentos_client/error.rs
1//! Error taxonomy for the Agent OS client SDK.
2//!
3//! Mirrors `spec.md` §4 / ADR-001 §4. Preserves the TypeScript SDK distinction so callers can still
4//! discriminate path-guard violations from kernel errno failures. Public methods return
5//! [`anyhow::Result`]; the typed [`ClientError`] is carried as the `source` so callers can downcast.
6//!
7//! Hard rule (parity): JSON-RPC errors are NOT Rust `Err`. `prompt`, `cancel_session`,
8//! `set_session_model`, `set_session_thought_level`, `respond_permission`, `raw_session_send`,
9//! `raw_send`, and `set_session_mode` return a [`crate::json_rpc::JsonRpcResponse`] whose `error`
10//! field may be populated (including `acp_timeout` and codex `-32601` fallbacks). Do not convert
11//! those into `Err`.
12
13use secure_exec_client::{ProtocolCodecError, TransportError};
14
15/// Typed error taxonomy for the client SDK.
16#[derive(thiserror::Error, Debug)]
17pub enum ClientError {
18 /// A filesystem path was not absolute (did not start with `/`).
19 ///
20 /// The message text matches the TypeScript `AgentOs` exactly (capital "P"). These strings are
21 /// observable data (they surface in `BatchWriteResult.error` / `BatchReadResult.error`), not
22 /// logs, so the casing follows TS rather than the lowercase log convention.
23 #[error("Path must be absolute: {0}")]
24 PathNotAbsolute(String),
25
26 /// A filesystem path was not in posix-normalized form.
27 ///
28 /// The message text matches the TypeScript `AgentOs` exactly (capital "P").
29 #[error("Path must be normalized: {0}")]
30 PathNotNormalized(String),
31
32 /// A write was attempted against a read-only path (for example `/proc`).
33 ///
34 /// The message text matches the TypeScript `AgentOs` exactly (capital "P").
35 #[error("Path is read-only: {0}")]
36 PathReadOnly(String),
37
38 /// An SDK-spawned process with the given pid was not found.
39 ///
40 /// The message text matches the TypeScript `AgentOs` exactly (capital "P"). These strings are
41 /// observable data (surfaced to callers), not logs, so the casing follows TS rather than the
42 /// lowercase log convention.
43 #[error("Process not found: {0}")]
44 ProcessNotFound(u32),
45
46 /// A shell with the given synthetic `shell-N` id was not found.
47 #[error("shell not found: {0}")]
48 ShellNotFound(String),
49
50 /// An ACP session with the given id was not found.
51 #[error("session not found: {0}")]
52 SessionNotFound(String),
53
54 /// A kernel/sidecar operation failed. The errno `code` string (`ENOENT`, `EEXIST`, `ENOTDIR`,
55 /// `EACCES`, `EISDIR`, `ENOTEMPTY`, ...) is preserved verbatim for parity with the TypeScript
56 /// `KernelError`.
57 #[error("kernel error [{code}]: {message}")]
58 Kernel { code: String, message: String },
59
60 /// A cron schedule string could not be parsed/validated.
61 #[error("invalid schedule: {0}")]
62 InvalidSchedule(String),
63
64 /// A one-shot (ISO-8601) cron schedule resolved to a time in the past.
65 #[error("schedule is in the past: {0}")]
66 PastSchedule(String),
67
68 /// A framing/codec failure on the sidecar transport.
69 #[error("transport error: {0}")]
70 Transport(#[from] ProtocolCodecError),
71
72 /// A generic sidecar rejection or I/O failure with context.
73 #[error("sidecar error: {0}")]
74 Sidecar(String),
75}
76
77impl From<TransportError> for ClientError {
78 fn from(error: TransportError) -> Self {
79 match error {
80 TransportError::Protocol(error) => ClientError::Transport(error),
81 TransportError::Sidecar(message) => ClientError::Sidecar(message),
82 }
83 }
84}
85
86impl ClientError {
87 /// Render this error the way the TypeScript `AgentOs` surfaces `err.message` into batch results
88 /// (`BatchWriteResult.error` / `BatchReadResult.error`).
89 ///
90 /// The general [`Display`](std::fmt::Display) impl carries a human/log-oriented prefix
91 /// (`kernel error [<code>]: ...`), but the batch surface is observable data that must match TS
92 /// byte-for-byte. For kernel failures TS reports `KernelError.message`, which is
93 /// `<code>: <message>` (and avoids doubling the code when the message already starts with it).
94 /// Path-guard variants already carry the exact TS strings via their `Display` impl.
95 pub fn batch_message(&self) -> String {
96 match self {
97 ClientError::Kernel { code, message } => {
98 if message.starts_with(&format!("{code}:")) {
99 message.clone()
100 } else {
101 format!("{code}: {message}")
102 }
103 }
104 ClientError::PathNotAbsolute(_)
105 | ClientError::PathNotNormalized(_)
106 | ClientError::PathReadOnly(_)
107 | ClientError::ProcessNotFound(_)
108 | ClientError::ShellNotFound(_)
109 | ClientError::SessionNotFound(_)
110 | ClientError::InvalidSchedule(_)
111 | ClientError::PastSchedule(_)
112 | ClientError::Transport(_)
113 | ClientError::Sidecar(_) => self.to_string(),
114 }
115 }
116}
117
118/// Convenience alias for results carrying a typed [`ClientError`].
119pub type ClientResult<T> = std::result::Result<T, ClientError>;