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;
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    /// A host capability cannot preserve the active sandbox contract.
74    #[error("{message}")]
75    SandboxUnsupported {
76        /// Fully-qualified builtin name.
77        builtin: &'static str,
78        /// Active sandbox profile.
79        profile: String,
80        /// Stable rejection message.
81        message: String,
82    },
83
84    /// A never-approvable UNIVERSAL catastrophic command (machine/disk/data
85    /// destruction) was rejected by the floor BEFORE spawning, at the shared
86    /// [`crate::process::spawn_process`] chokepoint. Enforced unconditionally
87    /// (no `command_policy` required), so it is universal across every hostlib
88    /// process tool, embedders, and standalone Harn. Mirrors the
89    /// `catastrophic_floor` disposition the `process.exec` command-policy
90    /// preflight surfaces. See
91    /// [`harn_vm::orchestration::universal_catastrophic_reason`].
92    #[error("{message}")]
93    CatastrophicFloor {
94        /// Fully-qualified builtin name.
95        builtin: &'static str,
96        /// The verbatim floor rationale (never-approvable reason).
97        message: String,
98    },
99}
100
101impl HostlibError {
102    /// The fully-qualified builtin name this error came from. Useful for
103    /// embedder logging and for the routing tests in `tests/`.
104    pub fn builtin(&self) -> &'static str {
105        match self {
106            HostlibError::Unimplemented { builtin }
107            | HostlibError::MissingParameter { builtin, .. }
108            | HostlibError::InvalidParameter { builtin, .. }
109            | HostlibError::Backend { builtin, .. }
110            | HostlibError::SandboxViolation { builtin, .. }
111            | HostlibError::SandboxUnsupported { builtin, .. }
112            | HostlibError::CatastrophicFloor { builtin, .. } => builtin,
113        }
114    }
115}
116
117impl From<HostlibError> for VmError {
118    fn from(err: HostlibError) -> VmError {
119        // Surface as a `Thrown` dict so Harn `try`/`catch` can pattern-match
120        // on `kind`, `builtin`, and `message`. This matches how the existing
121        // `host_call` error path shapes its exceptions.
122        let kind = match err {
123            HostlibError::Unimplemented { .. } => "unimplemented",
124            HostlibError::MissingParameter { .. } => "missing_parameter",
125            HostlibError::InvalidParameter { .. } => "invalid_parameter",
126            HostlibError::Backend { .. } => "backend_error",
127            HostlibError::SandboxViolation { .. } => "tool_rejected",
128            HostlibError::SandboxUnsupported { .. } => "sandbox_unsupported",
129            HostlibError::CatastrophicFloor { .. } => "catastrophic_floor",
130        };
131        // Carry the offending path on sandbox violations so `catch` blocks
132        // and telemetry can branch on it without re-parsing the message.
133        let path = match &err {
134            HostlibError::SandboxViolation { path, .. } => Some(path.clone()),
135            _ => None,
136        };
137        let profile = match &err {
138            HostlibError::SandboxUnsupported { profile, .. } => Some(profile.clone()),
139            _ => None,
140        };
141        let builtin = err.builtin();
142        let message = err.to_string();
143
144        let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
145        dict.put_str("kind", kind);
146        dict.put_str("builtin", builtin);
147        dict.put_str("message", message);
148        if let Some(path) = path {
149            dict.put_str("path", path);
150        }
151        if let Some(profile) = profile {
152            dict.put_str("profile", profile);
153        }
154        VmError::Thrown(VmValue::dict(dict))
155    }
156}