Skip to main content

soma_provider_core/
error.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
4pub struct ProviderError {
5    pub kind: &'static str,
6    pub schema_version: u32,
7    pub code: Box<str>,
8    pub provider: Box<str>,
9    pub action: Option<Box<str>>,
10    pub message: Box<str>,
11    pub retryable: bool,
12    pub remediation: Box<str>,
13    #[serde(flatten, skip_serializing_if = "Option::is_none")]
14    context: Option<Box<ProviderErrorContext>>,
15}
16
17#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
18struct ProviderErrorContext {
19    #[serde(skip_serializing_if = "Option::is_none")]
20    phase: Option<Box<str>>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    source: Option<Box<str>>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    provider_kind: Option<Box<str>>,
25    #[serde(skip)]
26    private_diagnostics: Option<Box<str>>,
27}
28
29impl ProviderError {
30    pub fn new(
31        code: impl Into<String>,
32        provider: impl Into<String>,
33        action: Option<String>,
34        message: impl Into<String>,
35        remediation: impl Into<String>,
36    ) -> Self {
37        Self {
38            kind: "provider_error",
39            schema_version: 1,
40            code: code.into().into_boxed_str(),
41            provider: provider.into().into_boxed_str(),
42            action: action.map(String::into_boxed_str),
43            message: redact_public(&message.into()).into_boxed_str(),
44            retryable: false,
45            remediation: remediation.into().into_boxed_str(),
46            context: None,
47        }
48    }
49
50    pub fn validation(
51        provider: impl Into<String>,
52        action: impl Into<String>,
53        code: impl Into<String>,
54        message: impl Into<String>,
55    ) -> Self {
56        Self::new(
57            code,
58            provider,
59            Some(action.into()),
60            message,
61            "Change the provider call input and retry.",
62        )
63    }
64
65    pub fn tool_not_found(tool: impl Into<String>) -> Self {
66        let tool = tool.into();
67        Self::validation(
68            "registry",
69            &tool,
70            "unknown_action",
71            format!("unknown provider action `{tool}`"),
72        )
73    }
74
75    pub fn execution(
76        provider: impl Into<String>,
77        action: impl Into<String>,
78        error: impl std::fmt::Display,
79    ) -> Self {
80        let diagnostic = error.to_string();
81        Self::new(
82            "provider_execution_failed",
83            provider,
84            Some(action.into()),
85            redact_public(&diagnostic),
86            "Check provider status and retry after the upstream issue is resolved.",
87        )
88        .with_private_diagnostics(diagnostic)
89    }
90
91    pub fn opaque_execution(
92        provider: impl Into<String>,
93        action: impl Into<String>,
94        error: impl std::fmt::Display,
95    ) -> Self {
96        let diagnostic = error.to_string();
97        Self::new(
98            "provider_execution_failed",
99            provider,
100            Some(action.into()),
101            "Provider execution failed. Check server logs for details.",
102            "Check provider status and retry after the upstream issue is resolved.",
103        )
104        .with_private_diagnostics(diagnostic)
105    }
106
107    pub fn with_retryable(mut self, retryable: bool) -> Self {
108        self.retryable = retryable;
109        self
110    }
111
112    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
113        self.context_mut().phase = Some(phase.into().into_boxed_str());
114        self
115    }
116
117    pub fn with_source(mut self, source: impl Into<String>) -> Self {
118        self.context_mut().source = Some(source.into().into_boxed_str());
119        self
120    }
121
122    pub fn with_provider_kind(mut self, provider_kind: impl Into<String>) -> Self {
123        self.context_mut().provider_kind = Some(provider_kind.into().into_boxed_str());
124        self
125    }
126
127    pub fn with_private_diagnostics(mut self, diagnostics: impl Into<String>) -> Self {
128        self.context_mut().private_diagnostics = Some(diagnostics.into().into_boxed_str());
129        self
130    }
131
132    pub fn log_code(&self) -> (&str, Option<&str>, &str) {
133        (&self.provider, self.action.as_deref(), &self.code)
134    }
135
136    pub fn private_diagnostics(&self) -> Option<&str> {
137        self.context
138            .as_deref()
139            .and_then(|context| context.private_diagnostics.as_deref())
140    }
141
142    fn context_mut(&mut self) -> &mut ProviderErrorContext {
143        self.context
144            .get_or_insert_with(|| Box::new(ProviderErrorContext::default()))
145            .as_mut()
146    }
147}
148
149impl std::fmt::Display for ProviderError {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{}: {}", self.code, self.message)
152    }
153}
154
155impl std::error::Error for ProviderError {}
156
157pub fn redact_public(input: &str) -> String {
158    let mut output = input.to_owned();
159    let lower = output.to_ascii_lowercase();
160    let markers = [
161        "authorization:",
162        "bearer ",
163        "token=",
164        "api_key=",
165        "apikey=",
166        "cookie:",
167        "set-cookie:",
168        "password=",
169        "secret=",
170        "stderr:",
171        "body:",
172    ];
173    if markers.iter().any(|marker| lower.contains(marker)) {
174        output = "[redacted provider diagnostic]".to_owned();
175    }
176    for (key, value) in std::env::vars() {
177        if key.ends_with("_TOKEN")
178            || key.ends_with("_KEY")
179            || key.ends_with("_SECRET")
180            || key.contains("PASSWORD")
181        {
182            output = output.replace(&key, "[redacted env]");
183            if value.len() >= 4 {
184                output = output.replace(&value, "[redacted env]");
185            }
186        }
187    }
188    output
189}