1use harn_vm::VmDictExt;
7
8use harn_vm::{VmError, VmValue};
9
10#[derive(Debug, thiserror::Error)]
17pub enum HostlibError {
18 #[error(
22 "hostlib: {builtin} is not implemented yet (scaffolded contract without an implementation)"
23 )]
24 Unimplemented {
25 builtin: &'static str,
27 },
28
29 #[error("hostlib: {builtin}: missing required parameter '{param}'")]
31 MissingParameter {
32 builtin: &'static str,
34 param: &'static str,
36 },
37
38 #[error("hostlib: {builtin}: invalid parameter '{param}': {message}")]
40 InvalidParameter {
41 builtin: &'static str,
43 param: &'static str,
45 message: String,
47 },
48
49 #[error("hostlib: {builtin}: {message}")]
51 Backend {
52 builtin: &'static str,
54 message: String,
56 },
57
58 #[error("hostlib: {builtin}: process spawn failed: {message}")]
61 ProcessSpawn {
62 builtin: &'static str,
64 kind: &'static str,
66 message: String,
68 requested_cwd: Option<String>,
71 cwd: String,
73 },
74
75 #[error("{message}")]
80 SandboxViolation {
81 builtin: &'static str,
83 path: String,
85 message: String,
88 },
89
90 #[error("{message}")]
92 SandboxUnsupported {
93 builtin: &'static str,
95 profile: String,
97 message: String,
99 },
100
101 #[error("{message}")]
110 CatastrophicFloor {
111 builtin: &'static str,
113 message: String,
115 },
116}
117
118impl HostlibError {
119 pub fn builtin(&self) -> &'static str {
122 match self {
123 HostlibError::Unimplemented { builtin }
124 | HostlibError::MissingParameter { builtin, .. }
125 | HostlibError::InvalidParameter { builtin, .. }
126 | HostlibError::Backend { builtin, .. }
127 | HostlibError::ProcessSpawn { builtin, .. }
128 | HostlibError::SandboxViolation { builtin, .. }
129 | HostlibError::SandboxUnsupported { builtin, .. }
130 | HostlibError::CatastrophicFloor { builtin, .. } => builtin,
131 }
132 }
133}
134
135impl From<HostlibError> for VmError {
136 fn from(err: HostlibError) -> VmError {
137 let kind = match &err {
141 HostlibError::Unimplemented { .. } => "unimplemented",
142 HostlibError::MissingParameter { .. } => "missing_parameter",
143 HostlibError::InvalidParameter { .. } => "invalid_parameter",
144 HostlibError::Backend { .. } => "backend_error",
145 HostlibError::ProcessSpawn { kind, .. } => *kind,
146 HostlibError::SandboxViolation { .. } => "tool_rejected",
147 HostlibError::SandboxUnsupported { .. } => "sandbox_unsupported",
148 HostlibError::CatastrophicFloor { .. } => "catastrophic_floor",
149 };
150 let path = match &err {
153 HostlibError::SandboxViolation { path, .. } => Some(path.clone()),
154 _ => None,
155 };
156 let profile = match &err {
157 HostlibError::SandboxUnsupported { profile, .. } => Some(profile.clone()),
158 _ => None,
159 };
160 let builtin = err.builtin();
161 let is_process_spawn = matches!(&err, HostlibError::ProcessSpawn { .. });
162 let process_cwd = match &err {
163 HostlibError::ProcessSpawn {
164 requested_cwd, cwd, ..
165 } => Some((requested_cwd.clone(), cwd.clone())),
166 _ => None,
167 };
168 let message = err.to_string();
169
170 let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
171 dict.put_str("kind", kind);
172 dict.put_str("builtin", builtin);
173 dict.put_str("message", message);
174 if is_process_spawn {
175 dict.put_str("error", "io_error");
176 dict.put_str("operation", "process_spawn");
177 dict.put_str("category", "environment");
178 }
179 if let Some((requested_cwd, cwd)) = process_cwd {
180 match requested_cwd {
181 Some(requested_cwd) => dict.put_str("requested_cwd", requested_cwd),
182 None => {
183 dict.insert(harn_vm::value::intern_key("requested_cwd"), VmValue::Nil);
184 }
185 }
186 dict.put_str("cwd", cwd);
187 }
188 if let Some(path) = path {
189 dict.put_str("path", path);
190 }
191 if let Some(profile) = profile {
192 dict.put_str("profile", profile);
193 }
194 VmError::Thrown(VmValue::dict(dict))
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn process_spawn_error_lowers_to_typed_io_value() {
204 let error = HostlibError::ProcessSpawn {
205 builtin: "hostlib_tools_run_command",
206 kind: "not_found",
207 message: "No such file or directory".to_string(),
208 requested_cwd: Some("/workspace/project".to_string()),
209 cwd: "/workspace/project".to_string(),
210 };
211 let VmError::Thrown(VmValue::Dict(fields)) = VmError::from(error) else {
212 panic!("expected a structured thrown value");
213 };
214 let field = |name| fields.get(name).map(VmValue::display);
215 assert_eq!(field("error").as_deref(), Some("io_error"));
216 assert_eq!(field("kind").as_deref(), Some("not_found"));
217 assert_eq!(field("operation").as_deref(), Some("process_spawn"));
218 assert_eq!(field("category").as_deref(), Some("environment"));
219 assert_eq!(
220 field("requested_cwd").as_deref(),
221 Some("/workspace/project")
222 );
223 assert_eq!(field("cwd").as_deref(), Some("/workspace/project"));
224 assert_eq!(
225 field("builtin").as_deref(),
226 Some("hostlib_tools_run_command")
227 );
228 }
229}