pub mod app;
pub mod config;
pub mod crypto;
pub mod dto;
pub mod error;
pub mod pool;
pub mod port;
pub mod state;
pub mod wire_proxy;
pub use config::EdgeConfig;
pub use error::ApiError;
pub use pool::{BrainPool, BrainPoolConfig};
pub use port::{
BearerResolver, CredentialResolver, MeterEvent, MeteringSink, NoopMeter, Outcome,
ResolvedCredential, bearer_token,
};
pub use state::EdgeState;
pub use wire_proxy::{RateLimitConfig, WireProxyConfig};
pub use brain_db_sdk::wire::types::ActAs;
use std::time::Duration;
use axum::Router;
use axum::extract::State;
use axum::http::{HeaderName, StatusCode, header};
use axum::response::IntoResponse;
use axum::routing::get;
use tower_http::catch_panic::CatchPanicLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::TraceLayer;
use tracing::{error, info};
const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
fn panic_to_response(err: &Box<dyn std::any::Any + Send + 'static>) -> axum::response::Response {
let detail: &str = err.downcast_ref::<&str>().copied().unwrap_or_else(|| {
err.downcast_ref::<String>()
.map_or("non-string panic payload", String::as_str)
});
ApiError::internal(format!("handler panicked: {detail}")).into_response()
}
pub fn harden(router: Router, request_timeout: Duration, max_body_bytes: usize) -> Router {
router
.layer(CatchPanicLayer::custom(|e| panic_to_response(&e)))
.layer(TraceLayer::new_for_http())
.layer(SetSensitiveRequestHeadersLayer::new([
header::AUTHORIZATION,
HeaderName::from_static("x-api-key"),
]))
.layer(TimeoutLayer::with_status_code(
StatusCode::REQUEST_TIMEOUT,
request_timeout,
))
.layer(RequestBodyLimitLayer::new(max_body_bytes))
.layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER))
.layer(SetRequestIdLayer::new(REQUEST_ID_HEADER, MakeRequestUuid))
}
pub async fn run(config: EdgeConfig) -> Result<(), Box<dyn std::error::Error>> {
let listen_addr = config.listen_addr;
let request_timeout = Duration::from_secs(config.request_timeout_secs);
let max_body_bytes = config.max_body_bytes;
let wire_listen_addr = config.wire_listen_addr;
let brain_addr = config.brain_addr;
let wire_rate = RateLimitConfig {
capacity: config.wire_rate_capacity,
refill_per_sec: config.wire_rate_refill_per_sec,
};
let keyring = crypto::keyring_from_env().map_err(|e| format!("llm kek: {e}"))?;
let state = EdgeState::new(config);
let state = if let Some(ring) = keyring {
{
tracing::info!(
kek_id = ring.current_kek_id(),
"BYOK enabled: provider keys sealed at the edge",
);
state.with_llm_credentials(byok_store()?, std::sync::Arc::new(ring))
}
} else {
tracing::info!(
"BYOK disabled: set {} to enable tenant provider keys",
crypto::KEK_SPEC_ENV,
);
state
};
if let Some(wire_addr) = wire_listen_addr {
let meter = state.meter();
let wcfg = WireProxyConfig {
wire_listen_addr: wire_addr,
brain_addr,
rate: wire_rate,
};
tokio::spawn(async move {
if let Err(e) = wire_proxy::serve(wcfg, meter).await {
error!(error = %e, "wire proxy listener exited");
}
});
}
{
let sweeper_state = state.clone();
tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(60));
ticker.tick().await; loop {
ticker.tick().await;
sweeper_state.pool().sweep_idle();
}
});
}
let health = axum::Router::new()
.route("/health/live", get(|| async { StatusCode::OK }))
.route("/health/ready", get(readiness))
.with_state(state.clone());
let plane = app::Plane::from_env().map_err(|e| format!("{}: {e}", app::PLANE_ENV))?;
tracing::info!(?plane, "serving plane");
let app = harden(
app::router_for(state, plane).merge(health),
request_timeout,
max_body_bytes,
);
let listener = tokio::net::TcpListener::bind(listen_addr).await?;
info!(addr = %listen_addr, "listening");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
info!("shutdown complete");
Ok(())
}
async fn readiness(State(state): State<EdgeState>) -> StatusCode {
if state.pool().probe(Duration::from_secs(2)).await {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
}
}
async fn shutdown_signal() {
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let terminate = async {
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut sig) => {
sig.recv().await;
}
Err(_) => std::future::pending::<()>().await,
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => info!("SIGINT received, shutting down"),
() = terminate => info!("SIGTERM received, shutting down"),
}
}
pub const CREDENTIAL_STORE_ENV: &str = "BRAIN_EDGE_LLM_CREDENTIAL_FILE";
fn byok_store() -> Result<std::sync::Arc<dyn port::LlmCredentialStore>, Box<dyn std::error::Error>>
{
match std::env::var(CREDENTIAL_STORE_ENV) {
Ok(path) if !path.trim().is_empty() => {
let store = port::FileLlmCredentialStore::open(path.trim())
.map_err(|e| format!("{CREDENTIAL_STORE_ENV}: {e}"))?;
tracing::info!(path = %path.trim(), "BYOK credential store: file-backed");
Ok(std::sync::Arc::new(store))
}
_ => {
tracing::warn!(
"BYOK credential store is IN-MEMORY: registered provider keys are lost on \
restart, and writes then fail until every tenant re-registers. Set {} to a \
file path, or inject a durable LlmCredentialStore.",
CREDENTIAL_STORE_ENV,
);
Ok(std::sync::Arc::new(
port::InMemoryLlmCredentialStore::default(),
))
}
}
}