Skip to main content

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}