Skip to main content

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