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