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