Skip to main content

phoxal_bundle/
error.rs

1//! Bundle document, selection, and I/O failures.
2
3use std::path::PathBuf;
4
5use phoxal_model::component::capability::MotorCommand;
6use phoxal_model::identity::{CapabilityRef, ComponentInstanceId};
7use phoxal_model::{AssetId, Clock};
8use phoxal_runtime_contract::identity::{ParticipantArtifactId, ParticipantId};
9use phoxal_runtime_contract::metadata::ParticipantKind;
10use phoxal_runtime_contract::metadata::ParticipantRequirement;
11use phoxal_runtime_contract::version::RobotApiVersion;
12
13use crate::{BundlePath, BundlePathError, DigestError, ParticipantClock, Sha256Digest};
14
15/// Why a persisted runtime document is not a valid execution plan.
16#[derive(Clone, Debug, thiserror::Error)]
17pub enum DocumentError {
18    #[error(
19        "runtime graph has {count} participants; limit is {}",
20        crate::MAX_RUNTIME_PARTICIPANTS
21    )]
22    TooManyParticipants { count: usize },
23    #[error(
24        "artifact '{artifact}' speaks robot API {actual}, but this runtime already selected {expected}"
25    )]
26    MixedRobotApi {
27        artifact: ParticipantArtifactId,
28        expected: RobotApiVersion,
29        actual: RobotApiVersion,
30    },
31    #[error("duplicate participant id '{id}'")]
32    DuplicateParticipant { id: ParticipantId },
33    #[error("duplicate participant binary path '{path}'")]
34    DuplicateBinary { path: BundlePath },
35    #[error("participant '{participant}' references unknown artifact '{artifact}'")]
36    UnknownArtifact {
37        participant: ParticipantId,
38        artifact: ParticipantArtifactId,
39    },
40    #[error("artifact '{artifact}' contract names '{contract}'")]
41    ArtifactContractMismatch {
42        artifact: ParticipantArtifactId,
43        contract: ParticipantArtifactId,
44    },
45    #[error("artifact '{artifact}' path is outside bin/: {path}")]
46    ArtifactOutsideBin {
47        artifact: ParticipantArtifactId,
48        path: BundlePath,
49    },
50    #[error("brain artifact id is '{actual}', expected 'brain'")]
51    BrainArtifactId { actual: ParticipantArtifactId },
52    #[error("brain artifact path is '{actual}', expected 'bin/brain'")]
53    BrainArtifactPath { actual: BundlePath },
54    #[error("artifact '{artifact}' is not selected by any runtime participant")]
55    UnusedArtifact { artifact: ParticipantArtifactId },
56    #[error("participant '{participant}' names unknown component instance '{component_instance}'")]
57    UnknownComponent {
58        participant: ParticipantId,
59        component_instance: ComponentInstanceId,
60    },
61    #[error("driver participant '{participant}' has no component binding")]
62    MissingDriverComponent { participant: ParticipantId },
63    #[error(
64        "{kind:?} participant '{participant}' cannot bind component instance '{component_instance}'"
65    )]
66    UnexpectedComponent {
67        participant: ParticipantId,
68        kind: ParticipantKind,
69        component_instance: ComponentInstanceId,
70    },
71    #[error("runtime graph has no mandatory 'brain' participant")]
72    MissingBrain,
73    #[error("brain artifact is mounted as participant '{actual}', expected 'brain'")]
74    BrainIdMismatch { actual: ParticipantId },
75    #[error("runtime graph contains more than one brain participant")]
76    DuplicateBrain,
77    #[error("simulated runtime graph has no simulator world authority")]
78    MissingSimulator,
79    #[error("simulated runtime graph contains more than one simulator world authority")]
80    DuplicateSimulator,
81    #[error(
82        "participant '{participant}' clock {participant_clock:?} conflicts with robot clock {robot:?}"
83    )]
84    ClockMismatch {
85        participant: ParticipantId,
86        robot: Clock,
87        participant_clock: ParticipantClock,
88    },
89    #[error(
90        "{kind:?} participant '{participant}' clock {participant_clock:?} is incompatible with robot clock {robot:?}"
91    )]
92    ExecutionModeMismatch {
93        participant: ParticipantId,
94        kind: ParticipantKind,
95        robot: Clock,
96        participant_clock: ParticipantClock,
97    },
98    #[error(
99        "artifact '{artifact}' requires {requirement:?}, but the robot uses {actual} kinematics"
100    )]
101    RequirementKinematicsMismatch {
102        artifact: ParticipantArtifactId,
103        requirement: ParticipantRequirement,
104        actual: phoxal_model::robot::KinematicKind,
105    },
106    #[error("artifact '{artifact}' requires at least one {side} actuator")]
107    RequirementActuatorListEmpty {
108        artifact: ParticipantArtifactId,
109        side: &'static str,
110    },
111    #[error("artifact '{artifact}' requirement could not resolve actuator '{actuator}': {error}")]
112    RequirementActuatorInvalid {
113        artifact: ParticipantArtifactId,
114        actuator: CapabilityRef,
115        error: String,
116    },
117    #[error(
118        "artifact '{artifact}' actuator '{actuator}' is configured for {actual:?}, but the binary requires {expected:?}"
119    )]
120    RequirementMotorModeMismatch {
121        artifact: ParticipantArtifactId,
122        actuator: CapabilityRef,
123        expected: MotorCommand,
124        actual: MotorCommand,
125    },
126    #[error("artifact '{artifact}' has an invalid config schema: {error}")]
127    InvalidConfigSchema {
128        artifact: ParticipantArtifactId,
129        error: String,
130    },
131    #[error("participant '{participant}' config does not match its binary schema: {error}")]
132    InvalidConfig {
133        participant: ParticipantId,
134        error: String,
135    },
136    #[error("asset id '{id}' is persisted at the wrong path '{path}'", id = id.as_str())]
137    AssetPathMismatch { id: AssetId, path: BundlePath },
138    #[error("asset path is outside assets/: {path}")]
139    AssetOutsideAssets { path: BundlePath },
140    #[error("duplicate asset id '{id}'", id = id.as_str())]
141    DuplicateAssetId { id: AssetId },
142    #[error("duplicate asset path '{path}'")]
143    DuplicateAssetPath { path: BundlePath },
144    #[error("router config is outside assets/: {path}")]
145    RouterOutsideAssets { path: BundlePath },
146    #[error("router config is not present in the asset index: {path}")]
147    RouterMissingAsset { path: BundlePath },
148    #[error("supplied asset bytes do not match the persisted asset index")]
149    AssetIndexMismatch,
150    #[error("supplied executable sources do not match the persisted artifact set")]
151    BinaryIndexMismatch,
152    #[error("bundle path is not valid: {0}")]
153    Path(#[from] BundlePathError),
154}
155
156/// Why an exact participant selection failed.
157#[derive(Clone, Debug, thiserror::Error)]
158pub enum SelectionError {
159    #[error("runtime bundle has no participant '{requested}'")]
160    Unknown { requested: ParticipantId },
161    #[error("participant '{participant}' references missing artifact '{artifact}'")]
162    MissingArtifact {
163        participant: ParticipantId,
164        artifact: ParticipantArtifactId,
165    },
166}
167
168/// Why reading or validating a bundle failed.
169#[derive(Debug, thiserror::Error)]
170pub enum BundleError {
171    #[error("failed to resolve bundle root {}: {source}", path.display())]
172    Root {
173        path: PathBuf,
174        #[source]
175        source: std::io::Error,
176    },
177    #[error("bundle root is not a directory: {0}")]
178    NotDirectory(PathBuf),
179    #[error("bundle target already exists: {0}")]
180    TargetExists(PathBuf),
181    #[error("secure no-follow opening is unsupported for bundle file {path}")]
182    UnsupportedSecureOpen { path: PathBuf },
183    #[error("atomic no-replace publication is unsupported for bundle target {path}")]
184    UnsupportedAtomicPublish { path: PathBuf },
185    #[error("failed to read runtime document {}: {source}", path.display())]
186    ReadDocument {
187        path: PathBuf,
188        #[source]
189        source: std::io::Error,
190    },
191    #[error("runtime document is not valid JSON: {0}")]
192    DocumentJson(#[from] serde_json::Error),
193    #[error(transparent)]
194    Document(#[from] DocumentError),
195    #[error("bundle contains forbidden symlink {path}")]
196    ForbiddenSymlink { path: PathBuf },
197    #[error("bundle contains unsupported filesystem entry {path}")]
198    UnsupportedEntry { path: PathBuf },
199    #[error("bundle executable is not executable: {path}")]
200    NotExecutable { path: PathBuf },
201    #[error(
202        "bundle executable {path} has mode {actual:#05o}, expected canonical mode {expected:#05o}"
203    )]
204    ExecutableMode {
205        path: PathBuf,
206        expected: u32,
207        actual: u32,
208    },
209    #[error(
210        "bundle data file {path} has mode {actual:#05o}, expected canonical mode {expected:#05o}"
211    )]
212    DataFileMode {
213        path: PathBuf,
214        expected: u32,
215        actual: u32,
216    },
217    #[error(
218        "bundle directory {path} has mode {actual:#05o}, expected canonical mode {expected:#05o}"
219    )]
220    DirectoryMode {
221        path: PathBuf,
222        expected: u32,
223        actual: u32,
224    },
225    #[error("bundle contains unexpected file {path}")]
226    UnexpectedFile { path: PathBuf },
227    #[error("bundle contains unindexed directory {path}")]
228    UnindexedDirectory { path: PathBuf },
229    #[error("bundle is missing required file {path}")]
230    MissingFile { path: PathBuf },
231    #[error("failed to read bundle file {path}: {source}", path = path.display())]
232    ReadFile {
233        path: PathBuf,
234        #[source]
235        source: std::io::Error,
236    },
237    #[error("bundle file {path} changed after indexing: expected {expected}, found {actual}")]
238    Integrity {
239        path: PathBuf,
240        expected: Sha256Digest,
241        actual: Sha256Digest,
242    },
243    #[error("bundle file {path} has size {actual}, expected {expected}")]
244    Size {
245        path: PathBuf,
246        expected: u64,
247        actual: u64,
248    },
249    #[error("asset '{id}' is not declared by runtime.json", id = id.as_str())]
250    UndeclaredAsset { id: AssetId },
251    #[error(transparent)]
252    Path(#[from] BundlePathError),
253    #[error(transparent)]
254    Digest(#[from] DigestError),
255    #[error(transparent)]
256    Selection(#[from] SelectionError),
257}