use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::Map;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SandboxBackend {
Podman,
Bailey,
}
impl std::fmt::Display for SandboxBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SandboxBackend::Podman => write!(f, "podman"),
SandboxBackend::Bailey => write!(f, "bailey"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum NetworkMode {
Restricted,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EgressMode {
Open,
Proxy,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EgressConfig {
pub mode: EgressMode,
pub allow: Vec<String>,
pub allow_internal: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatConfig {
pub token: String,
pub channel_id: String,
pub allowed_user_ids: Vec<String>,
pub blocked_user_ids: Vec<String>,
pub operator_user_ids: Vec<String>,
pub start_on_mention: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DelegateConfig {
pub model: String,
pub per_turn: u32,
pub deadline_ms: u64,
pub base_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentConfig {
pub provider: String,
pub model: Option<String>,
pub vision_model: Option<String>,
pub delegate: Option<DelegateConfig>,
pub rules_path: Option<String>,
pub providers: Map<String, serde_json::Value>,
pub aliases: BTreeMap<String, String>,
}
impl AgentConfig {
pub fn credential_of(&self, provider: &str) -> Option<&str> {
self.providers
.get(provider)?
.get("credential")?
.as_str()
.filter(|credential| !credential.trim().is_empty())
}
pub fn credential_name_of(&self, provider: &str) -> Option<&str> {
self.providers
.get(provider)?
.get("credentialName")?
.as_str()
.filter(|name| !name.trim().is_empty())
}
pub fn credential(&self) -> &str {
self.credential_of(&self.provider).unwrap_or_default()
}
pub fn credential_names(&self) -> Vec<&str> {
self.providers
.keys()
.filter_map(|provider| self.credential_name_of(provider))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GithubConfig {
pub token: String,
pub user_name: String,
pub user_email: String,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PolicyExtraConfig {
pub read: Vec<String>,
pub write: Vec<String>,
pub execute: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxConfig {
pub backend: SandboxBackend,
pub require_full_enforcement: bool,
pub network: NetworkMode,
pub egress_ports: Vec<u16>,
pub egress: EgressConfig,
pub image: String,
pub memory: String,
pub cpus: f64,
pub pids: u32,
pub file_max: String,
pub disk: String,
pub disk_check_ms: u64,
pub grace_period_ms: u64,
pub hide_host_address: bool,
pub policy_extra: Option<PolicyExtraConfig>,
pub path_extra: Option<Vec<String>>,
pub env: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShutdownConfig {
pub allowed_user_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputConfig {
pub forward_tool_output: bool,
pub max_tool_output_chars: usize,
pub max_attachment_bytes: u64,
pub max_attachments_per_message: usize,
pub post_diffs: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WebConfig {
pub host: String,
pub port: u16,
pub observer: bool,
pub public_url: Option<String>,
}
#[expect(
clippy::struct_field_names,
reason = "the names carry the configuration keys they stand for"
)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LimitsConfig {
pub max_concurrent_turns: u32,
pub max_live_sessions: u32,
pub max_queue_length: u32,
pub max_queue_wait_ms: u64,
}
#[expect(
clippy::struct_field_names,
reason = "the names carry the configuration keys they stand for"
)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeoutsConfig {
pub idle_ms: u64,
pub startup_ms: u64,
pub question_ms: u64,
pub abort_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub chat: ChatConfig,
pub agent: AgentConfig,
pub github: Option<GithubConfig>,
pub project_root: String,
pub state_dir: String,
pub sandbox: SandboxConfig,
pub output: OutputConfig,
pub shutdown: ShutdownConfig,
pub web: Option<WebConfig>,
pub limits: LimitsConfig,
pub timeouts: TimeoutsConfig,
}
pub const ALLOW_EVERY_USER: &str = "*";
pub const SECRET_PATHS: [&str; 2] = ["chat.token", "github.token"];
pub mod defaults {
use super::{EgressMode, NetworkMode, SandboxBackend};
pub const START_ON_MENTION: bool = false;
pub const BACKEND: SandboxBackend = SandboxBackend::Bailey;
pub const REQUIRE_FULL_ENFORCEMENT: bool = true;
pub const NETWORK: NetworkMode = NetworkMode::Restricted;
pub const EGRESS_PORTS: [u16; 1] = [443];
pub const EGRESS_MODE: EgressMode = EgressMode::Proxy;
pub const EGRESS_ALLOW: [&str; 1] = ["*"];
pub const EGRESS_ALLOW_INTERNAL: bool = false;
pub const HIDE_HOST_ADDRESS: bool = false;
pub const IMAGE: &str = "localhost/errand-agent:latest";
pub const MEMORY: &str = "4g";
pub const CPUS: f64 = 2.0;
pub const PIDS: u32 = 512;
pub const FILE_MAX: &str = "1g";
pub const DISK: &str = "5g";
pub const DISK_CHECK_MS: u64 = 30_000;
pub const GRACE_PERIOD_MS: u64 = 10_000;
pub const FORWARD_TOOL_OUTPUT: bool = false;
pub const MAX_TOOL_OUTPUT_CHARS: usize = 1_500;
pub const MAX_ATTACHMENT_BYTES: u64 = 5 * 1024 * 1024;
pub const MAX_ATTACHMENTS_PER_MESSAGE: usize = 4;
pub const POST_DIFFS: bool = true;
pub const MAX_CONCURRENT_TURNS: u32 = 2;
pub const MAX_LIVE_SESSIONS: u32 = 4;
pub const MAX_QUEUE_LENGTH: u32 = 32;
pub const MAX_QUEUE_WAIT_MS: u64 = 900_000;
pub const DELEGATE_PER_TURN: u32 = 8;
pub const DELEGATE_DEADLINE_MS: u64 = 60_000;
pub const WEB_HOST: &str = "127.0.0.1";
pub const WEB_PORT: u16 = 8787;
pub const WEB_OBSERVER: bool = false;
pub const IDLE_MS: u64 = 1_800_000;
pub const STARTUP_MS: u64 = 60_000;
pub const QUESTION_MS: u64 = 300_000;
pub const ABORT_MS: u64 = 15_000;
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigError {
pub problems: Vec<String>,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "configuration rejected:")?;
for problem in &self.problems {
write!(f, "\n - {problem}")?;
}
Ok(())
}
}
impl std::error::Error for ConfigError {}