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