Skip to main content

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>;