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}