use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use bitrouter_sdk::App;
use bitrouter_sdk::caller::CallerContext;
use bitrouter_sdk::language_model::RoutingPrefs;
use chrono::{DateTime, Utc};
use crate::acp_runtime::AcpRuntime;
use crate::actions::administration::{
Administration, AgentsReport, ObserveReport, PolicyInput, PolicyReport, ProvidersReport,
};
use crate::metering::{MeteringStore, TimeWindow};
use crate::reload::{ReloadAdmissionError, ReloadReport, ReloadReservation, ReloadState};
#[async_trait::async_trait]
pub trait DaemonReloader: Send + Sync {
async fn reload(&self) -> anyhow::Result<()>;
async fn reload_with_env(&self, env: Vec<(String, String)>) -> anyhow::Result<()> {
if env.is_empty() {
self.reload().await
} else {
Err(anyhow::anyhow!(
"this daemon reloader does not support coordinated environment overrides"
))
}
}
fn reload_state(&self) -> Option<ReloadState> {
None
}
async fn configuration_state(&self) -> Option<crate::reload::ConfigurationState> {
None
}
fn reserve_remote(
&self,
_expected_instance: &str,
_expected_generation: u64,
) -> Result<ReloadReservation, ReloadAdmissionError> {
Err(ReloadAdmissionError::Unsupported)
}
async fn reload_reserved(&self, _reservation: ReloadReservation) -> ReloadReport {
ReloadReport::unsupported()
}
}
pub struct NoopReloader;
#[async_trait::async_trait]
impl DaemonReloader for NoopReloader {
async fn reload(&self) -> anyhow::Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum DaemonCommand {
Stop,
Reload {
#[serde(default)]
env: Vec<(String, String)>,
},
ReloadState,
Status,
Models,
Route {
model: String,
},
ObserveStatus,
Inspect { inspection: DaemonInspection },
Evolution {
operation: crate::evolution::operator::EvolutionOperation,
},
AcpRecordingRegister {
connection_id: String,
api_principal: String,
controller_instance_id: String,
},
AcpControllerCleanup {
api_principal: String,
controller_instance_id: String,
},
AcpRouteList {
api_principal: String,
controller_instance_id: String,
session_id: String,
},
AcpRouteSet {
api_principal: String,
controller_instance_id: String,
session_id: String,
route: String,
},
AcpSessionSpend {
api_principal: String,
controller_instance_id: String,
session_id: String,
},
AcpRouteReset {
api_principal: String,
controller_instance_id: String,
session_id: String,
},
Sessions {
request: crate::supervisor::SessionRequest,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "inspection", rename_all = "snake_case")]
pub enum DaemonInspection {
Providers,
Agents,
Observe,
Policy {
input: PolicyInput,
},
Checks,
ChecksProbe {
checker: String,
},
CheckReceipts {
limit: usize,
},
CheckReceipt {
request_id: String,
incarnation: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "report", rename_all = "snake_case")]
pub enum DaemonInspectionReport {
Providers(ProvidersReport),
Agents(AgentsReport),
Observe(ObserveReport),
Policy(PolicyReport),
Checks(crate::actions::checks::ChecksReport),
ChecksProbe(crate::actions::checks::CheckerProbeReport),
CheckReceipts(bitrouter_sdk::language_model::receipts::RequestReceiptList),
CheckReceipt(bitrouter_sdk::language_model::receipts::RequestReceiptLookup),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteHop {
pub provider: String,
pub service_id: String,
pub api_protocol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "resp", rename_all = "snake_case")]
pub enum DaemonResponse {
Ok,
Status {
pid: u32,
listen: String,
models: usize,
#[serde(default)]
providers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
saved_routers: Option<Vec<crate::actions::models::RouterStatus>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
running_routers: Option<Vec<crate::actions::models::RouterStatus>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
router_restart_required: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
config_state: Option<crate::reload::ConfigurationState>,
},
Models {
models: Vec<bitrouter_sdk::language_model::routing::ModelInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
routers: Option<Vec<crate::actions::models::RouterStatus>>,
},
Route {
chain: Vec<RouteHop>,
#[serde(default, skip_serializing_if = "Option::is_none")]
resolved_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
router: Option<bitrouter_sdk::language_model::routing::RouterRequestIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
router_source: Option<crate::actions::models::RouterSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bound_policy: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
candidate_models: Vec<String>,
},
ObserveStatus {
payload: ObserveStatusPayload,
},
ReloadState { state: ReloadState },
Inspection { report: DaemonInspectionReport },
Evolution {
report: Box<crate::evolution::operator::EvolutionReport>,
},
AcpRecordingRegistered {
registration: crate::evolution::inventory::CoverageRegistration,
},
AcpRouteState {
available: Vec<String>,
current: Option<String>,
scope: String,
},
AcpSessionSpend {
spend_micro_usd: u64,
requests: u64,
unpriced: u64,
},
Sessions {
response: crate::supervisor::SessionResponse,
},
Error {
message: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObserveStatusPayload {
pub compiled_in: bool,
pub exporter_wired: bool,
pub endpoint: Option<String>,
pub header_count: usize,
pub service_name: Option<String>,
pub resource_attribute_count: usize,
pub sampler: Option<String>,
pub sampler_arg: Option<f64>,
pub metrics_enabled: bool,
pub api_key_count: usize,
pub api_key_cap: usize,
pub user_id_count: usize,
pub user_id_cap: usize,
pub active_spans: usize,
}
impl ObserveStatusPayload {
pub fn unwired(compiled_in: bool) -> Self {
Self {
compiled_in,
exporter_wired: false,
endpoint: None,
header_count: 0,
service_name: None,
resource_attribute_count: 0,
sampler: None,
sampler_arg: None,
metrics_enabled: false,
api_key_count: 0,
api_key_cap: 0,
user_id_count: 0,
user_id_cap: 0,
active_spans: 0,
}
}
}
#[async_trait::async_trait]
pub trait ObserveStatusProvider: Send + Sync {
fn status(&self) -> ObserveStatusPayload;
async fn shutdown(&self) {}
}
pub struct NoopObserveStatus {
pub compiled_in: bool,
}
#[async_trait::async_trait]
impl ObserveStatusProvider for NoopObserveStatus {
fn status(&self) -> ObserveStatusPayload {
ObserveStatusPayload::unwired(self.compiled_in)
}
}
pub const DEFAULT_CONTROL_SOCKET: &str = "./bitrouter.sock";
pub fn resolve_socket_path(config_path: &Path, cfg_socket: &str) -> PathBuf {
let raw = Path::new(cfg_socket);
if raw.is_absolute() {
return raw.to_path_buf();
}
let stripped = raw.strip_prefix("./").unwrap_or(raw);
let parent = config_path.parent().filter(|p| !p.as_os_str().is_empty());
match parent {
Some(dir) => dir.join(stripped),
None => stripped.to_path_buf(),
}
}
pub fn is_not_reachable(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.map(|io| {
matches!(
io.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
)
})
.unwrap_or(false)
})
}
#[derive(Debug, Clone)]
pub struct ReadyInfo {
pub pid: u32,
pub listen: String,
pub models: usize,
}
pub fn socket_path_for(
source: &crate::paths::ConfigSource,
cfg: &bitrouter_sdk::config::Config,
) -> PathBuf {
match source {
crate::paths::ConfigSource::File(path) => {
resolve_socket_path(path, &cfg.server.control_socket)
}
crate::paths::ConfigSource::Default { home } => home.join("bitrouter.sock"),
}
}
pub async fn probe_status(socket: &Path) -> Result<Option<ReadyInfo>> {
match send_command(socket, &DaemonCommand::Status).await {
Ok(DaemonResponse::Status {
pid,
listen,
models,
..
}) => Ok(Some(ReadyInfo {
pid,
listen,
models,
})),
Ok(DaemonResponse::Error { message }) => Err(anyhow::anyhow!(message)),
Ok(other) => Err(anyhow::anyhow!("unexpected response: {other:?}")),
Err(e) if is_not_reachable(&e) => Ok(None),
Err(e) => Err(e),
}
}
pub async fn run_control_socket(
socket_path: PathBuf,
app: Arc<App>,
listen: String,
reloader: Arc<dyn DaemonReloader>,
observe: Arc<dyn ObserveStatusProvider>,
metering: MeteringStore,
) -> Result<()> {
run_control_socket_with_acp_runtime(
socket_path,
app,
listen,
reloader,
observe,
AcpControlPlane {
runtime: Arc::new(AcpRuntime::new()),
metering,
inventory: None,
evolution: None,
},
)
.await
}
#[derive(Clone)]
pub struct AcpControlPlane {
pub runtime: Arc<AcpRuntime>,
pub metering: MeteringStore,
pub inventory: Option<crate::evolution::inventory::GatewayInventory>,
pub evolution: Option<crate::evolution::runtime::EvolutionRuntime>,
}
pub async fn run_control_socket_with_acp_runtime(
socket_path: PathBuf,
app: Arc<App>,
listen: String,
reloader: Arc<dyn DaemonReloader>,
observe: Arc<dyn ObserveStatusProvider>,
acp: AcpControlPlane,
) -> Result<()> {
run_control_socket_with_acp_runtime_and_administration(
socket_path,
app,
listen,
reloader,
observe,
acp,
None,
)
.await
}
pub async fn run_control_socket_with_acp_runtime_and_administration(
socket_path: PathBuf,
app: Arc<App>,
listen: String,
reloader: Arc<dyn DaemonReloader>,
observe: Arc<dyn ObserveStatusProvider>,
acp: AcpControlPlane,
administration: Option<Administration>,
) -> Result<()> {
let supervisor = match &administration {
Some(administration) => Some(
crate::supervisor::Supervisor::open(
administration.source.clone(),
administration.routing.clone(),
)
.await?,
),
None => None,
};
let services = LocalControlServices {
administration,
supervisor,
};
let mut listener = transport::bind(&socket_path).await?;
let result = accept_loop(
&mut listener,
&app,
&listen,
&reloader,
&observe,
&acp,
&services,
)
.await;
if let Some(supervisor) = &services.supervisor {
supervisor.shutdown().await;
}
listener.cleanup().await;
result
}
#[derive(Clone)]
struct LocalControlServices {
administration: Option<Administration>,
supervisor: Option<crate::supervisor::Supervisor>,
}
async fn accept_loop(
listener: &mut transport::ControlListener,
app: &Arc<App>,
listen: &str,
reloader: &Arc<dyn DaemonReloader>,
observe: &Arc<dyn ObserveStatusProvider>,
acp: &AcpControlPlane,
services: &LocalControlServices,
) -> Result<()> {
let (stop_tx, mut stop_rx) = tokio::sync::mpsc::unbounded_channel();
loop {
tokio::select! {
accepted = listener.accept() => {
let stream = accepted?;
let app = app.clone();
let listen = listen.to_string();
let reloader = reloader.clone();
let observe = observe.clone();
let acp = acp.clone();
let services = services.clone();
let stop_tx = stop_tx.clone();
tokio::spawn(async move {
match handle_connection(
stream,
&app,
&listen,
&reloader,
&observe,
&acp,
&services,
)
.await
{
Ok(true) => {
let _ = stop_tx.send(());
}
Ok(false) => {}
Err(error) => tracing::warn!(%error, "local control connection failed"),
}
});
}
stopped = stop_rx.recv() => {
if stopped.is_some() {
tracing::info!("stop command received — shutting down");
}
return Ok(());
}
}
}
}
async fn handle_connection<S>(
stream: S,
app: &Arc<App>,
listen: &str,
reloader: &Arc<dyn DaemonReloader>,
observe: &Arc<dyn ObserveStatusProvider>,
acp: &AcpControlPlane,
services: &LocalControlServices,
) -> Result<bool>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut reader = BufReader::new(stream);
let mut line = String::new();
if reader.read_line(&mut line).await? == 0 {
return Ok(false); }
let command: DaemonCommand = match serde_json::from_str(line.trim()) {
Ok(c) => c,
Err(e) => {
write_response(
reader.get_mut(),
&DaemonResponse::Error {
message: format!("invalid command: {e}"),
},
)
.await?;
return Ok(false);
}
};
let is_stop = matches!(command, DaemonCommand::Stop);
let response = dispatch(command, app, listen, reloader, observe, acp, services).await;
write_response(reader.get_mut(), &response).await?;
Ok(is_stop)
}
async fn dispatch(
command: DaemonCommand,
app: &Arc<App>,
listen: &str,
reloader: &Arc<dyn DaemonReloader>,
observe: &Arc<dyn ObserveStatusProvider>,
acp: &AcpControlPlane,
services: &LocalControlServices,
) -> DaemonResponse {
match command {
DaemonCommand::Stop => DaemonResponse::Ok,
DaemonCommand::Evolution { operation } => {
let Some(runtime) = &acp.evolution else {
return DaemonResponse::Error {
message: "checkpoint evolution is unavailable on this daemon".into(),
};
};
match runtime.operate("local", operation).await {
Ok(report) => DaemonResponse::Evolution {
report: Box::new(report),
},
Err(error) => DaemonResponse::Error {
message: error.to_string(),
},
}
}
DaemonCommand::AcpRecordingRegister {
connection_id,
api_principal,
controller_instance_id,
} => {
let Some(inventory) = &acp.inventory else {
return DaemonResponse::Error {
message: "gateway request inventory is unavailable on this daemon".into(),
};
};
match inventory
.register_capture(&connection_id, &api_principal, &controller_instance_id)
.await
{
Ok(registration) => DaemonResponse::AcpRecordingRegistered { registration },
Err(error) => DaemonResponse::Error {
message: error.to_string(),
},
}
}
DaemonCommand::Reload { env } => match reloader.reload_with_env(env).await {
Ok(()) => {
tracing::info!("reload succeeded");
DaemonResponse::Ok
}
Err(e) => DaemonResponse::Error {
message: format!("reload failed: {e}"),
},
},
DaemonCommand::ReloadState => match reloader.reload_state() {
Some(state) => DaemonResponse::ReloadState { state },
None => DaemonResponse::Error {
message: "reload state is unavailable on this daemon".to_string(),
},
},
DaemonCommand::Status => {
let routable = app
.language_model()
.map(|p| p.routing_table().list_models())
.unwrap_or_default();
let providers: Vec<String> = routable
.iter()
.flat_map(|m| m.providers.iter().cloned())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let router_state = router_state(&services.administration).await;
let config_state = reloader.configuration_state().await;
DaemonResponse::Status {
pid: std::process::id(),
listen: listen.to_string(),
models: routable.len(),
providers,
saved_routers: router_state.as_ref().and_then(|state| state.saved.clone()),
running_routers: router_state
.as_ref()
.and_then(|state| state.running.clone()),
router_restart_required: router_state.and_then(|state| state.restart_required),
config_state,
}
}
DaemonCommand::Models => {
let routers = router_state(&services.administration)
.await
.and_then(|state| state.running);
DaemonResponse::Models {
models: app
.language_model()
.map(|p| p.routing_table().list_models())
.unwrap_or_default(),
routers,
}
}
DaemonCommand::AcpSessionSpend {
api_principal,
controller_instance_id,
session_id,
} => {
if api_principal.trim().is_empty()
|| controller_instance_id.trim().is_empty()
|| session_id.trim().is_empty()
{
return DaemonResponse::Error {
message: "principal, controller and session must not be empty".to_string(),
};
}
let window = TimeWindow::Custom {
start: DateTime::<Utc>::UNIX_EPOCH,
end: Utc::now(),
};
match acp
.metering
.spend_summary_for_acp_session(
&api_principal,
&controller_instance_id,
&session_id,
window,
)
.await
{
Ok(summary) => DaemonResponse::AcpSessionSpend {
spend_micro_usd: summary.spend_micro_usd,
requests: summary.requests,
unpriced: summary.unpriced,
},
Err(error) => DaemonResponse::Error {
message: format!("session spend is unavailable: {error}"),
},
}
}
DaemonCommand::AcpControllerCleanup {
api_principal,
controller_instance_id,
} => {
acp.runtime
.remove_controller(&api_principal, &controller_instance_id);
DaemonResponse::Ok
}
DaemonCommand::AcpRouteList {
api_principal,
controller_instance_id,
session_id,
} => {
if api_principal.trim().is_empty()
|| controller_instance_id.trim().is_empty()
|| session_id.trim().is_empty()
{
DaemonResponse::Error {
message: "API principal, controller id, and session id must not be empty"
.to_string(),
}
} else {
let current = acp
.runtime
.current_route(&api_principal, &controller_instance_id, &session_id)
.map(|lease| lease.route().to_string());
DaemonResponse::AcpRouteState {
available: route_suggestions(app),
scope: if current.is_some() {
"session"
} else {
"default"
}
.to_string(),
current,
}
}
}
DaemonCommand::AcpRouteSet {
api_principal,
controller_instance_id,
session_id,
route,
} => {
let Some(pipeline) = app.language_model() else {
return DaemonResponse::Error {
message: "no language_model pipeline configured".to_string(),
};
};
if api_principal.trim().is_empty()
|| controller_instance_id.trim().is_empty()
|| session_id.trim().is_empty()
|| route.trim().is_empty()
{
return DaemonResponse::Error {
message:
"API principal, controller id, session id, and route must not be empty"
.to_string(),
};
}
if let Err(error) = pipeline
.routing_table()
.route_chain(&route, &RoutingPrefs::default(), &CallerContext::local())
.await
{
return DaemonResponse::Error {
message: format!("route is not available: {error}"),
};
}
match acp.runtime.set_route(
&api_principal,
&controller_instance_id,
&session_id,
&route,
) {
Ok(lease) => DaemonResponse::AcpRouteState {
available: route_suggestions(app),
current: Some(lease.route().to_string()),
scope: "session".to_string(),
},
Err(message) => DaemonResponse::Error { message },
}
}
DaemonCommand::AcpRouteReset {
api_principal,
controller_instance_id,
session_id,
} => {
if api_principal.trim().is_empty()
|| controller_instance_id.trim().is_empty()
|| session_id.trim().is_empty()
{
DaemonResponse::Error {
message: "API principal, controller id, and session id must not be empty"
.to_string(),
}
} else {
acp.runtime
.reset_route(&api_principal, &controller_instance_id, &session_id);
DaemonResponse::AcpRouteState {
available: route_suggestions(app),
current: None,
scope: "default".to_string(),
}
}
}
DaemonCommand::Route { model } => {
let Some(pipeline) = app.language_model() else {
return DaemonResponse::Error {
message: "no language_model pipeline configured".to_string(),
};
};
let table = pipeline.routing_table();
let resolution = match table.resolve_model(&model).await {
Ok(resolution) => resolution,
Err(error) => {
return DaemonResponse::Error {
message: error.to_string(),
};
}
};
let router_source = services.administration.as_ref().and_then(|administration| {
resolution.router.as_ref().and_then(|identity| {
crate::actions::route::router_source(
&administration.routing.snapshot_config(),
&identity.router_id,
)
})
});
let policy_report = services
.administration
.as_ref()
.map(|administration| administration.policy.administration_snapshot());
let candidate_models = crate::actions::route::policy_candidate_models(
policy_report.as_ref(),
resolution.policy.as_deref(),
&resolution.clean_model,
);
let candidate_models = crate::actions::route::resolvable_policy_candidates(
table.as_ref(),
candidate_models,
&resolution.prefs,
)
.await;
let chain = if resolution.policy.is_some() {
Ok(Vec::new())
} else {
table
.route_resolved(
&resolution.clean_model,
&resolution.prefs,
&CallerContext::local(),
)
.await
};
match chain {
Ok(chain) => DaemonResponse::Route {
chain: chain
.into_iter()
.map(|t| RouteHop {
provider: t.provider_name,
service_id: t.service_id,
api_protocol: format!("{:?}", t.api_protocol).to_lowercase(),
})
.collect(),
resolved_model: Some(resolution.clean_model),
router: resolution.router,
router_source,
bound_policy: resolution.policy,
candidate_models,
},
Err(error) => DaemonResponse::Error {
message: error.to_string(),
},
}
}
DaemonCommand::ObserveStatus => DaemonResponse::ObserveStatus {
payload: observe.status(),
},
DaemonCommand::Inspect { inspection } => {
inspection_response(inspection, &services.administration, reloader).await
}
DaemonCommand::Sessions { request } => {
let Some(supervisor) = &services.supervisor else {
return DaemonResponse::Error {
message: "supervised sessions are unavailable on this daemon".to_string(),
};
};
match supervisor.dispatch(request).await {
Ok(response) => DaemonResponse::Sessions { response },
Err(error) => DaemonResponse::Error {
message: format!("supervised session request failed: {error:#}"),
},
}
}
}
}
struct RouterState {
saved: Option<Vec<crate::actions::models::RouterStatus>>,
running: Option<Vec<crate::actions::models::RouterStatus>>,
restart_required: Option<bool>,
}
fn router_configuration_restart_required(
saved: &bitrouter_sdk::config::Config,
running: &bitrouter_sdk::config::Config,
) -> bool {
saved.routers != running.routers
}
async fn router_state(administration: &Option<Administration>) -> Option<RouterState> {
let administration = administration.as_ref()?;
let running = administration.routing.snapshot_config();
let policy_report = administration.policy.administration_snapshot();
let running_statuses = crate::actions::models::router_statuses(
&running,
crate::actions::models::PolicyReadiness::Known(&policy_report),
)
.await
.ok();
let saved = crate::reload::inspect_configuration_baseline(&administration.source)
.await
.ok();
let saved_statuses = match &saved {
Some(saved) => {
crate::actions::models::disk_router_statuses_for_config(
saved.config(),
&administration.source,
)
.await
}
None => None,
};
let restart_required = saved
.as_ref()
.map(|saved| router_configuration_restart_required(saved.config(), &running));
Some(RouterState {
saved: saved_statuses,
running: running_statuses,
restart_required,
})
}
async fn inspection_response(
inspection: DaemonInspection,
administration: &Option<Administration>,
reloader: &Arc<dyn DaemonReloader>,
) -> DaemonResponse {
let Some(administration) = administration else {
return DaemonResponse::Error {
message: "live administration reads are unavailable on this daemon".to_string(),
};
};
match inspection {
DaemonInspection::Providers => DaemonResponse::Inspection {
report: DaemonInspectionReport::Providers(administration.providers()),
},
DaemonInspection::Agents => DaemonResponse::Inspection {
report: DaemonInspectionReport::Agents(administration.agents()),
},
DaemonInspection::Observe => DaemonResponse::Inspection {
report: DaemonInspectionReport::Observe(administration.observe()),
},
DaemonInspection::Policy { input } => match administration.policy(input).await {
Ok(report) => DaemonResponse::Inspection {
report: DaemonInspectionReport::Policy(report),
},
Err(error) => DaemonResponse::Error {
message: format!("policy inspection failed: {error}"),
},
},
DaemonInspection::Checks => {
match administration.checks(reloader.configuration_state().await) {
Ok(report) => DaemonResponse::Inspection {
report: DaemonInspectionReport::Checks(report),
},
Err(error) => DaemonResponse::Error {
message: format!("request-check inspection failed: {error}"),
},
}
}
DaemonInspection::ChecksProbe { checker } => {
match administration.checks_probe(&checker).await {
Ok(report) => DaemonResponse::Inspection {
report: DaemonInspectionReport::ChecksProbe(report),
},
Err(error) => DaemonResponse::Error {
message: format!("request-check probe failed: {error}"),
},
}
}
DaemonInspection::CheckReceipts { limit } => match administration.check_receipts(limit) {
Ok(report) => DaemonResponse::Inspection {
report: DaemonInspectionReport::CheckReceipts(report),
},
Err(error) => DaemonResponse::Error {
message: format!("request-check receipt listing failed: {error}"),
},
},
DaemonInspection::CheckReceipt {
request_id,
incarnation,
} => match administration.check_receipt(&request_id, incarnation.as_deref()) {
Ok(report) => DaemonResponse::Inspection {
report: DaemonInspectionReport::CheckReceipt(report),
},
Err(error) => DaemonResponse::Error {
message: format!("request-check receipt lookup failed: {error}"),
},
},
}
}
fn route_suggestions(app: &Arc<App>) -> Vec<String> {
app.language_model()
.map(|pipeline| {
pipeline
.routing_table()
.list_models()
.into_iter()
.map(|model| model.id)
.collect()
})
.unwrap_or_default()
}
async fn write_response<S>(stream: &mut S, response: &DaemonResponse) -> Result<()>
where
S: AsyncWrite + Unpin,
{
let mut json = serde_json::to_string(response).context("serialising daemon response")?;
json.push('\n');
stream.write_all(json.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
pub async fn connect_control(socket_path: &Path) -> Result<impl AsyncRead + AsyncWrite + Unpin> {
transport::connect(socket_path).await
}
pub fn endpoint_in_use(path: &Path) -> bool {
transport::endpoint_in_use(path)
}
pub async fn send_command(socket_path: &Path, command: &DaemonCommand) -> Result<DaemonResponse> {
let stream = transport::connect(socket_path).await?;
let mut reader = BufReader::new(stream);
let mut json = serde_json::to_string(command).context("serialising command")?;
json.push('\n');
reader.get_mut().write_all(json.as_bytes()).await?;
reader.get_mut().flush().await?;
let mut line = String::new();
reader
.read_line(&mut line)
.await
.context("reading daemon response")?;
if line.trim().is_empty() {
anyhow::bail!("daemon closed the connection without responding");
}
serde_json::from_str(line.trim()).context("parsing daemon response")
}
#[cfg(unix)]
mod transport {
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use tokio::net::{UnixListener, UnixStream};
pub struct ControlListener {
listener: UnixListener,
path: PathBuf,
}
pub async fn bind(path: &Path) -> Result<ControlListener> {
let _ = tokio::fs::remove_file(path).await;
let listener = UnixListener::bind(path)
.with_context(|| format!("binding control socket {}", path.display()))?;
use std::os::unix::fs::PermissionsExt;
tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.await
.with_context(|| format!("chmod 0600 {}", path.display()))?;
tracing::info!(socket = %path.display(), "control socket listening (mode 0600)");
Ok(ControlListener {
listener,
path: path.to_path_buf(),
})
}
impl ControlListener {
pub async fn accept(&mut self) -> Result<UnixStream> {
let (stream, _addr) = self
.listener
.accept()
.await
.context("accepting control-socket connection")?;
Ok(stream)
}
pub async fn cleanup(self) {
let _ = tokio::fs::remove_file(&self.path).await;
}
}
pub async fn connect(path: &Path) -> Result<UnixStream> {
UnixStream::connect(path).await.with_context(|| {
format!(
"connecting to {} — is the daemon running? (`{cli} start`)",
path.display(),
cli = bitrouter_sdk::invocation::name()
)
})
}
pub fn endpoint_in_use(path: &Path) -> bool {
path.exists()
}
}
#[cfg(windows)]
mod transport {
use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result};
use tokio::net::windows::named_pipe::{
ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
};
const ERROR_PIPE_BUSY: i32 = 231;
pub fn pipe_name(path: &Path) -> String {
let raw = path.to_string_lossy();
let lower = raw.to_ascii_lowercase();
if lower.starts_with(r"\\.\pipe\") || lower.starts_with(r"\\?\pipe\") {
return raw.into_owned();
}
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(lower.as_bytes());
let digest = hasher.finalize();
format!(r"\\.\pipe\bitrouter-{}", hex::encode(&digest[..16]))
}
pub struct ControlListener {
name: String,
server: NamedPipeServer,
}
pub async fn bind(path: &Path) -> Result<ControlListener> {
let name = pipe_name(path);
let server = ServerOptions::new()
.first_pipe_instance(true)
.create(&name)
.with_context(|| format!("creating control pipe {name}"))?;
tracing::info!(pipe = %name, "control pipe listening");
Ok(ControlListener { name, server })
}
impl ControlListener {
pub async fn accept(&mut self) -> Result<NamedPipeServer> {
self.server
.connect()
.await
.context("waiting for control-pipe client")?;
let next = ServerOptions::new()
.create(&self.name)
.with_context(|| format!("re-arming control pipe {}", self.name))?;
Ok(std::mem::replace(&mut self.server, next))
}
pub async fn cleanup(self) {}
}
pub async fn connect(path: &Path) -> Result<NamedPipeClient> {
let name = pipe_name(path);
let mut last_busy: Option<std::io::Error> = None;
for _ in 0..50 {
match ClientOptions::new().open(&name) {
Ok(client) => return Ok(client),
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY) => {
last_busy = Some(e);
tokio::time::sleep(Duration::from_millis(20)).await;
}
Err(e) => {
return Err(e).with_context(|| {
format!(
"connecting to {name} — is the daemon running? (`{cli} start`)",
cli = bitrouter_sdk::invocation::name()
)
});
}
}
}
let e = last_busy.unwrap_or_else(|| {
std::io::Error::new(std::io::ErrorKind::TimedOut, "control pipe busy")
});
Err(e).with_context(|| format!("connecting to {name} — daemon stayed busy"))
}
pub fn endpoint_in_use(path: &Path) -> bool {
let name = pipe_name(path);
match ClientOptions::new().open(&name) {
Ok(_client) => true,
Err(e) => e.raw_os_error() == Some(ERROR_PIPE_BUSY),
}
}
#[cfg(test)]
mod tests {
use super::pipe_name;
use std::path::PathBuf;
#[test]
fn explicit_pipe_path_is_returned_verbatim() {
let p = PathBuf::from(r"\\.\pipe\bitrouter-custom");
assert_eq!(pipe_name(&p), r"\\.\pipe\bitrouter-custom");
}
#[test]
fn nt_style_pipe_path_is_returned_verbatim() {
let p = PathBuf::from(r"\\?\pipe\bitrouter-custom");
assert_eq!(pipe_name(&p), r"\\?\pipe\bitrouter-custom");
}
#[test]
fn filesystem_path_maps_to_deterministic_pipe_name() {
let a = pipe_name(&PathBuf::from(r"C:\Users\alice\.bitrouter\bitrouter.sock"));
let b = pipe_name(&PathBuf::from(r"C:\Users\alice\.bitrouter\bitrouter.sock"));
assert_eq!(a, b);
assert!(
a.starts_with(r"\\.\pipe\bitrouter-"),
"unexpected pipe name {a}"
);
}
#[test]
fn case_differences_collapse_to_one_name() {
let lower = pipe_name(&PathBuf::from(r"c:\users\alice\.bitrouter\bitrouter.sock"));
let upper = pipe_name(&PathBuf::from(r"C:\Users\Alice\.BitRouter\bitrouter.sock"));
assert_eq!(lower, upper);
}
#[test]
fn different_paths_map_to_different_pipe_names() {
let a = pipe_name(&PathBuf::from(r"C:\Users\alice\.bitrouter\bitrouter.sock"));
let b = pipe_name(&PathBuf::from(r"C:\Users\bob\.bitrouter\bitrouter.sock"));
assert_ne!(a, b);
}
}
}
pub async fn write_pid_file(path: &Path) -> Result<()> {
tokio::fs::write(path, std::process::id().to_string())
.await
.with_context(|| format!("writing pid file {}", path.display()))
}
pub async fn read_pid_file(path: &Path) -> Option<u32> {
let raw = tokio::fs::read_to_string(path).await.ok()?;
raw.trim().parse().ok()
}
pub async fn remove_pid_file(path: &Path) {
let _ = tokio::fs::remove_file(path).await;
}
pub const DAEMON_READY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const READY_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
#[derive(Debug)]
pub enum DaemonStartOutcome {
Ready(ReadyInfo),
NotReadyInTime {
pid: u32,
},
Exited {
status: String,
log_tail: String,
},
}
fn spawn_detached_serve(
source: &crate::paths::ConfigSource,
log_path: &Path,
) -> Result<(std::process::Child, u64)> {
let exe = std::env::current_exe().context("locating current bitrouter binary")?;
let log_size_before = std::fs::metadata(log_path).map(|m| m.len()).unwrap_or(0);
let log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.with_context(|| format!("opening daemon log {}", log_path.display()))?;
let log_err = log
.try_clone()
.context("duplicating log handle for stderr")?;
let mut cmd = std::process::Command::new(&exe);
cmd.arg("serve");
if let crate::paths::ConfigSource::File(path) = source {
cmd.arg("--config").arg(path);
}
cmd.stdout(std::process::Stdio::from(log))
.stderr(std::process::Stdio::from(log_err))
.stdin(std::process::Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
}
let child = cmd.spawn().with_context(|| {
format!(
"spawning detached `{} serve`",
bitrouter_sdk::invocation::name()
)
})?;
Ok((child, log_size_before))
}
async fn read_log_tail(path: &Path, offset: u64) -> String {
let bytes = match tokio::fs::read(path).await {
Ok(b) => b,
Err(_) => return String::new(),
};
let start = (offset as usize).min(bytes.len());
String::from_utf8_lossy(&bytes[start..]).into_owned()
}
pub fn eprint_failure_log(log_path: &Path, content: &str) {
let trimmed = content.trim_end();
if trimmed.is_empty() {
return;
}
let p = crate::style::Palette::for_stderr();
eprintln!(
"{dim}daemon log ({path}):{reset}",
dim = p.dim,
path = log_path.display(),
reset = p.reset,
);
for line in trimmed.lines() {
eprintln!(" {line}");
}
eprintln!();
}
pub async fn start_and_wait(
source: &crate::paths::ConfigSource,
log_path: &Path,
socket: Option<&Path>,
timeout: std::time::Duration,
) -> Result<DaemonStartOutcome> {
crate::paths::ensure_home_directory(source.home())?;
let (mut child, log_size_before) = spawn_detached_serve(source, log_path)?;
let deadline = std::time::Instant::now() + timeout;
loop {
if let Ok(Some(status)) = child.try_wait() {
let log_tail = read_log_tail(log_path, log_size_before).await;
return Ok(DaemonStartOutcome::Exited {
status: status.to_string(),
log_tail,
});
}
if let Some(socket) = socket {
match probe_status(socket).await {
Ok(Some(info)) => return Ok(DaemonStartOutcome::Ready(info)),
Ok(None) => {}
Err(e) => tracing::debug!(error = %e, "readiness probe failed; retrying"),
}
}
if std::time::Instant::now() >= deadline {
return Ok(DaemonStartOutcome::NotReadyInTime { pid: child.id() });
}
tokio::time::sleep(READY_POLL_INTERVAL).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
struct InvalidSavedConfigReloader;
#[async_trait::async_trait]
impl DaemonReloader for InvalidSavedConfigReloader {
async fn reload(&self) -> anyhow::Result<()> {
Ok(())
}
async fn configuration_state(&self) -> Option<crate::reload::ConfigurationState> {
Some(crate::reload::ConfigurationState {
server_instance_id: Some("test-incarnation".into()),
generation: Some(0),
source: crate::reload::ConfigSourceKind::File,
saved: crate::reload::SavedConfigState::Invalid,
running: crate::reload::RunningConfigState::Unknown,
reload_required_fields: Vec::new(),
restart_required_fields: vec!["checkers".into()],
named_policy: crate::reload::AuxiliaryConfigState::NotConfigured,
access_policies: crate::reload::AuxiliaryConfigState::NotConfigured,
last_reload: None,
mixed_state_history: Vec::new(),
})
}
}
async fn checker_administration() -> anyhow::Result<Administration> {
let config = bitrouter_sdk::config::parse(
"inherit_defaults: false\ncheckers:\n company:\n endpoint: http://127.0.0.1:9/check\n contract_version: 1\n credential_env: BITROUTER_TEST_MISSING_CHECKER_PARITY_TOKEN\n",
)?;
let db = crate::db::connect("sqlite::memory:").await?;
crate::db::run_migrations(&db).await?;
let policy = crate::policy_lock::PolicyRuntime::new(
&config,
None,
db,
None,
crate::eval::settlement::PendingEvalDecisionStore::default(),
None,
)
.await?;
let request_checks = Arc::new(crate::request_checks::RequestCheckRuntime::activate(
&config,
)?);
let receipts = request_checks.receipts();
let binding = bitrouter_sdk::language_model::request_checks::RequestCheckBinding {
checker_id: "company".into(),
binding_digest: "sha256:checker-binding".into(),
max_input_bytes: 1024,
timeout_ms: 500,
};
let handle = receipts.admit(
"request-1",
&bitrouter_sdk::language_model::routing::RouterRequestIdentity {
router_id: "coding".into(),
original_selector: "bitrouter/coding".into(),
binding_digest: "sha256:router-binding".into(),
},
&[binding],
)?;
handle.mark_upstream_started();
handle.finish(
bitrouter_sdk::language_model::receipts::RequestReceiptOutcome::Completed,
bitrouter_sdk::language_model::receipts::RequestDeliveryStatus::ServerCommitted,
None,
);
Ok(Administration {
source: crate::paths::ConfigSource::Default {
home: PathBuf::from("/nonexistent-checker-management-test"),
},
routing: Arc::new(bitrouter_sdk::config::ConfigRoutingTable::from_config(
config,
)),
policy,
observe: Arc::new(NoopObserveStatus { compiled_in: false }),
request_checks: Some(request_checks),
})
}
#[test]
fn commands_round_trip_as_json() {
for cmd in [
DaemonCommand::Stop,
DaemonCommand::Reload { env: Vec::new() },
DaemonCommand::ReloadState,
DaemonCommand::Status,
DaemonCommand::Route {
model: "gpt-5".to_string(),
},
DaemonCommand::ObserveStatus,
DaemonCommand::Inspect {
inspection: DaemonInspection::Policy {
input: PolicyInput::default(),
},
},
DaemonCommand::Inspect {
inspection: DaemonInspection::Checks,
},
DaemonCommand::Inspect {
inspection: DaemonInspection::ChecksProbe {
checker: "company".into(),
},
},
DaemonCommand::Inspect {
inspection: DaemonInspection::CheckReceipts { limit: 10 },
},
DaemonCommand::Inspect {
inspection: DaemonInspection::CheckReceipt {
request_id: "request-1".into(),
incarnation: Some("incarnation-1".into()),
},
},
] {
let json = serde_json::to_string(&cmd).unwrap();
let back: DaemonCommand = serde_json::from_str(&json).unwrap();
assert_eq!(std::mem::discriminant(&cmd), std::mem::discriminant(&back));
}
}
#[tokio::test]
async fn local_and_remote_checker_receipts_share_one_runtime_authority() -> anyhow::Result<()> {
use axum::body::{Body, to_bytes};
use http::{Request, StatusCode, header};
use tower::ServiceExt;
const TOKEN: &str = "0123456789abcdef0123456789abcdef";
let administration = checker_administration().await?;
let reloader: Arc<dyn DaemonReloader> = Arc::new(InvalidSavedConfigReloader);
let inventory = inspection_response(
DaemonInspection::Checks,
&Some(administration.clone()),
&reloader,
)
.await;
match inventory {
DaemonResponse::Inspection {
report: DaemonInspectionReport::Checks(report),
} => {
assert_eq!(report.checkers.len(), 1);
let state = report
.config_state
.ok_or_else(|| anyhow::anyhow!("missing configuration state"))?;
assert_eq!(state.saved, crate::reload::SavedConfigState::Invalid);
assert_eq!(state.running, crate::reload::RunningConfigState::Unknown);
}
other => anyhow::bail!("expected checker inventory, got {other:?}"),
}
let before = administration.check_receipts(10)?;
let probe = inspection_response(
DaemonInspection::ChecksProbe {
checker: "company".into(),
},
&Some(administration.clone()),
&reloader,
)
.await;
assert!(matches!(
probe,
DaemonResponse::Inspection {
report: DaemonInspectionReport::ChecksProbe(_)
}
));
let after = administration.check_receipts(10)?;
assert_eq!(after.receipts, before.receipts);
let local = inspection_response(
DaemonInspection::CheckReceipt {
request_id: "request-1".into(),
incarnation: None,
},
&Some(administration.clone()),
&reloader,
)
.await;
let DaemonResponse::Inspection {
report: DaemonInspectionReport::CheckReceipt(local),
} = local
else {
anyhow::bail!("local checker receipt lookup returned the wrong response")
};
let source = administration.source.clone();
let router = crate::remote_control::control_router_with_administration(
source,
PathBuf::from("missing.sock"),
TOKEN.as_bytes().to_vec(),
administration,
)?;
let request = Request::builder()
.uri("/control/v1/checks/receipts/request-1")
.header(header::AUTHORIZATION, format!("Bearer {TOKEN}"))
.body(Body::empty())?;
let response = router.oneshot(request).await?;
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), 1024 * 1024).await?;
let remote: bitrouter_sdk::language_model::receipts::RequestReceiptLookup =
serde_json::from_slice(&body)?;
assert_eq!(remote, local);
match remote {
bitrouter_sdk::language_model::receipts::RequestReceiptLookup::Found {
receipt,
retained_matches,
} => {
assert_eq!(retained_matches, 1);
assert_eq!(receipt.identity.incarnation_id, before.incarnation_id);
assert_eq!(
receipt.identity.router_binding_digest,
"sha256:router-binding"
);
assert_eq!(
receipt.outcome,
Some(bitrouter_sdk::language_model::receipts::RequestReceiptOutcome::Completed)
);
}
other => anyhow::bail!("expected retained receipt, got {other:?}"),
}
Ok(())
}
#[test]
fn inspection_response_round_trips_as_json() -> anyhow::Result<()> {
let response = DaemonResponse::Inspection {
report: DaemonInspectionReport::Providers(ProvidersReport {
resolved_via: "live".to_string(),
providers: Vec::new(),
}),
};
let json = serde_json::to_string(&response)?;
let decoded: DaemonResponse = serde_json::from_str(&json)?;
match decoded {
DaemonResponse::Inspection {
report: DaemonInspectionReport::Providers(report),
} => assert_eq!(report.resolved_via, "live"),
other => anyhow::bail!("expected provider inspection, got {other:?}"),
}
Ok(())
}
#[test]
fn observe_status_payload_round_trips() {
let payload = ObserveStatusPayload {
compiled_in: true,
exporter_wired: true,
endpoint: Some("http://collector:4318".to_string()),
header_count: 2,
service_name: Some("bitrouter".to_string()),
resource_attribute_count: 1,
sampler: Some("parentbased_always_on".to_string()),
sampler_arg: None,
metrics_enabled: true,
api_key_count: 42,
api_key_cap: 1024,
user_id_count: 7,
user_id_cap: 256,
active_spans: 3,
};
let json = serde_json::to_string(&DaemonResponse::ObserveStatus {
payload: payload.clone(),
})
.unwrap();
let back: DaemonResponse = serde_json::from_str(&json).unwrap();
match back {
DaemonResponse::ObserveStatus { payload: p } => {
assert_eq!(p.endpoint, payload.endpoint);
assert_eq!(p.api_key_count, 42);
assert_eq!(p.active_spans, 3);
}
other => panic!("expected ObserveStatus, got {other:?}"),
}
}
#[test]
fn noop_observe_status_reports_unwired() {
let p = NoopObserveStatus { compiled_in: true }.status();
assert!(p.compiled_in);
assert!(!p.exporter_wired);
assert!(p.endpoint.is_none());
assert_eq!(p.api_key_count, 0);
}
#[test]
fn legacy_unit_reload_command_still_deserialises() {
let back: DaemonCommand = serde_json::from_str(r#"{"cmd":"reload"}"#).unwrap();
match back {
DaemonCommand::Reload { env } => assert!(env.is_empty()),
other => panic!("expected Reload, got {other:?}"),
}
}
#[test]
fn reload_command_carries_env_overrides() {
let cmd = DaemonCommand::Reload {
env: vec![("OPENAI_API_KEY".to_string(), "sk-test".to_string())],
};
let json = serde_json::to_string(&cmd).unwrap();
let back: DaemonCommand = serde_json::from_str(&json).unwrap();
match back {
DaemonCommand::Reload { env } => {
assert_eq!(env.len(), 1);
assert_eq!(
env[0],
("OPENAI_API_KEY".to_string(), "sk-test".to_string())
);
}
other => panic!("expected Reload, got {other:?}"),
}
}
#[test]
fn responses_round_trip_as_json() {
let resp = DaemonResponse::Status {
pid: 42,
listen: "0.0.0.0:4356".to_string(),
models: 3,
providers: vec!["openai".to_string()],
saved_routers: None,
running_routers: None,
router_restart_required: None,
config_state: None,
};
let json = serde_json::to_string(&resp).unwrap();
let back: DaemonResponse = serde_json::from_str(&json).unwrap();
match back {
DaemonResponse::Status {
pid,
models,
providers,
..
} => {
assert_eq!(pid, 42);
assert_eq!(models, 3);
assert_eq!(providers, ["openai"]);
}
other => panic!("expected Status, got {other:?}"),
}
}
#[test]
fn legacy_status_response_keeps_configuration_evidence_unknown() -> anyhow::Result<()> {
let response: DaemonResponse = serde_json::from_str(
r#"{
"resp":"status",
"pid":42,
"listen":"127.0.0.1:4356",
"models":3,
"providers":["openai"]
}"#,
)?;
let DaemonResponse::Status {
saved_routers,
running_routers,
router_restart_required,
config_state,
..
} = response
else {
anyhow::bail!("legacy status payload decoded as the wrong response variant");
};
assert!(saved_routers.is_none());
assert!(running_routers.is_none());
assert!(router_restart_required.is_none());
assert!(config_state.is_none());
Ok(())
}
#[test]
fn router_restart_detection_uses_secret_defaults_without_exposing_them() -> anyhow::Result<()> {
let saved = bitrouter_sdk::config::parse(
"routers:\n coding:\n selection:\n kind: model\n model: demo/model\n defaults:\n system_prompt: saved secret\n params:\n temperature: 0.2\n",
)?;
let running = bitrouter_sdk::config::parse(
"routers:\n coding:\n selection:\n kind: model\n model: demo/model\n defaults:\n system_prompt: prior secret\n params:\n temperature: 0.8\n",
)?;
let saved_inventory = saved.router_inventory()?;
let saved_digest = saved_inventory
.first()
.ok_or_else(|| anyhow::anyhow!("saved router inventory is empty"))?
.binding_digest
.clone();
let running_inventory = running.router_inventory()?;
let running_digest = running_inventory
.first()
.ok_or_else(|| anyhow::anyhow!("running router inventory is empty"))?
.binding_digest
.clone();
assert_eq!(
saved_digest, running_digest,
"redacted binding identity deliberately excludes default values"
);
assert!(router_configuration_restart_required(&saved, &running));
Ok(())
}
#[test]
fn resolve_socket_joins_relative_paths_onto_config_directory() {
let resolved = resolve_socket_path(
Path::new("/Users/x/.bitrouter/bitrouter.yaml"),
"./bitrouter.sock",
);
assert_eq!(
resolved,
PathBuf::from("/Users/x/.bitrouter/bitrouter.sock")
);
}
#[test]
fn resolve_socket_handles_relative_without_dot_prefix() {
let resolved = resolve_socket_path(
Path::new("/Users/x/.bitrouter/bitrouter.yaml"),
"bitrouter.sock",
);
assert_eq!(
resolved,
PathBuf::from("/Users/x/.bitrouter/bitrouter.sock")
);
}
#[test]
fn resolve_socket_joins_nested_relative_path() {
let resolved = resolve_socket_path(
Path::new("/Users/x/.bitrouter/bitrouter.yaml"),
"sub/bitrouter.sock",
);
assert_eq!(
resolved,
PathBuf::from("/Users/x/.bitrouter/sub/bitrouter.sock")
);
}
#[test]
fn resolve_socket_passes_absolute_paths_through() {
let resolved = resolve_socket_path(
Path::new("/Users/x/.bitrouter/bitrouter.yaml"),
"/var/run/bitrouter.sock",
);
assert_eq!(resolved, PathBuf::from("/var/run/bitrouter.sock"));
}
#[test]
fn resolve_socket_handles_config_paths_without_parent() {
let resolved = resolve_socket_path(Path::new("bitrouter.yaml"), "./bitrouter.sock");
assert_eq!(resolved, PathBuf::from("bitrouter.sock"));
}
#[test]
fn is_not_reachable_detects_socket_not_found() {
let io_err = std::io::Error::from(std::io::ErrorKind::NotFound);
let err: anyhow::Error =
anyhow::Error::new(io_err).context("connecting to /tmp/bitrouter.sock");
assert!(is_not_reachable(&err));
}
#[test]
fn is_not_reachable_detects_connection_refused() {
let io_err = std::io::Error::from(std::io::ErrorKind::ConnectionRefused);
let err: anyhow::Error =
anyhow::Error::new(io_err).context("connecting to /tmp/bitrouter.sock");
assert!(is_not_reachable(&err));
}
#[test]
fn is_not_reachable_ignores_unrelated_io_errors() {
let io_err = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
let err: anyhow::Error =
anyhow::Error::new(io_err).context("connecting to /tmp/bitrouter.sock");
assert!(!is_not_reachable(&err));
}
#[test]
fn is_not_reachable_false_for_plain_messages() {
let err = anyhow::anyhow!("daemon closed the connection without responding");
assert!(!is_not_reachable(&err));
}
}