use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum JacsError {
ConfigError(String),
CryptoError(String),
SchemaError(String),
AgentError(String),
DocumentError(String),
NetworkError(String),
TrustError(String),
IoError(std::io::Error),
ValidationError(String),
ConfigNotFound { path: String },
ConfigInvalid { field: String, reason: String },
KeyNotFound { path: String },
KeyDecryptionFailed { reason: String },
KeyGenerationFailed { algorithm: String, reason: String },
SigningFailed { reason: String },
SignatureInvalid { expected: String, got: String },
SignatureVerificationFailed { reason: String },
HashMismatch { expected: String, got: String },
DocumentMalformed { field: String, reason: String },
SignerUnknown { agent_id: String },
DnsLookupFailed { domain: String, reason: String },
DnsRecordMissing { domain: String },
DnsRecordInvalid { domain: String, reason: String },
DocumentTooLarge { size: usize, max_size: usize },
DocumentNotFound { document_id: String },
FileNotFound { path: String },
FileReadFailed { path: String, reason: String },
FileWriteFailed { path: String, reason: String },
DirectoryCreateFailed { path: String, reason: String },
MimeTypeUnknown { path: String },
AgentNotTrusted { agent_id: String },
RegistrationFailed { reason: String },
SearchError(String),
StorageError(String),
DatabaseError { operation: String, reason: String },
VerificationClaimFailed { claim: String, reason: String },
#[cfg(feature = "attestation")]
AttestationFailed { message: String },
#[cfg(feature = "attestation")]
VerificationFailed { message: String },
AgentNotLoaded,
Internal { message: String },
}
impl fmt::Display for JacsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JacsError::ConfigError(msg) => write!(f, "Configuration error: {}", msg),
JacsError::CryptoError(msg) => write!(f, "Cryptographic error: {}", msg),
JacsError::SchemaError(msg) => write!(f, "Schema error: {}", msg),
JacsError::AgentError(msg) => write!(f, "Agent error: {}", msg),
JacsError::DocumentError(msg) => write!(f, "Document error: {}", msg),
JacsError::NetworkError(msg) => write!(f, "Network error: {}", msg),
JacsError::TrustError(msg) => write!(f, "Trust store error: {}", msg),
JacsError::IoError(err) => write!(f, "IO error: {}", err),
JacsError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
JacsError::ConfigNotFound { path } => {
write!(
f,
"Configuration not found at '{}'. Run jacs.create(name=\"...\") to create a new agent.",
path
)
}
JacsError::ConfigInvalid { field, reason } => {
write!(f, "Invalid configuration field '{}': {}", field, reason)
}
JacsError::KeyNotFound { path } => {
write!(
f,
"Key file not found at '{}'. Ensure keys were generated during agent creation.",
path
)
}
JacsError::KeyDecryptionFailed { reason } => {
write!(f, "Failed to decrypt private key: {}", reason)
}
JacsError::KeyGenerationFailed { algorithm, reason } => {
write!(f, "Failed to generate {} keypair: {}", algorithm, reason)
}
JacsError::SigningFailed { reason } => {
write!(f, "Document signing failed: {}", reason)
}
JacsError::SignatureInvalid { expected, got } => {
write!(
f,
"Invalid signature: expected '{}...', got '{}...'",
&expected[..expected.len().min(16)],
&got[..got.len().min(16)]
)
}
JacsError::SignatureVerificationFailed { reason } => {
write!(f, "Signature verification failed: {}", reason)
}
JacsError::HashMismatch { expected, got } => {
write!(
f,
"Hash mismatch: document may have been tampered with. Expected '{}...', got '{}...'",
&expected[..expected.len().min(16)],
&got[..got.len().min(16)]
)
}
JacsError::DocumentMalformed { field, reason } => {
write!(f, "Malformed document: field '{}' - {}", field, reason)
}
JacsError::SignerUnknown { agent_id } => {
write!(
f,
"Unknown signer '{}'. Use jacs.trust_agent() to add them to your trust store.",
agent_id
)
}
JacsError::DnsLookupFailed { domain, reason } => {
write!(f, "DNS lookup failed for '{}': {}", domain, reason)
}
JacsError::DnsRecordMissing { domain } => {
write!(
f,
"DNS TXT record not found for '{}'. Add the record shown by `jacs dns-record`.",
domain
)
}
JacsError::DnsRecordInvalid { domain, reason } => {
write!(f, "Invalid DNS record for '{}': {}", domain, reason)
}
JacsError::DocumentTooLarge { size, max_size } => {
write!(
f,
"Document too large: {} bytes exceeds maximum allowed size of {} bytes. \
To increase the limit, set JACS_MAX_DOCUMENT_SIZE environment variable.",
size, max_size
)
}
JacsError::DocumentNotFound { document_id } => {
write!(
f,
"Document '{}' not found in storage. Verify the document ID is correct and the document has been stored.",
document_id
)
}
JacsError::FileNotFound { path } => {
write!(
f,
"File not found: '{}'. Ensure the file path is correct or create the file first.",
path
)
}
JacsError::FileReadFailed { path, reason } => {
write!(
f,
"Failed to read file '{}': {}. Check that the file exists and has read permissions.",
path, reason
)
}
JacsError::FileWriteFailed { path, reason } => {
write!(
f,
"Failed to write file '{}': {}. Check that the directory exists and has write permissions.",
path, reason
)
}
JacsError::DirectoryCreateFailed { path, reason } => {
write!(
f,
"Failed to create directory '{}': {}. Check that the parent directory exists and has write permissions.",
path, reason
)
}
JacsError::MimeTypeUnknown { path } => {
write!(
f,
"Could not determine MIME type for '{}'. The file will be treated as application/octet-stream.",
path
)
}
JacsError::AgentNotTrusted { agent_id } => {
write!(
f,
"Agent '{}' is not trusted. Use jacs.trust_agent() to add them.",
agent_id
)
}
JacsError::RegistrationFailed { reason } => {
write!(f, "Registration failed: {}", reason)
}
JacsError::SearchError(msg) => write!(f, "Search error: {}", msg),
JacsError::StorageError(msg) => write!(f, "Storage error: {}", msg),
JacsError::DatabaseError { operation, reason } => {
write!(f, "Database error during '{}': {}", operation, reason)
}
JacsError::VerificationClaimFailed { claim, reason } => {
write!(
f,
"Verification claim '{}' failed: {}\n\n\
Fix: ",
claim, reason
)?;
match claim.as_str() {
"verified"
if reason.contains("jacsAgentDomain") || reason.contains("domain") =>
{
write!(
f,
"Add \"jacsAgentDomain\": \"your-domain.com\" to your agent,\n \
or use \"jacsVerificationClaim\": \"unverified\" if DNS verification is not needed."
)?;
}
_ if reason.contains("downgrade") || reason.contains("Cannot downgrade") => {
write!(
f,
"Verification claims cannot be downgraded for security. Create a new agent if you need a lower claim level."
)?;
}
_ => {
write!(
f,
"Ensure all security requirements for '{}' are met.",
claim
)?;
}
}
write!(
f,
"\n\nSee: https://jacs.ai/docs/security#verification-claims"
)
}
#[cfg(feature = "attestation")]
JacsError::AttestationFailed { message } => {
write!(f, "Attestation failed: {}", message)
}
#[cfg(feature = "attestation")]
JacsError::VerificationFailed { message } => {
write!(f, "Attestation verification failed: {}", message)
}
JacsError::AgentNotLoaded => {
write!(
f,
"No agent loaded. Call jacs.quickstart(name, domain, ...) to create or load an agent automatically, or jacs.create() / jacs.load() for explicit control."
)
}
JacsError::Internal { message } => {
write!(f, "{}", message)
}
}
}
}
impl Error for JacsError {}
impl JacsError {
pub fn user_message(&self) -> String {
let full = self.to_string();
if let JacsError::Internal { message } = self {
let parts: Vec<&str> = message.split(": ").collect();
if parts.len() > 1 {
if let Some(last) = parts.last() {
if !last.is_empty() {
return last.to_string();
}
}
}
}
full
}
}
impl From<Box<dyn Error>> for JacsError {
fn from(err: Box<dyn Error>) -> Self {
let msg = err.to_string();
let lower = msg.to_lowercase();
tracing::warn!(
error_msg = %msg,
"Box<dyn Error> -> JacsError bridge hit; prefer constructing JacsError directly"
);
if lower.contains("password")
|| lower.contains("encrypt")
|| lower.contains("decrypt")
|| lower.contains("pbkdf2")
|| lower.contains("aes")
{
return JacsError::CryptoError(msg);
}
if lower.contains("config")
|| lower.contains("environment variable")
|| lower.contains("jacs_")
|| lower.contains("not found at")
{
return JacsError::ConfigError(msg);
}
if lower.contains("key")
&& (lower.contains("generate") || lower.contains("load") || lower.contains("not found"))
{
return JacsError::CryptoError(msg);
}
JacsError::Internal { message: msg }
}
}
impl From<std::io::Error> for JacsError {
fn from(err: std::io::Error) -> Self {
JacsError::IoError(err)
}
}
impl From<serde_json::Error> for JacsError {
fn from(err: serde_json::Error) -> Self {
JacsError::DocumentMalformed {
field: "json".to_string(),
reason: err.to_string(),
}
}
}
impl From<String> for JacsError {
fn from(err: String) -> Self {
JacsError::Internal { message: err }
}
}
impl From<&str> for JacsError {
fn from(err: &str) -> Self {
JacsError::Internal {
message: err.to_string(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<object_store::Error> for JacsError {
fn from(err: object_store::Error) -> Self {
JacsError::StorageError(err.to_string())
}
}
impl From<crate::storage::jenv::EnvError> for JacsError {
fn from(err: crate::storage::jenv::EnvError) -> Self {
JacsError::ConfigError(err.to_string())
}
}
#[cfg(feature = "a2a")]
impl From<crate::a2a::A2AError> for JacsError {
fn from(err: crate::a2a::A2AError) -> Self {
match err {
crate::a2a::A2AError::SerializationError(msg) => {
JacsError::DocumentError(format!("A2A serialization: {}", msg))
}
crate::a2a::A2AError::SigningError(msg) => {
JacsError::CryptoError(format!("A2A signing: {}", msg))
}
crate::a2a::A2AError::ValidationError(msg) => {
JacsError::ValidationError(format!("A2A validation: {}", msg))
}
crate::a2a::A2AError::KeyGenerationError(msg) => {
JacsError::CryptoError(format!("A2A key generation: {}", msg))
}
}
}
}
impl From<base64::DecodeError> for JacsError {
fn from(err: base64::DecodeError) -> Self {
JacsError::CryptoError(format!("base64 decode failed: {}", err))
}
}
#[cfg(feature = "sqlx-sqlite")]
impl From<sqlx::Error> for JacsError {
fn from(err: sqlx::Error) -> Self {
JacsError::DatabaseError {
operation: "sqlite".to_string(),
reason: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_error_display() {
let err = JacsError::ConfigError("missing required field 'name'".to_string());
let msg = err.to_string();
assert!(msg.contains("Configuration error"));
assert!(msg.contains("missing required field"));
}
#[test]
fn test_crypto_error_display() {
let err = JacsError::CryptoError("key generation failed".to_string());
let msg = err.to_string();
assert!(msg.contains("Cryptographic error"));
assert!(msg.contains("key generation"));
}
#[test]
fn test_schema_error_display() {
let err = JacsError::SchemaError("schema validation failed".to_string());
let msg = err.to_string();
assert!(msg.contains("Schema error"));
assert!(msg.contains("validation failed"));
}
#[test]
fn test_agent_error_display() {
let err = JacsError::AgentError("failed to load agent".to_string());
let msg = err.to_string();
assert!(msg.contains("Agent error"));
assert!(msg.contains("failed to load"));
}
#[test]
fn test_document_error_display() {
let err = JacsError::DocumentError("document signing failed".to_string());
let msg = err.to_string();
assert!(msg.contains("Document error"));
assert!(msg.contains("signing failed"));
}
#[test]
fn test_network_error_display() {
let err = JacsError::NetworkError("connection refused".to_string());
let msg = err.to_string();
assert!(msg.contains("Network error"));
assert!(msg.contains("connection refused"));
}
#[test]
fn test_trust_error_display() {
let err = JacsError::TrustError("trust store not found".to_string());
let msg = err.to_string();
assert!(msg.contains("Trust store error"));
assert!(msg.contains("not found"));
}
#[test]
fn test_io_error_display() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = JacsError::IoError(io_err);
let msg = err.to_string();
assert!(msg.contains("IO error"));
assert!(msg.contains("file not found"));
}
#[test]
fn test_validation_error_display() {
let err = JacsError::ValidationError("invalid input".to_string());
let msg = err.to_string();
assert!(msg.contains("Validation error"));
assert!(msg.contains("invalid input"));
}
#[test]
fn test_io_error_from_std_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let jacs_err: JacsError = io_err.into();
assert!(matches!(jacs_err, JacsError::IoError(_)));
let msg = jacs_err.to_string();
assert!(msg.contains("access denied"));
}
#[test]
fn test_error_display_config_not_found() {
let err = JacsError::ConfigNotFound {
path: "./jacs.config.json".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("jacs.config.json"));
assert!(msg.contains("create"));
}
#[test]
fn test_error_display_agent_not_loaded() {
let err = JacsError::AgentNotLoaded;
let msg = err.to_string();
assert!(msg.contains("quickstart"));
assert!(msg.contains("create"));
assert!(msg.contains("load"));
}
#[test]
fn test_error_from_string() {
let err: JacsError = "test error".into();
assert!(matches!(err, JacsError::Internal { .. }));
}
#[test]
fn test_error_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<JacsError>();
assert_sync::<JacsError>();
}
#[test]
fn test_error_implements_std_error() {
let err = JacsError::ConfigError("test".to_string());
let _: &dyn Error = &err;
}
#[test]
fn test_error_debug_format() {
let err = JacsError::CryptoError("test crypto error".to_string());
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("CryptoError"));
assert!(debug_str.contains("test crypto error"));
}
#[test]
fn test_document_not_found_error_is_actionable() {
let err = JacsError::DocumentNotFound {
document_id: "doc-abc-123".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("doc-abc-123"),
"Should include the document ID"
);
assert!(
msg.contains("not found"),
"Should state the document was not found"
);
assert!(
msg.contains("Verify") || msg.contains("stored"),
"Should provide guidance"
);
}
#[test]
fn test_file_not_found_error_is_actionable() {
let err = JacsError::FileNotFound {
path: "/path/to/missing.json".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("/path/to/missing.json"),
"Should include the file path"
);
assert!(
msg.contains("Ensure") || msg.contains("create"),
"Should provide guidance"
);
}
#[test]
fn test_file_read_failed_error_is_actionable() {
let err = JacsError::FileReadFailed {
path: "/path/to/file.json".to_string(),
reason: "permission denied".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("/path/to/file.json"),
"Should include the file path"
);
assert!(
msg.contains("permission denied"),
"Should include the reason"
);
assert!(
msg.contains("permission") || msg.contains("Check"),
"Should provide guidance"
);
}
#[test]
fn test_file_write_failed_error_is_actionable() {
let err = JacsError::FileWriteFailed {
path: "/path/to/output.json".to_string(),
reason: "disk full".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("/path/to/output.json"),
"Should include the file path"
);
assert!(msg.contains("disk full"), "Should include the reason");
assert!(
msg.contains("write") || msg.contains("Check"),
"Should provide guidance"
);
}
#[test]
fn test_directory_create_failed_error_is_actionable() {
let err = JacsError::DirectoryCreateFailed {
path: "/path/to/new_dir".to_string(),
reason: "permission denied".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("/path/to/new_dir"),
"Should include the directory path"
);
assert!(
msg.contains("permission denied"),
"Should include the reason"
);
assert!(
msg.contains("parent") || msg.contains("Check"),
"Should suggest checking parent directory"
);
}
#[test]
fn test_verification_claim_domain_error_is_actionable() {
let err = JacsError::VerificationClaimFailed {
claim: "verified".to_string(),
reason: "Verified agents must have jacsAgentDomain set".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("verified"), "Should state the claim");
assert!(
msg.contains("jacsAgentDomain"),
"Should mention the required field"
);
assert!(msg.contains("Fix:"), "Should provide fix guidance");
assert!(msg.contains("jacs.ai/docs"), "Should include docs link");
}
#[test]
fn test_verification_claim_generic_error_is_actionable() {
let err = JacsError::VerificationClaimFailed {
claim: "custom-claim".to_string(),
reason: "Requirements not met".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("custom-claim"), "Should state the claim");
assert!(msg.contains("Fix:"), "Should provide fix guidance");
assert!(
msg.contains("security requirements"),
"Should mention security requirements"
);
}
#[test]
fn test_search_error_display() {
let err = JacsError::SearchError("fulltext index unavailable".to_string());
let msg = err.to_string();
assert!(msg.contains("Search error"));
assert!(msg.contains("fulltext index unavailable"));
}
#[test]
fn test_storage_error_display() {
let err = JacsError::StorageError("backend unavailable".to_string());
let msg = err.to_string();
assert!(msg.contains("Storage error"));
assert!(msg.contains("backend unavailable"));
}
#[test]
fn test_database_error_display() {
let err = JacsError::DatabaseError {
operation: "store".to_string(),
reason: "connection refused".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("Database error"));
assert!(msg.contains("store"));
assert!(msg.contains("connection refused"));
}
#[test]
fn test_user_message_extracts_last_segment() {
let err = JacsError::Internal {
message: "Failed to load config: file not found: /path/to/config.json".to_string(),
};
let msg = err.user_message();
assert_eq!(msg, "/path/to/config.json");
}
#[test]
fn test_user_message_on_non_internal() {
let err = JacsError::CryptoError("key generation failed".to_string());
let msg = err.user_message();
assert!(msg.contains("key generation failed"));
}
#[test]
fn test_from_box_error_categorizes_password() {
let boxed: Box<dyn Error> = "password validation failed".into();
let err: JacsError = boxed.into();
assert!(
matches!(err, JacsError::CryptoError(_)),
"password error should be CryptoError, got: {:?}",
err
);
}
#[test]
fn test_from_box_error_categorizes_config() {
let boxed: Box<dyn Error> = "config file not found at /path".into();
let err: JacsError = boxed.into();
assert!(
matches!(err, JacsError::ConfigError(_)),
"config error should be ConfigError, got: {:?}",
err
);
}
#[test]
fn test_database_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<JacsError>();
let err = JacsError::DatabaseError {
operation: "query".to_string(),
reason: "timeout".to_string(),
};
let handle = std::thread::spawn(move || err.to_string());
let msg = handle.join().unwrap();
assert!(msg.contains("timeout"));
}
#[test]
fn test_from_env_error_not_found() {
let env_err = crate::storage::jenv::EnvError::NotFound("JACS_TEST_VAR".to_string());
let err: JacsError = env_err.into();
assert!(
matches!(err, JacsError::ConfigError(_)),
"EnvError::NotFound should map to ConfigError, got: {:?}",
err
);
assert!(err.to_string().contains("JACS_TEST_VAR"));
}
#[test]
fn test_from_env_error_empty() {
let env_err = crate::storage::jenv::EnvError::Empty("JACS_KEY".to_string());
let err: JacsError = env_err.into();
assert!(
matches!(err, JacsError::ConfigError(_)),
"EnvError::Empty should map to ConfigError, got: {:?}",
err
);
assert!(err.to_string().contains("JACS_KEY"));
}
#[cfg(feature = "a2a")]
#[test]
fn test_from_a2a_serialization_error() {
let a2a_err = crate::a2a::A2AError::SerializationError("bad json".to_string());
let err: JacsError = a2a_err.into();
assert!(
matches!(err, JacsError::DocumentError(_)),
"SerializationError should map to DocumentError, got: {:?}",
err
);
assert!(err.to_string().contains("A2A serialization"));
}
#[cfg(feature = "a2a")]
#[test]
fn test_from_a2a_signing_error() {
let a2a_err = crate::a2a::A2AError::SigningError("key missing".to_string());
let err: JacsError = a2a_err.into();
assert!(
matches!(err, JacsError::CryptoError(_)),
"SigningError should map to CryptoError, got: {:?}",
err
);
}
#[cfg(feature = "a2a")]
#[test]
fn test_from_a2a_validation_error() {
let a2a_err = crate::a2a::A2AError::ValidationError("schema mismatch".to_string());
let err: JacsError = a2a_err.into();
assert!(
matches!(err, JacsError::ValidationError(_)),
"A2A ValidationError should map to JacsError::ValidationError, got: {:?}",
err
);
}
#[cfg(feature = "a2a")]
#[test]
fn test_from_a2a_key_generation_error() {
let a2a_err = crate::a2a::A2AError::KeyGenerationError("entropy".to_string());
let err: JacsError = a2a_err.into();
assert!(
matches!(err, JacsError::CryptoError(_)),
"KeyGenerationError should map to CryptoError, got: {:?}",
err
);
}
#[test]
fn test_from_serde_json_error_converts_to_document_malformed() {
let bad_json = "{ invalid json }";
let serde_err = serde_json::from_str::<serde_json::Value>(bad_json).unwrap_err();
let jacs_err: JacsError = serde_err.into();
assert!(
matches!(jacs_err, JacsError::DocumentMalformed { .. }),
"serde_json::Error should map to DocumentMalformed, got: {:?}",
jacs_err
);
let msg = jacs_err.to_string();
assert!(
msg.contains("Malformed document"),
"Display should mention 'Malformed document'"
);
}
#[test]
fn test_from_base64_decode_error_converts_to_crypto() {
use base64::Engine;
let b64_err = base64::engine::general_purpose::STANDARD
.decode("not-valid-base64!!!")
.unwrap_err();
let jacs_err: JacsError = b64_err.into();
assert!(
matches!(jacs_err, JacsError::CryptoError(_)),
"base64::DecodeError should map to CryptoError, got: {:?}",
jacs_err
);
}
#[test]
fn test_config_not_found_display_includes_path_and_guidance() {
let err = JacsError::ConfigNotFound {
path: "/tmp/nonexistent.json".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("/tmp/nonexistent.json"),
"Should include the path"
);
assert!(
msg.contains("create") || msg.contains("Run"),
"Should provide actionable guidance"
);
}
#[test]
fn test_verification_claim_downgrade_error_is_actionable() {
let err = JacsError::VerificationClaimFailed {
claim: "unverified".to_string(),
reason: "Cannot downgrade from 'verified' to 'unverified'. Create a new agent instead."
.to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("downgrade") || msg.contains("Cannot"),
"Should explain downgrade block"
);
assert!(msg.contains("Fix:"), "Should provide fix guidance");
assert!(
msg.contains("new agent"),
"Should suggest creating new agent"
);
}
}