frame_host/error.rs
1//! Typed failures for every host boot, serve, and shutdown path.
2
3use std::net::SocketAddr;
4use std::path::PathBuf;
5
6use frame_core::capability::CapabilityMutationError;
7use frame_core::component::ComponentId;
8use frame_core::error::RegistryError;
9use frame_core::event::LifecycleState;
10use thiserror::Error;
11
12/// A typed failure from the frame host.
13#[derive(Debug, Error)]
14pub enum HostError {
15 /// The dev management door failed to open or serve (F-7b): attach,
16 /// conversation, or adapter-thread failure, rendered.
17 #[error("dev management door: {detail}")]
18 DevManagement {
19 /// The typed failure, rendered.
20 detail: String,
21 },
22 /// The embedding application's own hook (readiness proof or fact
23 /// announcement) refused.
24 #[error("application {stage} failed: {source}")]
25 Application {
26 /// Which application hook refused (`readiness`, `announce`).
27 stage: &'static str,
28 /// The application's typed failure.
29 #[source]
30 source: crate::spec::AppError,
31 },
32 /// The application-event announcer failed to connect to the embedded
33 /// bus at boot. A typed boot failure — never a silently event-less page.
34 #[error(
35 "application-event announcer failed to connect to the embedded bus at {address}: {detail}"
36 )]
37 AnnouncerConnect {
38 /// The embedded bus TCP wire address the connect targeted.
39 address: String,
40 /// Exact SDK connect failure.
41 detail: String,
42 },
43 /// The application-event announcer failed to publish on the bus. At
44 /// boot this is a boot failure; at runtime the announcer records the
45 /// death loudly and the served page shows it by absence of events.
46 #[error("application-event announcer failed to publish on '{channel}': {detail}")]
47 AnnouncerPublish {
48 /// The application channel the publish targeted.
49 channel: String,
50 /// Exact SDK publish failure.
51 detail: String,
52 },
53 /// A fact was announced after teardown had already closed the
54 /// announcer's intake.
55 #[error("application-event announcer intake is closed: teardown has begun")]
56 AnnouncerIntakeClosed,
57 /// The announcer pump was started twice or its subscription was
58 /// already consumed — a host composition error.
59 #[error("application-event announcer pump was already started")]
60 AnnouncerAlreadyStarted,
61 /// The announcer pump thread could not be spawned.
62 #[error("failed to spawn application-event announcer pump thread: {source}")]
63 AnnouncerSpawn {
64 /// Underlying spawn failure.
65 #[source]
66 source: std::io::Error,
67 },
68 /// The announcer pump thread panicked.
69 #[error("application-event announcer pump thread panicked")]
70 AnnouncerPanicked,
71 /// The composed component runtime refused or failed an operation
72 /// (composition, policy validation, or residue-checked teardown).
73 #[error("component runtime operation failed")]
74 Runtime(#[from] frame_core::runtime::RuntimeError),
75 /// The component registry refused or failed an operation.
76 #[error("component registry operation failed")]
77 Registry(#[from] RegistryError),
78 /// The host capability facade refused an operation.
79 #[error("host capability operation failed")]
80 Capability(#[from] CapabilityMutationError),
81 /// An installed component vanished from the registry between operations.
82 #[error("component {id} has no status snapshot in the registry")]
83 StatusMissing {
84 /// Identity whose snapshot was absent.
85 id: ComponentId,
86 },
87 /// An installed component did not reach Running after start.
88 #[error("component {id} is {state:?} after start instead of Running")]
89 NotRunning {
90 /// Identity that failed to reach Running.
91 id: ComponentId,
92 /// Observed lifecycle state.
93 state: LifecycleState,
94 },
95 /// The lifecycle event logger thread could not be spawned.
96 #[error("failed to spawn lifecycle event logger thread: {source}")]
97 EventLoggerSpawn {
98 /// Underlying spawn failure.
99 #[source]
100 source: std::io::Error,
101 },
102 /// The lifecycle event logger thread panicked.
103 #[error("lifecycle event logger thread panicked")]
104 EventLoggerPanicked,
105 /// The application-truth recorder thread ([`crate::truth::AppTruth`])
106 /// could not be spawned.
107 #[error("failed to spawn app-truth recorder thread: {source}")]
108 TruthRecorderSpawn {
109 /// Underlying spawn failure.
110 #[source]
111 source: std::io::Error,
112 },
113 /// The application-truth recorder thread panicked.
114 #[error("app-truth recorder thread panicked")]
115 TruthRecorderPanicked,
116 /// The asset directory is unusable as a shell root.
117 #[error("asset directory {path} is unusable: {detail}")]
118 AssetRoot {
119 /// Configured asset directory.
120 path: PathBuf,
121 /// Exact refusal detail.
122 detail: String,
123 },
124 /// The served config would be refused by the shell's config contract.
125 #[error("shell config contract violation: {detail}")]
126 ConfigContract {
127 /// Exact contract refusal and the flag that fixes it.
128 detail: String,
129 },
130 /// The asset directory has no `index.html` shell entry point.
131 #[error(
132 "asset directory {path} has no index.html; refusing to serve a shell with no entry point"
133 )]
134 MissingIndex {
135 /// Configured asset directory.
136 path: PathBuf,
137 },
138 /// Binding the shell listener failed.
139 #[error("failed to bind shell server on {addr}: {source}")]
140 Bind {
141 /// Requested socket address.
142 addr: SocketAddr,
143 /// Underlying bind failure.
144 #[source]
145 source: std::io::Error,
146 },
147 /// A stated `[frame].bind` page-server address is already in use. The page
148 /// server binds exactly what `[frame].bind` states and never silently
149 /// moves — this is the loud refusal (2026-07-22 portless ruling).
150 #[error(
151 "page server address {addr} ([frame].bind) is already in use: {source}. The page server \
152 binds exactly the stated [frame].bind and never silently moves to another port. Free \
153 {addr}, or omit [frame].bind entirely to let the host prefer 127.0.0.1:6010 and walk \
154 forward to a free port. If you deliberately moved [frame].bind, also update \
155 [bus.websocket].allowed_origins to the page's new origin (http://HOST:PORT) or the \
156 served page is Origin-refused by the bus."
157 )]
158 PageServerUnavailable {
159 /// The stated page-server address that could not be bound.
160 addr: SocketAddr,
161 /// Underlying bind failure.
162 #[source]
163 source: std::io::Error,
164 },
165 /// The forward walk from the preferred page-server port found no free port
166 /// anywhere above it — a genuinely exhausted local port space, never a
167 /// silent give-up.
168 #[error(
169 "no free page-server port found walking forward from {from}: the entire port range above \
170 the preferred port is in use. Free a port, or state an explicit [frame].bind."
171 )]
172 NoFreePagePort {
173 /// The preferred port the exhausted walk started from.
174 from: u16,
175 },
176 /// The shell server failed while serving.
177 #[error("shell server failed: {source}")]
178 Serve {
179 /// Underlying accept/serve failure.
180 #[source]
181 source: std::io::Error,
182 },
183 /// The async runtime hosting the shell server could not be built.
184 #[error("failed to build tokio runtime for the shell server: {source}")]
185 AsyncRuntime {
186 /// Underlying builder failure.
187 #[source]
188 source: std::io::Error,
189 },
190 /// The shutdown signal handler could not be installed or failed.
191 #[error("shutdown signal handling failed: {source}")]
192 ShutdownSignal {
193 /// Underlying signal failure.
194 #[source]
195 source: std::io::Error,
196 },
197 /// Ordered shutdown left live processes on the scheduler.
198 #[error("ordered stop leaked {count} live scheduler process(es)")]
199 ProcessResidue {
200 /// Processes still alive after the ordered drain.
201 count: usize,
202 },
203 /// Host-internal synchronization was poisoned by a panic.
204 #[error("host synchronization is poisoned")]
205 SynchronizationPoisoned,
206 /// The frame configuration file could not be read.
207 #[error("failed to read frame config {path}: {source}")]
208 ConfigRead {
209 /// Configured frame.toml path.
210 path: PathBuf,
211 /// Underlying read failure.
212 #[source]
213 source: std::io::Error,
214 },
215 /// The frame configuration file could not be parsed as TOML into the
216 /// `[frame]` + `[bus]` schema (or `[liminal]`, the deprecated alias of
217 /// `[bus]` during the compatibility window), or its bus section was
218 /// missing or doubled.
219 #[error("failed to parse frame config {path}: {detail}")]
220 ConfigParse {
221 /// Configured frame.toml path.
222 path: PathBuf,
223 /// Exact TOML/deserialization refusal.
224 detail: String,
225 },
226 /// The embedded `[bus]` config failed liminal's own validation.
227 #[error("embedded bus config ([bus] section) is invalid: {source}")]
228 LiminalConfig {
229 /// Exact typed liminal validation failure.
230 #[source]
231 source: liminal_server::ServerError,
232 },
233 /// The `[bus]` config selects a shape the embedded frame server does
234 /// not faithfully orchestrate (cluster, worker-front-door, or an absent
235 /// WebSocket transport). Loud refusal at startup, never a silent downgrade.
236 #[error("embedded frame mode does not support this bus shape: {detail}")]
237 EmbeddedModeUnsupported {
238 /// Which shape was refused and why.
239 detail: String,
240 },
241 /// A liminal component failed to bind or boot. Frame-host exits nonzero
242 /// with the component named — never a half-up stack.
243 #[error("embedded liminal component '{component}' failed to boot: {source}")]
244 LiminalComponent {
245 /// Which liminal component failed (health endpoint, connection
246 /// services, connection supervisor, TCP listener, WebSocket listener).
247 component: &'static str,
248 /// Exact typed liminal failure.
249 #[source]
250 source: liminal_server::ServerError,
251 },
252 /// Liminal's graceful shutdown sequence failed.
253 #[error("embedded liminal graceful shutdown failed: {source}")]
254 LiminalShutdown {
255 /// Exact typed liminal shutdown failure.
256 #[source]
257 source: liminal_server::ServerError,
258 },
259 /// The embedded liminal component stopped answering at runtime while the
260 /// host was still meant to be serving. A dead server behind a healthy host
261 /// is forbidden: frame-host tears down and exits nonzero.
262 #[error("embedded liminal component terminated unexpectedly at runtime: {detail}")]
263 LiminalExited {
264 /// Which liveness probe failed and against which address.
265 detail: String,
266 },
267 /// The `[document]` initial-content file could not be read.
268 #[error("failed to read [document].content_path {path}: {source}")]
269 DocumentContent {
270 /// The resolved content path.
271 path: PathBuf,
272 /// Underlying read failure.
273 #[source]
274 source: std::io::Error,
275 },
276 /// The document service's frame-state store refused an operation.
277 #[error("document state store failed: {source}")]
278 DocumentState {
279 /// Exact typed frame-state failure.
280 #[source]
281 source: frame_state::StateError,
282 },
283 /// The document service refused to boot, run, or shut down.
284 #[error("document service failed: {source}")]
285 DocumentService {
286 /// Exact typed service failure.
287 #[source]
288 source: frame_doc_service::DocServiceError,
289 },
290 /// The document authority's declared configuration was refused.
291 #[error("[document] authority configuration refused: {source}")]
292 DocumentAuthorityConfig {
293 /// Exact typed configuration refusal.
294 #[source]
295 source: frame_authority::ConfigError,
296 },
297 /// The document binding's bus client (publisher or authoring
298 /// subscriber) failed to connect or subscribe.
299 #[error("document bus client '{component}' failed: {detail}")]
300 DocumentBusClient {
301 /// Which client failed (feed publisher, authoring subscriber).
302 component: &'static str,
303 /// Exact SDK failure detail.
304 detail: String,
305 },
306 /// The authoring pump thread could not start or join.
307 #[error("authoring pump thread failure: {detail}")]
308 AuthoringPump {
309 /// What failed.
310 detail: String,
311 },
312}