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 "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#[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#[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}