frame-host 0.4.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
//! The frame-host binary's own application composition — the FIRST consumer
//! of the embedding seam (design §4.2's unwired-seam rule: every seam entry
//! point has a production caller at the bytes).
//!
//! The bundled graph-view component ([`crate::manifest`]) rides
//! [`crate::spec::AppSpec`] and [`crate::application::run_application`]
//! exactly as a generated application does: same install path, same
//! readiness proof, same announcer, same ordered teardown.

use std::time::Duration;

use frame_core::runtime::RuntimePolicy;

use crate::application::run_application;
use crate::cli::Cli;
use crate::config::FrameConfig;
use crate::error::HostError;
use crate::manifest;
use crate::page::PageServer;
use crate::spec::{AppError, AppSpec, ComponentInstall};

// Explicit host policy: frame-core intentionally supplies no lifecycle
// defaults, so the embedding application declares them (same convention as
// the frame-cli scaffold's generated composition host).

/// Scheduler worker threads for the single-component tree this binary runs.
const SCHEDULER_THREADS: usize = 2;

/// Deadline bounding each spawn, mailbox round-trip, or tombstone observation.
const OPERATION_TIMEOUT: Duration = Duration::from_secs(3);

/// Runs the full frame server to completion: load config, boot the bundled
/// component and the embedded bus through the embedding seam, serve the
/// console until a shutdown signal (or a bus liveness failure), then tear
/// the whole stack down in order.
///
/// # Errors
///
/// Returns the first typed failure from config loading, boot, serving, or
/// ordered shutdown — the same contract as
/// [`crate::application::run_application`].
pub fn run(cli: &Cli) -> Result<(), HostError> {
    let config = FrameConfig::load(&cli.config)?;
    // Resolve and BIND the page server before booting the bus, so its real
    // address seeds the derived bus origin allow-list and the loud "serving at"
    // line states the actually-bound port (prefer-and-walk, or stated-exact).
    let page = PageServer::resolve(&config.frame)?;
    println!("frame-host serving at http://{}", page.local_addr());
    run_application(config, bundled_spec(), page)
}

/// The spec for the bundled graph-view component: its manifest and
/// bytecode, this binary's stated runtime policy, a readiness proof
/// requiring the presence child's fresh-incarnation liveness answer, and a
/// boot fact announcing the component's real contract and probe answer.
fn bundled_spec() -> AppSpec {
    AppSpec {
        components: vec![ComponentInstall {
            meta: manifest::component_meta(),
            bytecode: manifest::PRESENCE_BYTECODE.to_vec(),
            support_modules: Vec::new(),
        }],
        policy: RuntimePolicy {
            scheduler_threads: SCHEDULER_THREADS,
            operation_timeout: OPERATION_TIMEOUT,
            max_fragment_bytes: None,
        },
        readiness: Box::new(|registry| {
            let answer = registry
                .probe_child(manifest::component_id(), manifest::PRESENCE_CHILD)
                .map_err(|error| AppError::new(format!("liveness probe failed: {error}")))?;
            if answer == manifest::LIVENESS_MESSAGE {
                Ok(())
            } else {
                Err(AppError::new(format!(
                    "presence child answered liveness probe with {answer}, expected {}",
                    manifest::LIVENESS_MESSAGE
                )))
            }
        }),
        announce: Box::new(|registry, announcer| {
            // The announced fact is real end to end: the live probe answer
            // of the running presence child, under the component's contract.
            let answer = registry
                .probe_child(manifest::component_id(), manifest::PRESENCE_CHILD)
                .map_err(|error| AppError::new(format!("announce-time probe failed: {error}")))?;
            announcer
                .announce_fact(serde_json::json!({
                    "componentId": manifest::component_id().to_string(),
                    "contract": manifest::CONTRACT_ID,
                    "presenceProbe": answer,
                }))
                .map_err(|error| AppError::new(format!("fact announcement refused: {error}")))
        }),
    }
}