rlmesh_sandbox/error.rs
1//! Public error type for the rlmesh-sandbox crate.
2//!
3//! The crate uses [`anyhow`] internally for ergonomic context propagation but
4//! exposes this typed [`SandboxError`] enum on its public API so consumers can
5//! discriminate failure classes (e.g. "docker unavailable" vs "unpinned HF
6//! source" vs "invalid options") without matching on `Display` text.
7
8use thiserror::Error;
9
10/// Errors returned by the public rlmesh-sandbox API.
11///
12/// This enum is `#[non_exhaustive]`: new variants may be added in future
13/// releases, so downstream `match` expressions must include a wildcard arm.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum SandboxError {
17 /// An environment source reference could not be parsed.
18 #[error("invalid sandbox source: {message}")]
19 InvalidSource {
20 /// Human-readable description of the parse failure.
21 message: String,
22 },
23
24 /// A sandbox option (base image, packages, imports, num_envs,
25 /// vectorization mode, rlmesh package spec, ...) was invalid.
26 #[error("invalid sandbox option: {message}")]
27 InvalidOption {
28 /// Human-readable description of the invalid option.
29 message: String,
30 },
31
32 /// An `hf://` source was rejected by the trust/pinning policy (remote code
33 /// not trusted, or revision not pinned to a full git SHA).
34 #[error("hugging face source policy violation: {message}")]
35 HuggingFacePolicy {
36 /// Human-readable description of the policy violation.
37 message: String,
38 },
39
40 /// Resolving an `hf://` revision (via `git ls-remote`) or materializing the
41 /// source tree failed.
42 #[error("failed to resolve hugging face source: {message}")]
43 SourceResolution {
44 /// Human-readable description of the resolution failure.
45 message: String,
46 },
47
48 /// Selecting or reading a local RLMesh wheel failed.
49 #[error("rlmesh wheel resolution failed: {message}")]
50 Wheel {
51 /// Human-readable description of the wheel resolution failure.
52 message: String,
53 },
54
55 /// Building the sandbox image failed (e.g. `docker build` returned an
56 /// error, or the build context could not be assembled).
57 #[error("sandbox image build failed: {message}")]
58 ImageBuild {
59 /// Human-readable description of the build failure.
60 message: String,
61 },
62
63 /// Starting the container or waiting for it to become ready failed.
64 #[error("sandbox container failed to start: {message}")]
65 ContainerStartup {
66 /// Human-readable description of the startup failure, including any
67 /// captured container state and logs.
68 message: String,
69 },
70
71 /// Invoking the `docker` CLI failed (e.g. docker is not installed or the
72 /// daemon is unavailable), or a docker subcommand returned an error.
73 #[error("docker command failed: {message}")]
74 Docker {
75 /// Human-readable description of the docker failure.
76 message: String,
77 },
78}
79
80impl SandboxError {
81 pub(crate) fn invalid_source(err: impl std::fmt::Display) -> Self {
82 Self::InvalidSource {
83 message: err.to_string(),
84 }
85 }
86
87 pub(crate) fn invalid_option(err: impl std::fmt::Display) -> Self {
88 Self::InvalidOption {
89 message: err.to_string(),
90 }
91 }
92
93 pub(crate) fn huggingface_policy(err: impl std::fmt::Display) -> Self {
94 Self::HuggingFacePolicy {
95 message: err.to_string(),
96 }
97 }
98
99 pub(crate) fn source_resolution(err: impl std::fmt::Display) -> Self {
100 Self::SourceResolution {
101 message: err.to_string(),
102 }
103 }
104
105 pub(crate) fn wheel(err: impl std::fmt::Display) -> Self {
106 Self::Wheel {
107 message: err.to_string(),
108 }
109 }
110
111 pub(crate) fn container_startup(err: impl std::fmt::Display) -> Self {
112 Self::ContainerStartup {
113 message: err.to_string(),
114 }
115 }
116
117 /// Classify an internal docker-backend error. If the failure was an
118 /// inability to spawn the `docker` CLI (e.g. it is not installed or the
119 /// daemon socket is unreachable), it is reported as [`SandboxError::Docker`]
120 /// regardless of which operation triggered it; otherwise the supplied
121 /// operation-specific variant is used.
122 pub(crate) fn from_docker_op(
123 err: anyhow::Error,
124 operation: impl FnOnce(String) -> Self,
125 ) -> Self {
126 if let Some(io_err) = err
127 .chain()
128 .find_map(|cause| cause.downcast_ref::<std::io::Error>())
129 && io_err.kind() == std::io::ErrorKind::NotFound
130 {
131 return Self::Docker {
132 message: format!("docker CLI not found: {err:#}"),
133 };
134 }
135 operation(format!("{err:#}"))
136 }
137}