//! Configuration for the NEAR Agent. use std::path::PathBuf; use std::time::Duration; use secrecy::{ExposeSecret, SecretString}; use crate::error::ConfigError; /// Main configuration for the agent. #[derive(Debug, Clone)] pub struct Config { pub database: DatabaseConfig, pub llm: LlmConfig, pub channels: ChannelsConfig, pub agent: AgentConfig, pub safety: SafetyConfig, pub wasm: WasmConfig, pub secrets: SecretsConfig, } impl Config { /// Load configuration from environment variables. pub fn from_env() -> Result { // Load .env file if present (ignore errors if not found) let _ = dotenvy::dotenv(); Ok(Self { database: DatabaseConfig::from_env()?, llm: LlmConfig::from_env()?, channels: ChannelsConfig::from_env()?, agent: AgentConfig::from_env()?, safety: SafetyConfig::from_env()?, wasm: WasmConfig::from_env()?, secrets: SecretsConfig::from_env()?, }) } } /// Database configuration. #[derive(Debug, Clone)] pub struct DatabaseConfig { pub url: SecretString, pub pool_size: usize, } impl DatabaseConfig { fn from_env() -> Result { Ok(Self { url: SecretString::from(required_env("DATABASE_URL")?), pool_size: optional_env("DATABASE_POOL_SIZE")? .map(|s| s.parse()) .transpose() .map_err(|e| ConfigError::InvalidValue { key: "DATABASE_POOL_SIZE".to_string(), message: format!("must be a positive integer: {e}"), })? .unwrap_or(10), }) } /// Get the database URL (exposes the secret). pub fn url(&self) -> &str { self.url.expose_secret() } } /// LLM provider configuration (NEAR AI only). #[derive(Debug, Clone)] pub struct LlmConfig { pub nearai: NearAiConfig, } /// NEAR AI chat-api configuration. #[derive(Debug, Clone)] pub struct NearAiConfig { /// Session token for authentication (format: sess_xxx) pub session_token: SecretString, /// Model to use (e.g., "claude-3-5-sonnet-20241022", "gpt-4o") pub model: String, /// Base URL for the NEAR AI chat-api (default: https://api.near.ai) pub base_url: String, } impl LlmConfig { fn from_env() -> Result { let session_token = required_env("NEARAI_SESSION_TOKEN")?; Ok(Self { nearai: NearAiConfig { session_token: SecretString::from(session_token), model: optional_env("NEARAI_MODEL")? .unwrap_or_else(|| "claude-3-5-sonnet-20241022".to_string()), base_url: optional_env("NEARAI_BASE_URL")? .unwrap_or_else(|| "https://api.near.ai".to_string()), }, }) } } /// Channel configurations. #[derive(Debug, Clone)] pub struct ChannelsConfig { pub cli: CliConfig, pub slack: Option, pub telegram: Option, pub http: Option, } #[derive(Debug, Clone)] pub struct CliConfig { pub enabled: bool, } #[derive(Debug, Clone)] pub struct SlackConfig { pub bot_token: SecretString, pub app_token: SecretString, pub signing_secret: SecretString, } #[derive(Debug, Clone)] pub struct TelegramConfig { pub bot_token: SecretString, } #[derive(Debug, Clone)] pub struct HttpConfig { pub host: String, pub port: u16, pub webhook_secret: Option, } impl ChannelsConfig { fn from_env() -> Result { let slack = match ( optional_env("SLACK_BOT_TOKEN")?, optional_env("SLACK_APP_TOKEN")?, optional_env("SLACK_SIGNING_SECRET")?, ) { (Some(bot_token), Some(app_token), Some(signing_secret)) => Some(SlackConfig { bot_token: SecretString::from(bot_token), app_token: SecretString::from(app_token), signing_secret: SecretString::from(signing_secret), }), (None, None, None) => None, _ => { return Err(ConfigError::InvalidValue { key: "SLACK_*".to_string(), message: "all Slack environment variables must be set together".to_string(), }); } }; let telegram = optional_env("TELEGRAM_BOT_TOKEN")?.map(|token| TelegramConfig { bot_token: SecretString::from(token), }); let http = if optional_env("HTTP_PORT")?.is_some() || optional_env("HTTP_HOST")?.is_some() { Some(HttpConfig { host: optional_env("HTTP_HOST")?.unwrap_or_else(|| "0.0.0.0".to_string()), port: optional_env("HTTP_PORT")? .map(|s| s.parse()) .transpose() .map_err(|e| ConfigError::InvalidValue { key: "HTTP_PORT".to_string(), message: format!("must be a valid port number: {e}"), })? .unwrap_or(8080), webhook_secret: optional_env("HTTP_WEBHOOK_SECRET")?.map(SecretString::from), }) } else { None }; Ok(Self { cli: CliConfig { enabled: true }, slack, telegram, http, }) } } /// Agent behavior configuration. #[derive(Debug, Clone)] pub struct AgentConfig { pub name: String, pub max_parallel_jobs: usize, pub job_timeout: Duration, pub stuck_threshold: Duration, pub repair_check_interval: Duration, pub max_repair_attempts: u32, } impl AgentConfig { fn from_env() -> Result { Ok(Self { name: optional_env("AGENT_NAME")?.unwrap_or_else(|| "near-agent".to_string()), max_parallel_jobs: parse_optional_env("AGENT_MAX_PARALLEL_JOBS", 5)?, job_timeout: Duration::from_secs(parse_optional_env("AGENT_JOB_TIMEOUT_SECS", 3600)?), stuck_threshold: Duration::from_secs(parse_optional_env( "AGENT_STUCK_THRESHOLD_SECS", 300, )?), repair_check_interval: Duration::from_secs(parse_optional_env( "SELF_REPAIR_CHECK_INTERVAL_SECS", 60, )?), max_repair_attempts: parse_optional_env("SELF_REPAIR_MAX_ATTEMPTS", 3)?, }) } } /// Safety configuration. #[derive(Debug, Clone)] pub struct SafetyConfig { pub max_output_length: usize, pub injection_check_enabled: bool, } impl SafetyConfig { fn from_env() -> Result { Ok(Self { max_output_length: parse_optional_env("SAFETY_MAX_OUTPUT_LENGTH", 100_000)?, injection_check_enabled: optional_env("SAFETY_INJECTION_CHECK_ENABLED")? .map(|s| s.parse()) .transpose() .map_err(|e| ConfigError::InvalidValue { key: "SAFETY_INJECTION_CHECK_ENABLED".to_string(), message: format!("must be 'true' or 'false': {e}"), })? .unwrap_or(true), }) } } /// WASM sandbox configuration. #[derive(Debug, Clone)] pub struct WasmConfig { /// Whether WASM tool execution is enabled. pub enabled: bool, /// Default memory limit in bytes (default: 10 MB). pub default_memory_limit: u64, /// Default execution timeout in seconds (default: 60). pub default_timeout_secs: u64, /// Default fuel limit for CPU metering (default: 10M). pub default_fuel_limit: u64, /// Whether to cache compiled modules. pub cache_compiled: bool, /// Directory for compiled module cache. pub cache_dir: Option, } /// Secrets management configuration. #[derive(Clone, Default)] pub struct SecretsConfig { /// Master key for encrypting secrets (loaded from SECRETS_MASTER_KEY env var). /// Must be at least 32 bytes for AES-256-GCM. pub master_key: Option, /// Whether secrets management is enabled. pub enabled: bool, } impl std::fmt::Debug for SecretsConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SecretsConfig") .field("master_key", &self.master_key.is_some()) .field("enabled", &self.enabled) .finish() } } impl SecretsConfig { fn from_env() -> Result { let master_key = optional_env("SECRETS_MASTER_KEY")?.map(SecretString::from); let enabled = master_key.is_some(); // Validate master key length if provided if let Some(ref key) = master_key { if key.expose_secret().len() < 32 { return Err(ConfigError::InvalidValue { key: "SECRETS_MASTER_KEY".to_string(), message: "must be at least 32 bytes for AES-256-GCM".to_string(), }); } } Ok(Self { master_key, enabled, }) } /// Get the master key if configured. pub fn master_key(&self) -> Option<&SecretString> { self.master_key.as_ref() } } impl Default for WasmConfig { fn default() -> Self { Self { enabled: true, default_memory_limit: 10 * 1024 * 1024, // 10 MB default_timeout_secs: 60, default_fuel_limit: 10_000_000, cache_compiled: true, cache_dir: None, } } } impl WasmConfig { fn from_env() -> Result { Ok(Self { enabled: optional_env("WASM_ENABLED")? .map(|s| s.parse()) .transpose() .map_err(|e| ConfigError::InvalidValue { key: "WASM_ENABLED".to_string(), message: format!("must be 'true' or 'false': {e}"), })? .unwrap_or(true), default_memory_limit: parse_optional_env( "WASM_DEFAULT_MEMORY_LIMIT", 10 * 1024 * 1024, )?, default_timeout_secs: parse_optional_env("WASM_DEFAULT_TIMEOUT_SECS", 60)?, default_fuel_limit: parse_optional_env("WASM_DEFAULT_FUEL_LIMIT", 10_000_000)?, cache_compiled: optional_env("WASM_CACHE_COMPILED")? .map(|s| s.parse()) .transpose() .map_err(|e| ConfigError::InvalidValue { key: "WASM_CACHE_COMPILED".to_string(), message: format!("must be 'true' or 'false': {e}"), })? .unwrap_or(true), cache_dir: optional_env("WASM_CACHE_DIR")?.map(PathBuf::from), }) } /// Convert to WasmRuntimeConfig. pub fn to_runtime_config(&self) -> crate::tools::wasm::WasmRuntimeConfig { use crate::tools::wasm::{FuelConfig, ResourceLimits, WasmRuntimeConfig}; use std::time::Duration; WasmRuntimeConfig { default_limits: ResourceLimits { memory_bytes: self.default_memory_limit, fuel: self.default_fuel_limit, timeout: Duration::from_secs(self.default_timeout_secs), }, fuel_config: FuelConfig { initial_fuel: self.default_fuel_limit, enabled: true, }, cache_compiled: self.cache_compiled, cache_dir: self.cache_dir.clone(), optimization_level: wasmtime::OptLevel::Speed, } } } // Helper functions fn required_env(key: &str) -> Result { std::env::var(key).map_err(|_| ConfigError::MissingEnvVar(key.to_string())) } fn optional_env(key: &str) -> Result, ConfigError> { match std::env::var(key) { Ok(val) if val.is_empty() => Ok(None), Ok(val) => Ok(Some(val)), Err(std::env::VarError::NotPresent) => Ok(None), Err(e) => Err(ConfigError::ParseError(format!( "failed to read {key}: {e}" ))), } } fn parse_optional_env(key: &str, default: T) -> Result where T: std::str::FromStr, T::Err: std::fmt::Display, { optional_env(key)? .map(|s| { s.parse().map_err(|e| ConfigError::InvalidValue { key: key.to_string(), message: format!("{e}"), }) }) .transpose() .map(|opt| opt.unwrap_or(default)) }