frame-host 0.3.0

Frame host server and embedding seam — boots an application's frame-core component tree with an embedded liminal bus, announces the host's real application events on the bus, and serves the built frame page
Documentation
//! Typed failures for every host boot, serve, and shutdown path.

use std::net::SocketAddr;
use std::path::PathBuf;

use frame_core::capability::CapabilityMutationError;
use frame_core::component::ComponentId;
use frame_core::error::RegistryError;
use frame_core::event::LifecycleState;
use thiserror::Error;

/// A typed failure from the frame host.
#[derive(Debug, Error)]
pub enum HostError {
    /// The embedding application's own hook (readiness proof or fact
    /// announcement) refused.
    #[error("application {stage} failed: {source}")]
    Application {
        /// Which application hook refused (`readiness`, `announce`).
        stage: &'static str,
        /// The application's typed failure.
        #[source]
        source: crate::spec::AppError,
    },
    /// The application-event announcer failed to connect to the embedded
    /// bus at boot. A typed boot failure — never a silently event-less page.
    #[error(
        "application-event announcer failed to connect to the embedded bus at {address}: {detail}"
    )]
    AnnouncerConnect {
        /// The embedded bus TCP wire address the connect targeted.
        address: String,
        /// Exact SDK connect failure.
        detail: String,
    },
    /// The application-event announcer failed to publish on the bus. At
    /// boot this is a boot failure; at runtime the announcer records the
    /// death loudly and the served page shows it by absence of events.
    #[error("application-event announcer failed to publish on '{channel}': {detail}")]
    AnnouncerPublish {
        /// The application channel the publish targeted.
        channel: String,
        /// Exact SDK publish failure.
        detail: String,
    },
    /// A fact was announced after teardown had already closed the
    /// announcer's intake.
    #[error("application-event announcer intake is closed: teardown has begun")]
    AnnouncerIntakeClosed,
    /// The announcer pump was started twice or its subscription was
    /// already consumed — a host composition error.
    #[error("application-event announcer pump was already started")]
    AnnouncerAlreadyStarted,
    /// The announcer pump thread could not be spawned.
    #[error("failed to spawn application-event announcer pump thread: {source}")]
    AnnouncerSpawn {
        /// Underlying spawn failure.
        #[source]
        source: std::io::Error,
    },
    /// The announcer pump thread panicked.
    #[error("application-event announcer pump thread panicked")]
    AnnouncerPanicked,
    /// The composed component runtime refused or failed an operation
    /// (composition, policy validation, or residue-checked teardown).
    #[error("component runtime operation failed")]
    Runtime(#[from] frame_core::runtime::RuntimeError),
    /// The component registry refused or failed an operation.
    #[error("component registry operation failed")]
    Registry(#[from] RegistryError),
    /// The host capability facade refused an operation.
    #[error("host capability operation failed")]
    Capability(#[from] CapabilityMutationError),
    /// An installed component vanished from the registry between operations.
    #[error("component {id} has no status snapshot in the registry")]
    StatusMissing {
        /// Identity whose snapshot was absent.
        id: ComponentId,
    },
    /// An installed component did not reach Running after start.
    #[error("component {id} is {state:?} after start instead of Running")]
    NotRunning {
        /// Identity that failed to reach Running.
        id: ComponentId,
        /// Observed lifecycle state.
        state: LifecycleState,
    },
    /// The lifecycle event logger thread could not be spawned.
    #[error("failed to spawn lifecycle event logger thread: {source}")]
    EventLoggerSpawn {
        /// Underlying spawn failure.
        #[source]
        source: std::io::Error,
    },
    /// The lifecycle event logger thread panicked.
    #[error("lifecycle event logger thread panicked")]
    EventLoggerPanicked,
    /// The application-truth recorder thread ([`crate::truth::AppTruth`])
    /// could not be spawned.
    #[error("failed to spawn app-truth recorder thread: {source}")]
    TruthRecorderSpawn {
        /// Underlying spawn failure.
        #[source]
        source: std::io::Error,
    },
    /// The application-truth recorder thread panicked.
    #[error("app-truth recorder thread panicked")]
    TruthRecorderPanicked,
    /// The asset directory is unusable as a shell root.
    #[error("asset directory {path} is unusable: {detail}")]
    AssetRoot {
        /// Configured asset directory.
        path: PathBuf,
        /// Exact refusal detail.
        detail: String,
    },
    /// The served config would be refused by the shell's config contract.
    #[error("shell config contract violation: {detail}")]
    ConfigContract {
        /// Exact contract refusal and the flag that fixes it.
        detail: String,
    },
    /// The asset directory has no `index.html` shell entry point.
    #[error(
        "asset directory {path} has no index.html; refusing to serve a shell with no entry point"
    )]
    MissingIndex {
        /// Configured asset directory.
        path: PathBuf,
    },
    /// Binding the shell listener failed.
    #[error("failed to bind shell server on {addr}: {source}")]
    Bind {
        /// Requested socket address.
        addr: SocketAddr,
        /// Underlying bind failure.
        #[source]
        source: std::io::Error,
    },
    /// A stated `[frame].bind` page-server address is already in use. The page
    /// server binds exactly what `[frame].bind` states and never silently
    /// moves — this is the loud refusal (2026-07-22 portless ruling).
    #[error(
        "page server address {addr} ([frame].bind) is already in use: {source}. The page server \
         binds exactly the stated [frame].bind and never silently moves to another port. Free \
         {addr}, or omit [frame].bind entirely to let the host prefer 127.0.0.1:4190 and walk \
         forward to a free port. If you deliberately moved [frame].bind, also update \
         [bus.websocket].allowed_origins to the page's new origin (http://HOST:PORT) or the \
         served page is Origin-refused by the bus."
    )]
    PageServerUnavailable {
        /// The stated page-server address that could not be bound.
        addr: SocketAddr,
        /// Underlying bind failure.
        #[source]
        source: std::io::Error,
    },
    /// The forward walk from the preferred page-server port found no free port
    /// anywhere above it — a genuinely exhausted local port space, never a
    /// silent give-up.
    #[error(
        "no free page-server port found walking forward from {from}: the entire port range above \
         the preferred port is in use. Free a port, or state an explicit [frame].bind."
    )]
    NoFreePagePort {
        /// The preferred port the exhausted walk started from.
        from: u16,
    },
    /// The shell server failed while serving.
    #[error("shell server failed: {source}")]
    Serve {
        /// Underlying accept/serve failure.
        #[source]
        source: std::io::Error,
    },
    /// The async runtime hosting the shell server could not be built.
    #[error("failed to build tokio runtime for the shell server: {source}")]
    AsyncRuntime {
        /// Underlying builder failure.
        #[source]
        source: std::io::Error,
    },
    /// The shutdown signal handler could not be installed or failed.
    #[error("shutdown signal handling failed: {source}")]
    ShutdownSignal {
        /// Underlying signal failure.
        #[source]
        source: std::io::Error,
    },
    /// Ordered shutdown left live processes on the scheduler.
    #[error("ordered stop leaked {count} live scheduler process(es)")]
    ProcessResidue {
        /// Processes still alive after the ordered drain.
        count: usize,
    },
    /// Host-internal synchronization was poisoned by a panic.
    #[error("host synchronization is poisoned")]
    SynchronizationPoisoned,
    /// The frame configuration file could not be read.
    #[error("failed to read frame config {path}: {source}")]
    ConfigRead {
        /// Configured frame.toml path.
        path: PathBuf,
        /// Underlying read failure.
        #[source]
        source: std::io::Error,
    },
    /// The frame configuration file could not be parsed as TOML into the
    /// `[frame]` + `[bus]` schema (or `[liminal]`, the deprecated alias of
    /// `[bus]` during the compatibility window), or its bus section was
    /// missing or doubled.
    #[error("failed to parse frame config {path}: {detail}")]
    ConfigParse {
        /// Configured frame.toml path.
        path: PathBuf,
        /// Exact TOML/deserialization refusal.
        detail: String,
    },
    /// The embedded `[bus]` config failed liminal's own validation.
    #[error("embedded bus config ([bus] section) is invalid: {source}")]
    LiminalConfig {
        /// Exact typed liminal validation failure.
        #[source]
        source: liminal_server::ServerError,
    },
    /// The `[bus]` config selects a shape the embedded frame server does
    /// not faithfully orchestrate (cluster, worker-front-door, or an absent
    /// WebSocket transport). Loud refusal at startup, never a silent downgrade.
    #[error("embedded frame mode does not support this bus shape: {detail}")]
    EmbeddedModeUnsupported {
        /// Which shape was refused and why.
        detail: String,
    },
    /// A liminal component failed to bind or boot. Frame-host exits nonzero
    /// with the component named — never a half-up stack.
    #[error("embedded liminal component '{component}' failed to boot: {source}")]
    LiminalComponent {
        /// Which liminal component failed (health endpoint, connection
        /// services, connection supervisor, TCP listener, WebSocket listener).
        component: &'static str,
        /// Exact typed liminal failure.
        #[source]
        source: liminal_server::ServerError,
    },
    /// Liminal's graceful shutdown sequence failed.
    #[error("embedded liminal graceful shutdown failed: {source}")]
    LiminalShutdown {
        /// Exact typed liminal shutdown failure.
        #[source]
        source: liminal_server::ServerError,
    },
    /// The embedded liminal component stopped answering at runtime while the
    /// host was still meant to be serving. A dead server behind a healthy host
    /// is forbidden: frame-host tears down and exits nonzero.
    #[error("embedded liminal component terminated unexpectedly at runtime: {detail}")]
    LiminalExited {
        /// Which liveness probe failed and against which address.
        detail: String,
    },
    /// The `[document]` initial-content file could not be read.
    #[error("failed to read [document].content_path {path}: {source}")]
    DocumentContent {
        /// The resolved content path.
        path: PathBuf,
        /// Underlying read failure.
        #[source]
        source: std::io::Error,
    },
    /// The document service's frame-state store refused an operation.
    #[error("document state store failed: {source}")]
    DocumentState {
        /// Exact typed frame-state failure.
        #[source]
        source: frame_state::StateError,
    },
    /// The document service refused to boot, run, or shut down.
    #[error("document service failed: {source}")]
    DocumentService {
        /// Exact typed service failure.
        #[source]
        source: frame_doc_service::DocServiceError,
    },
    /// The document authority's declared configuration was refused.
    #[error("[document] authority configuration refused: {source}")]
    DocumentAuthorityConfig {
        /// Exact typed configuration refusal.
        #[source]
        source: frame_authority::ConfigError,
    },
    /// The document binding's bus client (publisher or authoring
    /// subscriber) failed to connect or subscribe.
    #[error("document bus client '{component}' failed: {detail}")]
    DocumentBusClient {
        /// Which client failed (feed publisher, authoring subscriber).
        component: &'static str,
        /// Exact SDK failure detail.
        detail: String,
    },
    /// The authoring pump thread could not start or join.
    #[error("authoring pump thread failure: {detail}")]
    AuthoringPump {
        /// What failed.
        detail: String,
    },
}