#![allow(missing_docs)]
use serde::Deserialize;
use crate::Error;
#[derive(Debug, Clone, Deserialize)]
pub struct DaemonConfig {
#[serde(default)]
pub kafka: Option<KafkaConfig>,
#[serde(default = "default_daemon_bind")]
pub bind: String,
#[serde(default = "default_max_concurrent")]
pub max_concurrent_tasks: usize,
#[serde(default)]
pub metrics: Option<MetricsConfig>,
#[serde(default)]
pub database_url: Option<String>,
pub auth: Option<AuthConfig>,
#[serde(default)]
pub memory: DaemonMemoryConfig,
#[serde(default)]
pub audit: DaemonAuditConfig,
#[serde(default)]
pub idempotency: IdempotencyConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DaemonMcpServerConfig {
#[serde(default = "default_mcp_server_name")]
pub name: String,
#[serde(default = "super::default_true")]
pub expose_tools: bool,
#[serde(default = "super::default_true")]
pub expose_resources: bool,
#[serde(default)]
pub expose_prompts: bool,
}
fn default_mcp_server_name() -> String {
"heartbit".into()
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DaemonAuditConfig {
#[serde(default)]
pub retain_days: Option<u32>,
#[serde(default)]
pub prune_interval_minutes: Option<u64>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct IdempotencyConfig {
#[serde(default)]
pub ttl_hours: Option<u32>,
#[serde(default)]
pub sweep_interval_minutes: Option<u32>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DaemonMemoryConfig {
#[serde(default)]
pub shared_write_roles: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthConfig {
#[serde(default)]
pub bearer_tokens: Vec<String>,
pub jwks_url: Option<String>,
pub issuer: Option<String>,
pub audience: Option<String>,
pub user_id_claim: Option<String>,
pub tenant_id_claim: Option<String>,
pub roles_claim: Option<String>,
pub token_exchange: Option<TokenExchangeConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TokenExchangeConfig {
pub exchange_url: String,
pub client_id: String,
pub client_secret: String,
pub tenant_id: Option<String>,
#[serde(default)]
pub agent_token: String,
#[serde(default)]
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HeartbitPulseConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_pulse_interval")]
pub interval_seconds: u64,
pub active_hours: Option<ActiveHoursConfig>,
pub prompt: Option<String>,
#[serde(default = "default_idle_backoff_threshold")]
pub idle_backoff_threshold: u32,
}
fn default_pulse_interval() -> u64 {
1800
}
fn default_idle_backoff_threshold() -> u32 {
6
}
#[derive(Debug, Clone, Deserialize)]
pub struct ActiveHoursConfig {
pub start: String,
pub end: String,
}
impl ActiveHoursConfig {
pub fn parse_start(&self) -> Result<(u32, u32), Error> {
parse_hhmm(&self.start)
}
pub fn parse_end(&self) -> Result<(u32, u32), Error> {
parse_hhmm(&self.end)
}
}
fn parse_hhmm(s: &str) -> Result<(u32, u32), Error> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return Err(Error::Config(format!(
"invalid time format '{}': expected HH:MM",
s
)));
}
let hour: u32 = parts[0]
.parse()
.map_err(|_| Error::Config(format!("invalid hour in '{s}'")))?;
let minute: u32 = parts[1]
.parse()
.map_err(|_| Error::Config(format!("invalid minute in '{s}'")))?;
if hour > 23 || minute > 59 {
return Err(Error::Config(format!(
"time '{}' out of range (00:00-23:59)",
s
)));
}
Ok((hour, minute))
}
#[derive(Debug, Clone, Deserialize)]
pub struct WsConfig {
#[serde(default = "super::default_true")]
pub enabled: bool,
#[serde(default = "default_interaction_timeout")]
pub interaction_timeout_seconds: u64,
#[serde(default = "default_max_ws_connections")]
pub max_connections: usize,
#[serde(default)]
pub database_url: Option<String>,
}
fn default_interaction_timeout() -> u64 {
120
}
fn default_max_ws_connections() -> usize {
100
}
#[derive(Debug, Clone, Deserialize)]
pub struct KafkaConfig {
pub brokers: String,
#[serde(default = "default_consumer_group")]
pub consumer_group: String,
#[serde(default = "default_commands_topic")]
pub commands_topic: String,
#[serde(default = "default_events_topic")]
pub events_topic: String,
#[serde(default = "default_dead_letter_topic")]
pub dead_letter_topic: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduleEntry {
pub name: String,
pub cron: String,
pub task: String,
#[serde(default = "super::default_true")]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MetricsConfig {
#[serde(default = "super::default_true")]
pub enabled: bool,
}
fn default_daemon_bind() -> String {
"127.0.0.1:3000".into()
}
fn default_max_concurrent() -> usize {
4
}
fn default_consumer_group() -> String {
"heartbit-daemon".into()
}
fn default_commands_topic() -> String {
"heartbit.commands".into()
}
fn default_events_topic() -> String {
"heartbit.events".into()
}
fn default_dead_letter_topic() -> String {
"heartbit.dead-letter".into()
}