harn_hostlib/error.rs
1//! Error type for hostlib host calls.
2//!
3//! Builtins translate this into VM-level errors via [`Into<harn_vm::VmError>`]
4//! so that Harn scripts see structured exceptions rather than panics.
5
6use harn_vm::VmDictExt;
7
8use harn_vm::{VmError, VmValue};
9
10/// All errors a hostlib builtin can surface.
11///
12/// Variants intentionally describe the *kind* of failure rather than the
13/// specific module — every module routes its missing-implementation errors
14/// through [`HostlibError::Unimplemented`] so embedders and tests can
15/// distinguish intentionally scaffolded contracts from runtime failures.
16#[derive(Debug, thiserror::Error)]
17pub enum HostlibError {
18 /// The method exists in the registration table but has no implementation
19 /// yet. This is the canonical scaffold-stage error: it tells callers
20 /// "the contract is stable, but this module has not been implemented."
21 #[error(
22 "hostlib: {builtin} is not implemented yet (scaffolded contract without an implementation)"
23 )]
24 Unimplemented {
25 /// Fully-qualified builtin name, e.g. `"hostlib_ast_parse_file"`.
26 builtin: &'static str,
27 },
28
29 /// A required parameter was missing from the call payload.
30 #[error("hostlib: {builtin}: missing required parameter '{param}'")]
31 MissingParameter {
32 /// Fully-qualified builtin name.
33 builtin: &'static str,
34 /// Name of the missing parameter.
35 param: &'static str,
36 },
37
38 /// A parameter was present but had the wrong shape (wrong type, malformed).
39 #[error("hostlib: {builtin}: invalid parameter '{param}': {message}")]
40 InvalidParameter {
41 /// Fully-qualified builtin name.
42 builtin: &'static str,
43 /// Name of the invalid parameter.
44 param: &'static str,
45 /// Human-readable description of the violation.
46 message: String,
47 },
48
49 /// Catch-all wrapper for I/O, parsing, or other backend failures.
50 #[error("hostlib: {builtin}: {message}")]
51 Backend {
52 /// Fully-qualified builtin name.
53 builtin: &'static str,
54 /// Human-readable failure description.
55 message: String,
56 },
57
58 /// A path the builtin resolved fell outside the session's workspace
59 /// roots under a restricted sandbox profile. The mirror of the
60 /// `harness.fs.*` `tool_rejected` rejection — both surfaces reject an
61 /// out-of-root path with the same message.
62 #[error("{message}")]
63 SandboxViolation {
64 /// Fully-qualified builtin name.
65 builtin: &'static str,
66 /// The normalized path that was rejected, for telemetry.
67 path: String,
68 /// The canonical rejection message (see
69 /// [`harn_vm::process_sandbox::SandboxViolation::message`]).
70 message: String,
71 },
72}
73
74impl HostlibError {
75 /// The fully-qualified builtin name this error came from. Useful for
76 /// embedder logging and for the routing tests in `tests/`.
77 pub fn builtin(&self) -> &'static str {
78 match self {
79 HostlibError::Unimplemented { builtin }
80 | HostlibError::MissingParameter { builtin, .. }
81 | HostlibError::InvalidParameter { builtin, .. }
82 | HostlibError::Backend { builtin, .. }
83 | HostlibError::SandboxViolation { builtin, .. } => builtin,
84 }
85 }
86}
87
88impl From<HostlibError> for VmError {
89 fn from(err: HostlibError) -> VmError {
90 // Surface as a `Thrown` dict so Harn `try`/`catch` can pattern-match
91 // on `kind`, `builtin`, and `message`. This matches how the existing
92 // `host_call` error path shapes its exceptions.
93 let kind = match err {
94 HostlibError::Unimplemented { .. } => "unimplemented",
95 HostlibError::MissingParameter { .. } => "missing_parameter",
96 HostlibError::InvalidParameter { .. } => "invalid_parameter",
97 HostlibError::Backend { .. } => "backend_error",
98 HostlibError::SandboxViolation { .. } => "tool_rejected",
99 };
100 // Carry the offending path on sandbox violations so `catch` blocks
101 // and telemetry can branch on it without re-parsing the message.
102 let path = match &err {
103 HostlibError::SandboxViolation { path, .. } => Some(path.clone()),
104 _ => None,
105 };
106 let builtin = err.builtin();
107 let message = err.to_string();
108
109 let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
110 dict.put_str("kind", kind);
111 dict.put_str("builtin", builtin);
112 dict.put_str("message", message);
113 if let Some(path) = path {
114 dict.put_str("path", path);
115 }
116 VmError::Thrown(VmValue::dict(dict))
117 }
118}