use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ErrorCode {
pub code: &'static str,
pub docs_url: &'static str,
}
impl ErrorCode {
pub const fn new(code: &'static str, docs_url: &'static str) -> Self {
Self { code, docs_url }
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code)
}
}
macro_rules! ol_codes {
( $( $name:ident = $code:literal ; )* ) => {
$(
#[doc = concat!("`", $code, "` — see https://docs.openlatch.ai/errors/", $code)]
pub const $name: ErrorCode = ErrorCode::new(
$code,
concat!("https://docs.openlatch.ai/errors/", $code),
);
)*
pub const ALL_CODES: &[ErrorCode] = &[ $( $name ),* ];
};
}
ol_codes! {
OL_4200_TOKEN_EXPIRED = "OL-4200";
OL_4201_KEYRING_UNAVAILABLE = "OL-4201";
OL_4202_BROWSER_LAUNCH_FAILED = "OL-4202";
OL_4203_PKCE_STATE_MISMATCH = "OL-4203";
OL_4204_TOKEN_FILE_UNREADABLE = "OL-4204";
OL_4210_SCHEMA_MISMATCH = "OL-4210";
OL_4211_UNKNOWN_THREAT_CATEGORY = "OL-4211";
OL_4212_INVALID_ENDPOINT_URL = "OL-4212";
OL_4213_INVALID_AGENTS_SUPPORTED = "OL-4213";
OL_4214_INVALID_HOOKS_SUPPORTED = "OL-4214";
OL_4215_UNKNOWN_AGENT_PLATFORM = "OL-4215";
OL_4216_CAPABILITY_MISSING_FIELD = "OL-4216";
OL_4220_HMAC_FAILED = "OL-4220";
OL_4221_MALFORMED_BODY = "OL-4221";
OL_4222_BINDING_NOT_CONFIGURED = "OL-4222";
OL_4223_VERDICT_TOO_LARGE = "OL-4223";
OL_4224_TOOL_UNREACHABLE = "OL-4224";
OL_4225_TOOL_5XX = "OL-4225";
OL_4226_TIMESTAMP_SKEW = "OL-4226";
OL_4227_REPLAY_REJECTED = "OL-4227";
OL_4228_DEADLINE_EXCEEDED = "OL-4228";
OL_4230_BACKEND_4XX = "OL-4230";
OL_4231_BACKEND_5XX = "OL-4231";
OL_4232_BACKEND_UNAUTHORIZED = "OL-4232";
OL_4233_BACKEND_FORBIDDEN = "OL-4233";
OL_4234_BACKEND_NOT_FOUND = "OL-4234";
OL_4235_BACKEND_CONFLICT = "OL-4235";
OL_4236_BACKEND_GONE = "OL-4236";
OL_4237_BACKEND_INTERNAL = "OL-4237";
OL_4238_BACKEND_BAD_GATEWAY = "OL-4238";
OL_4239_BACKEND_UNAVAILABLE = "OL-4239";
OL_4240_ENDPOINT_NOT_HTTPS = "OL-4240";
OL_4241_PRIVATE_IP = "OL-4241";
OL_4242_TLS_TOO_LOW = "OL-4242";
OL_4243_REDIRECT_NOT_FOLLOWED = "OL-4243";
OL_4244_SYNTHETIC_PROBE_FAILED = "OL-4244";
OL_4245_LATENCY_PROBE_FAILED = "OL-4245";
OL_4246_CLOUD_METADATA_IP = "OL-4246";
OL_4247_IPV4_MAPPED_V6 = "OL-4247";
OL_4250_UPDATE_FETCH_FAILED = "OL-4250";
OL_4251_INTEGRITY_MISMATCH = "OL-4251";
OL_4252_NPM_REGISTRY_UNREACHABLE = "OL-4252";
OL_4253_UPDATE_APPLY_FAILED = "OL-4253";
OL_4254_UPDATE_SIGNATURE_FAILED = "OL-4254";
OL_4255_UPDATE_SANITY_FAILED = "OL-4255";
OL_4256_UPDATE_SWAP_FS_ERROR = "OL-4256";
OL_4257_UPDATE_ROLLED_BACK = "OL-4257";
OL_4258_UPDATE_REFUSED_CARGO_INSTALL = "OL-4258";
OL_4259_UPDATE_BLOCKED_MIN_SUPPORTED = "OL-4259";
OL_4260_SENTRY_INIT_FAILED = "OL-4260";
OL_4261_POSTHOG_INIT_FAILED = "OL-4261";
OL_4262_CONSENT_FILE_CORRUPT = "OL-4262";
OL_4263_CONSENT_WRITE_FAILED = "OL-4263";
OL_4264_TELEMETRY_BATCH_FAILED = "OL-4264";
OL_4270_CONFIG_UNREADABLE = "OL-4270";
OL_4271_PROFILE_NOT_FOUND = "OL-4271";
OL_4272_XDG_DIR_UNWRITABLE = "OL-4272";
OL_4273_MANIFEST_UNREADABLE = "OL-4273";
OL_4274_MANIFEST_WRITE_FAILED = "OL-4274";
OL_4275_EDITOR_LAUNCH_FAILED = "OL-4275";
OL_4280_PLATFORM_DUPLICATE_TOOL_SLUG = "OL-4280";
OL_4281_PLATFORM_DUPLICATE_PROVIDER_SLUG = "OL-4281";
OL_4282_PLATFORM_DUPLICATE_EDITOR_SLUG = "OL-4282";
OL_4283_PLATFORM_DUPLICATE_BINDING = "OL-4283";
OL_4284_PREFLIGHT_VALIDATION_FAILED = "OL-4284";
OL_4290_RATE_LIMIT = "OL-4290";
OL_4291_TOOL_SUBMISSIONS_RATE_LIMIT = "OL-4291";
OL_4292_PROVIDER_SUBMISSIONS_RATE_LIMIT = "OL-4292";
OL_4293_BINDING_SUBMISSIONS_RATE_LIMIT = "OL-4293";
OL_4294_PLAN_QUOTA_EXCEEDED = "OL-4294";
OL_4300_PROCESS_SPEC_INVALID = "OL-4300";
OL_4301_PROCESS_SPAWN_FAILED = "OL-4301";
OL_4302_STARTUP_PROBE_TIMEOUT = "OL-4302";
OL_4303_RESTART_RATE_LIMIT = "OL-4303";
OL_4304_PORT_ALREADY_BOUND = "OL-4304";
OL_4305_ORPHAN_RECONCILE_FAILED = "OL-4305";
OL_4320_PROVIDER_SCHEMA_INVALID = "OL-4320";
OL_4321_TOOL_SCHEMA_INVALID = "OL-4321";
OL_4322_AMBIGUOUS_TOOL_REF = "OL-4322";
OL_4323_UNRESOLVED_TOOL_REF = "OL-4323";
OL_4324_DUPLICATE_TOOL_REGISTRY = "OL-4324";
OL_4325_TOOL_PATHS_ZERO_MATCH = "OL-4325";
OL_4326_OVERRIDE_COMMAND_CONFLICT = "OL-4326";
OL_4327_V1_MANIFEST_REJECTED = "OL-4327";
OL_4328_TOOL_MANIFEST_MULTI_EDITOR = "OL-4328";
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitCode {
Success = 0,
UserError = 1,
ConfigOrAuth = 2,
Network = 3,
Runtime = 4,
PartialSuccess = 5,
Sigint = 130,
}
impl From<ExitCode> for i32 {
fn from(c: ExitCode) -> i32 {
c as i32
}
}
#[derive(Debug, Error)]
#[error("[{code}] {message}", code = code.code, message = message)]
pub struct OlError {
pub code: ErrorCode,
pub message: String,
pub suggestion: Option<String>,
pub context: Option<serde_json::Value>,
#[source]
pub source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}
impl OlError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
suggestion: None,
context: None,
source: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn with_context(mut self, context: serde_json::Value) -> Self {
self.context = Some(context);
self
}
pub fn with_source<E>(mut self, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
self.source = Some(Box::new(source));
self
}
pub fn is(&self, code: ErrorCode) -> bool {
self.code == code
}
pub fn exit_code(&self) -> ExitCode {
match self.code.code.as_bytes() {
b"OL-4200" | b"OL-4201" | b"OL-4202" | b"OL-4203" | b"OL-4204" | b"OL-4232"
| b"OL-4233" | b"OL-4270" | b"OL-4271" | b"OL-4272" => ExitCode::ConfigOrAuth,
b"OL-4230" | b"OL-4231" | b"OL-4234" | b"OL-4235" | b"OL-4236" | b"OL-4237"
| b"OL-4238" | b"OL-4239" | b"OL-4252" | b"OL-4290" | b"OL-4291" | b"OL-4292"
| b"OL-4293" | b"OL-4294" => ExitCode::Network,
b"OL-4280" | b"OL-4281" | b"OL-4282" | b"OL-4283" | b"OL-4284" => ExitCode::UserError,
b"OL-4220" | b"OL-4221" | b"OL-4222" | b"OL-4223" | b"OL-4224" | b"OL-4225"
| b"OL-4226" | b"OL-4227" | b"OL-4228" | b"OL-4240" | b"OL-4241" | b"OL-4242"
| b"OL-4243" | b"OL-4244" | b"OL-4245" | b"OL-4246" | b"OL-4247" | b"OL-4250"
| b"OL-4251" | b"OL-4253" | b"OL-4254" | b"OL-4255" | b"OL-4256" | b"OL-4257" => {
ExitCode::Runtime
}
b"OL-4258" | b"OL-4259" => ExitCode::PartialSuccess,
b"OL-4301" | b"OL-4302" | b"OL-4303" | b"OL-4304" | b"OL-4305" => ExitCode::Runtime,
_ => ExitCode::UserError,
}
}
pub fn auth(message: impl Into<String>) -> Self {
Self::new(OL_4200_TOKEN_EXPIRED, message)
}
pub fn manifest(message: impl Into<String>) -> Self {
Self::new(OL_4210_SCHEMA_MISMATCH, message)
}
pub fn backend(message: impl Into<String>) -> Self {
Self::new(OL_4230_BACKEND_4XX, message)
}
pub fn config(message: impl Into<String>) -> Self {
Self::new(OL_4270_CONFIG_UNREADABLE, message)
}
}
impl From<std::io::Error> for OlError {
fn from(e: std::io::Error) -> Self {
OlError::new(OL_4270_CONFIG_UNREADABLE, e.to_string()).with_source(e)
}
}
pub type Result<T, E = OlError> = std::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_codes_are_unique_and_well_formed() {
let mut seen = std::collections::BTreeSet::new();
for c in ALL_CODES {
assert!(
c.code.starts_with("OL-42") || c.code.starts_with("OL-43"),
"out-of-range code: {}",
c.code
);
assert_eq!(c.code.len(), 7, "malformed code: {}", c.code);
assert!(
c.docs_url.starts_with("https://docs.openlatch.ai/errors/"),
"bad docs_url: {}",
c.docs_url
);
assert!(seen.insert(c.code), "duplicate code: {}", c.code);
}
}
#[test]
fn exit_code_routing() {
assert_eq!(
OlError::new(OL_4200_TOKEN_EXPIRED, "x").exit_code(),
ExitCode::ConfigOrAuth
);
assert_eq!(
OlError::new(OL_4220_HMAC_FAILED, "x").exit_code(),
ExitCode::Runtime
);
assert_eq!(
OlError::new(OL_4231_BACKEND_5XX, "x").exit_code(),
ExitCode::Network
);
assert_eq!(
OlError::new(OL_4210_SCHEMA_MISMATCH, "x").exit_code(),
ExitCode::UserError
);
}
#[test]
fn display_includes_code_and_message() {
let e = OlError::new(OL_4220_HMAC_FAILED, "signature mismatch");
let s = format!("{e}");
assert!(s.contains("OL-4220"));
assert!(s.contains("signature mismatch"));
}
}