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 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}