use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub profile: String,
#[serde(default)]
pub model: ModelConfig,
#[serde(default)]
pub agent: AgentConfig,
#[serde(default)]
pub record: RecordConfig,
#[serde(default)]
pub security: SecurityConfig,
#[serde(default)]
pub sandbox: SandboxConfig,
#[serde(default)]
pub tools: ToolsConfig,
#[serde(default)]
pub feedback: FeedbackConfig,
#[serde(default)]
pub memory: MemoryConfig,
#[serde(default)]
pub output: OutputConfig,
#[serde(default)]
pub components: ComponentsConfig,
#[serde(default)]
pub mcp_servers: std::collections::HashMap<String, McpServerConfig>,
#[serde(default)]
pub browser: BrowserConfig,
#[serde(default)]
pub maigret: MaigretConfig,
#[serde(default)]
pub perception: PerceptionConfig,
#[serde(default)]
pub learning: LearningConfig,
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub peers: Vec<PeerConfig>,
#[serde(default)]
pub gateway: GatewayConfig,
#[serde(default)]
pub update: UpdateConfig,
#[serde(default)]
pub watch: Vec<WatchConfig>,
#[serde(default)]
pub self_watch: SelfWatchConfig,
#[serde(default)]
pub logs: LogsConfig,
#[serde(default)]
pub upgrade: UpgradeConfig,
#[serde(default)]
pub endurance: EnduranceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogsConfig {
#[serde(default = "d_agent_log_mb")]
pub agent_max_mb: u64,
#[serde(default = "d_small_log_mb")]
pub audit_max_mb: u64,
#[serde(default = "d_small_log_mb")]
pub alerts_max_mb: u64,
}
fn d_agent_log_mb() -> u64 {
10
}
fn d_small_log_mb() -> u64 {
5
}
impl Default for LogsConfig {
fn default() -> Self {
Self {
agent_max_mb: d_agent_log_mb(),
audit_max_mb: d_small_log_mb(),
alerts_max_mb: d_small_log_mb(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchConfig {
pub name: String,
#[serde(default = "d_watch_schedule")]
pub schedule: String,
pub check: String,
#[serde(default)]
pub contains: Option<String>,
#[serde(default)]
pub output_gt: Option<f64>,
#[serde(default)]
pub output_lt: Option<f64>,
#[serde(default)]
pub notify_to: Option<String>,
#[serde(default)]
pub message: Option<String>,
#[serde(default = "d_watch_cooldown")]
pub cooldown_secs: u64,
#[serde(default = "d_watch_timeout")]
pub timeout_secs: u64,
#[serde(default = "d_true")]
pub enabled: bool,
}
fn d_watch_schedule() -> String {
"every 5m".to_string()
}
fn d_watch_cooldown() -> u64 {
1800
}
fn d_watch_timeout() -> u64 {
30
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelfWatchConfig {
#[serde(default = "d_true")]
pub enabled: bool,
#[serde(default)]
pub notify_to: Option<String>,
#[serde(default = "d_disk_pct")]
pub disk_pct: f64,
#[serde(default = "d_mem_pct")]
pub mem_pct: f64,
}
fn d_disk_pct() -> f64 {
90.0
}
fn d_mem_pct() -> f64 {
10.0
}
impl Default for SelfWatchConfig {
fn default() -> Self {
Self {
enabled: true,
notify_to: None,
disk_pct: 90.0,
mem_pct: 10.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateConfig {
#[serde(default = "d_true")]
pub check: bool,
#[serde(default)]
pub repo: String,
}
impl Default for UpdateConfig {
fn default() -> Self {
Self { check: true, repo: String::new() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpgradeConfig {
#[serde(default = "d_true")]
pub auto_check: bool,
#[serde(default = "d_upgrade_interval")]
pub check_interval_hours: u64,
#[serde(default)]
pub auto_download: bool,
#[serde(default = "d_upgrade_apply")]
pub auto_apply: String,
#[serde(default = "d_upgrade_channel")]
pub channel: String,
}
fn d_upgrade_interval() -> u64 {
24
}
fn d_upgrade_apply() -> String {
"ask".into()
}
fn d_upgrade_channel() -> String {
"stable".into()
}
impl Default for UpgradeConfig {
fn default() -> Self {
Self {
auto_check: true,
check_interval_hours: d_upgrade_interval(),
auto_download: false,
auto_apply: d_upgrade_apply(),
channel: d_upgrade_channel(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnduranceConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_endurance_probe")]
pub probe_interval_secs: u64,
#[serde(default)]
pub max_total_wait_secs: u64,
}
fn d_endurance_probe() -> u64 {
120
}
impl Default for EnduranceConfig {
fn default() -> Self {
Self {
enabled: false,
probe_interval_secs: d_endurance_probe(),
max_total_wait_secs: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_gw_max_sessions")]
pub max_live_sessions: usize,
#[serde(default = "d_gw_max_concurrency")]
pub max_concurrency: usize,
#[serde(default = "d_gw_isolation")]
pub default_isolation: String,
#[serde(default = "d_gw_permission")]
pub default_permission: String,
#[serde(default = "d_gw_idle_secs")]
pub session_idle_secs: u64,
#[serde(default)]
pub a2a: GatewayA2aConfig,
#[serde(default)]
pub feishu: GatewayFeishuConfig,
#[serde(default)]
pub telegram: GatewayTelegramConfig,
#[serde(default)]
pub discord: GatewayDiscordConfig,
#[serde(default)]
pub slack: GatewaySlackConfig,
#[serde(default)]
pub simplex: GatewaySimplexConfig,
#[serde(default = "d_true")]
pub autostart: bool,
}
impl Default for GatewayConfig {
fn default() -> Self {
Self {
enabled: false,
max_live_sessions: d_gw_max_sessions(),
max_concurrency: d_gw_max_concurrency(),
default_isolation: d_gw_isolation(),
default_permission: d_gw_permission(),
session_idle_secs: d_gw_idle_secs(),
a2a: GatewayA2aConfig::default(),
feishu: GatewayFeishuConfig::default(),
telegram: GatewayTelegramConfig::default(),
discord: GatewayDiscordConfig::default(),
slack: GatewaySlackConfig::default(),
simplex: GatewaySimplexConfig::default(),
autostart: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayFeishuConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub app_id: String,
#[serde(default)]
pub app_secret: String,
#[serde(default = "d_gw_feishu_host")]
pub host: String,
#[serde(default = "d_gw_feishu_port")]
pub port: u16,
#[serde(default = "d_gw_feishu_base")]
pub base_url: String,
#[serde(default)]
pub verification_token: String,
#[serde(default)]
pub encrypt_key: String,
#[serde(default = "d_gw_feishu_mode")]
pub mode: String,
}
fn d_gw_feishu_mode() -> String {
"webhook".to_string()
}
fn d_gw_feishu_base() -> String {
"https://open.feishu.cn".to_string()
}
impl Default for GatewayFeishuConfig {
fn default() -> Self {
Self {
enabled: false,
app_id: String::new(),
app_secret: String::new(),
host: d_gw_feishu_host(),
port: d_gw_feishu_port(),
base_url: d_gw_feishu_base(),
verification_token: String::new(),
encrypt_key: String::new(),
mode: d_gw_feishu_mode(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GatewayTelegramConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub bot_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GatewayDiscordConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub bot_token: String,
#[serde(default)]
pub channel_id: String,
#[serde(default)]
pub mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GatewaySlackConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub bot_token: String,
#[serde(default)]
pub channel_id: String,
#[serde(default)]
pub mode: String,
#[serde(default)]
pub app_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewaySimplexConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_gw_simplex_host")]
pub host: String,
#[serde(default = "d_gw_simplex_port")]
pub port: u16,
#[serde(default = "d_gw_simplex_name")]
pub bot_name: String,
}
fn d_gw_simplex_host() -> String {
"127.0.0.1".to_string()
}
fn d_gw_simplex_port() -> u16 {
5225
}
fn d_gw_simplex_name() -> String {
"AegisBot".to_string()
}
impl Default for GatewaySimplexConfig {
fn default() -> Self {
Self {
enabled: false,
host: d_gw_simplex_host(),
port: d_gw_simplex_port(),
bot_name: d_gw_simplex_name(),
}
}
}
fn d_gw_feishu_host() -> String {
"0.0.0.0".to_string()
}
fn d_gw_feishu_port() -> u16 {
9001
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayA2aConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_gw_a2a_host")]
pub host: String,
#[serde(default = "d_gw_a2a_port")]
pub port: u16,
#[serde(default)]
pub token: Option<String>,
}
impl Default for GatewayA2aConfig {
fn default() -> Self {
Self { enabled: false, host: d_gw_a2a_host(), port: d_gw_a2a_port(), token: None }
}
}
fn d_gw_max_sessions() -> usize {
16
}
fn d_gw_max_concurrency() -> usize {
3
}
fn d_gw_isolation() -> String {
"per_user".to_string()
}
fn d_gw_permission() -> String {
"safe".to_string()
}
fn d_gw_idle_secs() -> u64 {
1800
}
fn d_gw_a2a_host() -> String {
"127.0.0.1".to_string()
}
fn d_gw_a2a_port() -> u16 {
41241
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PeerConfig {
pub name: String,
#[serde(default)]
pub role: String,
#[serde(default)]
pub expertise: String,
pub url: String,
#[serde(default)]
pub token: Option<String>,
#[serde(default)]
pub trust_level: aegis_security::TrustLevel,
}
pub fn peer_trust_level(
peers: &[PeerConfig],
agent_id: &str,
) -> aegis_security::TrustLevel {
peers
.iter()
.find(|p| p.name == agent_id)
.map(|p| p.trust_level)
.unwrap_or_default()
}
pub fn set_peer_trust(
peers: &mut Vec<PeerConfig>,
agent_id: &str,
trust: aegis_security::TrustLevel,
) -> bool {
if let Some(p) = peers.iter_mut().find(|p| p.name == agent_id) {
p.trust_level = trust;
false
} else {
peers.push(PeerConfig {
name: agent_id.to_string(),
trust_level: trust,
..Default::default()
});
true
}
}
pub fn remove_peer(peers: &mut Vec<PeerConfig>, agent_id: &str) -> bool {
let before = peers.len();
peers.retain(|p| p.name != agent_id);
peers.len() < before
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
#[serde(default = "d_model")]
pub default: String,
#[serde(default = "d_provider")]
pub provider: String,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default = "d_max_tokens")]
pub max_tokens: u32,
#[serde(default = "d_timeout_secs")]
pub timeout_secs: u64,
#[serde(default = "d_max_retries")]
pub max_retries: u32,
#[serde(default)]
pub api_keys: Option<Vec<String>>,
#[serde(default)]
pub fallback_providers: Option<Vec<FallbackProviderConfig>>,
#[serde(default)]
pub context_tokens: Option<u32>,
#[serde(default)]
pub daily_token_limit: u64,
#[serde(default)]
pub frugal: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FallbackProviderConfig {
pub provider: String,
pub model: String,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub base_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
#[serde(default = "d_max_iterations")]
pub max_iterations: u32,
#[serde(default = "d_identity")]
pub identity: String,
#[serde(default = "d_context_window")]
pub context_window: u32,
#[serde(default = "d_reflect_every")]
pub reflect_every: u32,
#[serde(default)]
pub workspace: Option<String>,
#[serde(default = "d_no_progress_limit")]
pub no_progress_limit: u32,
#[serde(default)]
pub auto_continue: bool,
#[serde(default = "d_max_auto_continues")]
pub max_auto_continues: u32,
}
fn d_no_progress_limit() -> u32 {
3
}
fn d_max_auto_continues() -> u32 {
2
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordConfig {
#[serde(default = "d_retention_days")]
pub session_retention_days: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
#[serde(default = "d_true")]
pub command_approval: bool,
#[serde(default)]
pub yolo: bool,
#[serde(default)]
pub reckless: bool,
#[serde(default = "d_true")]
pub trash: bool,
#[serde(default = "d_trash_sessions")]
pub trash_max_sessions: usize,
#[serde(default = "d_trash_mb")]
pub trash_max_mb: u64,
#[serde(default = "d_true")]
pub snapshot: bool,
#[serde(default = "d_snapshot_cwd_mb")]
pub snapshot_cwd_max_mb: u64,
#[serde(default = "d_snapshot_store_mb")]
pub snapshot_store_mb: u64,
#[serde(default)]
pub allowed_commands: Vec<String>,
#[serde(default)]
pub enable_dlp: bool,
#[serde(default = "d_true")]
pub redact_tool_output: bool,
#[serde(default)]
pub rules: Vec<aegis_security::RuleConfig>,
#[serde(default)]
pub permission_mode: Option<String>,
#[serde(default = "d_true")]
pub secret_vault: bool,
#[serde(default = "d_true")]
pub secret_auto_scan: bool,
#[serde(default = "d_secret_display")]
pub secret_display: String,
}
fn d_secret_display() -> String {
"real".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SandboxConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub allow_degrade: bool,
#[serde(default = "d_sandbox_backend")]
pub backend: String,
}
fn d_sandbox_backend() -> String {
"auto".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsConfig {
#[serde(default = "d_tool_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub disabled: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackConfig {
#[serde(default = "d_true")]
pub enabled: bool,
#[serde(default = "d_min_tool_calls")]
pub min_tool_calls_for_extraction: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: std::collections::HashMap<String, String>,
}
fn d_model() -> String {
"gpt-4o-mini".into()
}
fn d_provider() -> String {
"openai".into()
}
fn d_max_tokens() -> u32 {
4096
}
fn d_timeout_secs() -> u64 {
120
}
fn d_max_retries() -> u32 {
3
}
fn d_max_iterations() -> u32 {
50
}
fn d_reflect_every() -> u32 {
10
}
fn d_context_window() -> u32 {
50
}
fn d_retention_days() -> u32 {
90
}
fn d_true() -> bool {
true
}
fn d_trash_sessions() -> usize {
20
}
fn d_trash_mb() -> u64 {
512
}
fn d_snapshot_cwd_mb() -> u64 {
200
}
fn d_snapshot_store_mb() -> u64 {
1024
}
fn d_tool_timeout() -> u64 {
300
}
fn d_min_tool_calls() -> u32 {
5
}
fn d_identity() -> String {
"You are Aegis, a cognitive agent runtime. You help with software \
engineering, research, automation, and general tasks, and you act through \
tools (shell, file read/write/edit, search, sub-task orchestration, \
memory) to get real work done — not just give advice.\n\n\
Identity: when asked who or what you are, you are Aegis. Do not identify as \
the underlying language model or its vendor. If a user specifically asks \
which model powers you, you may name it, but your identity is Aegis.\n\n\
Style: be concise and direct — lead with the answer, prefer doing over \
explaining, and use tools to verify rather than guessing. If a command \
fails ~twice with the same error, stop and diagnose the root cause instead \
of retrying small variations. Reply in the user's language.\n\n\
Capabilities: the tools listed above are what you can do directly. In the \
interactive CLI, users also have slash commands — when asked what you can \
do or how to do something, you can point them to `/help` (or typing `/` \
then Tab to list commands), e.g. `/memory` and `/profile` (your long-term \
memory and learned user profile), `/style` (answer verbosity), `/set` \
(adjust settings).\n\n\
Self-modification: you can modify your own behavior and capabilities:\n\
- Personality & style: edit ~/.aegis/SOUL.md (changes take effect next turn)\n\
- Output filtering: edit ~/.aegis/filters.toml to add compression/transform rules\n\
- New tools (no compilation): create ~/.aegis/tools.d/<name>.toml with a script \
— becomes a callable tool on next turn\n\
- Strategies & skills: add to ~/.aegis/strategies/ or ~/.aegis/skills/\n\
- Widgets: modify ~/.aegis/widgets.json for persistent UI elements\n\
- Multi-agent peers: register in ~/.aegis/peers.json for A2A delegation\n\
- Source-level (requires source checkout): use `selfmod` tool to locate, patch, \
build, test, and commit changes to aegis's own Rust code (git-isolated, \
auto-rollback on failure)\n\
When asked to change how you work or add a capability, prefer config-layer \
changes (instant, safe) unless compiled functionality is explicitly needed.\n\n\
Choices: whenever you offer the user a decision — confirming or adjusting a \
plan, picking a direction, yes/no approvals — call the `clarify` tool with \
`options` so they pick from an interactive arrow-key menu. Do NOT list the \
choices in prose and wait for them to type a reply.\n\n\
Long tasks: for a big, multi-step or multi-session task, call `task register` \
at the start, break the work into a `todo` list, and mark steps complete as \
you finish them. If you are interrupted or restarted, aegis reopens the same \
session and you continue from the first unfinished todo step — so do not \
redo completed work. Mark EVERY step complete the moment you finish it \
(including the last one), and call `task complete` at the very end — \
otherwise the task looks unfinished and is needlessly resumed. For \
anything long-running (builds, training, pipelines) use the `background` \
tool and poll it rather than blocking."
.into()
}
pub fn model_context_window(model: &str) -> u32 {
let m = model.to_ascii_lowercase();
let starts = |p: &str| m.starts_with(p);
if starts("gpt-4.1") {
1_047_576
} else if starts("o1") || starts("o3") || starts("o4") {
200_000
} else if starts("gpt-4o") || starts("gpt-4-turbo") {
128_000
} else if starts("gpt-4-32k") {
32_768
} else if starts("gpt-4") {
8_192
} else if starts("gpt-3.5") {
16_385
} else if m.contains("claude") {
200_000
} else if starts("gemini-1.5-pro") {
2_097_152
} else if starts("gemini-2.5") || starts("gemini-2.0") || starts("gemini-1.5") {
1_048_576
} else if starts("gemini-1.0") {
32_760
} else if starts("llama-4-scout") {
10_000_000
} else if starts("llama-4") {
1_000_000
} else if starts("llama-3.1") || starts("llama-3.2") || starts("llama-3.3") {
128_000
} else if starts("llama-3") {
8_192
} else if starts("llama-2") {
4_096
} else if m.contains("mistral") || m.contains("mixtral") || starts("deepseek") || starts("qwen") || starts("qwq") {
131_072
} else if m.contains("minimax") {
1_000_000
} else if starts("command-a") {
256_000
} else if starts("command-r") {
128_000
} else if starts("phi") {
128_000
} else {
128_000
}
}
impl Default for ModelConfig {
fn default() -> Self {
Self {
default: d_model(),
provider: d_provider(),
api_key: None,
base_url: None,
max_tokens: d_max_tokens(),
timeout_secs: d_timeout_secs(),
max_retries: d_max_retries(),
api_keys: None,
fallback_providers: None,
context_tokens: None,
daily_token_limit: 0,
frugal: false,
}
}
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
max_iterations: d_max_iterations(),
identity: d_identity(),
context_window: d_context_window(),
reflect_every: d_reflect_every(),
workspace: None,
no_progress_limit: d_no_progress_limit(),
auto_continue: false,
max_auto_continues: d_max_auto_continues(),
}
}
}
impl Default for RecordConfig {
fn default() -> Self {
Self {
session_retention_days: d_retention_days(),
}
}
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
command_approval: true,
yolo: false,
reckless: false,
trash: true,
trash_max_sessions: d_trash_sessions(),
trash_max_mb: d_trash_mb(),
snapshot: true,
snapshot_cwd_max_mb: d_snapshot_cwd_mb(),
snapshot_store_mb: d_snapshot_store_mb(),
allowed_commands: Vec::new(),
enable_dlp: false,
redact_tool_output: true,
rules: Vec::new(),
permission_mode: None,
secret_vault: true,
secret_auto_scan: true,
secret_display: d_secret_display(),
}
}
}
impl Default for ToolsConfig {
fn default() -> Self {
Self {
timeout_secs: d_tool_timeout(),
disabled: Vec::new(),
}
}
}
impl Default for FeedbackConfig {
fn default() -> Self {
Self {
enabled: true,
min_tool_calls_for_extraction: d_min_tool_calls(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
#[serde(default)]
pub compaction: CompactionConfig,
#[serde(default)]
pub write: MemoryWriteConfig,
#[serde(default = "d_memory_backend")]
pub backend: String,
#[serde(default = "d_compose_mode")]
pub compose_mode: String,
#[serde(default = "d_recall_limit")]
pub recall_limit: u32,
#[serde(default = "d_min_confidence")]
pub min_confidence: f32,
#[serde(default = "d_sidecar_min_candidates")]
pub sidecar_min_candidates: usize,
#[serde(default = "d_max_entries")]
pub max_entries: usize,
#[serde(default)]
pub existence_encoding: bool,
#[serde(default = "d_extraction_timeout")]
pub extraction_timeout_secs: u64,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
compaction: CompactionConfig::default(),
write: MemoryWriteConfig::default(),
backend: d_memory_backend(),
compose_mode: d_compose_mode(),
recall_limit: d_recall_limit(),
min_confidence: d_min_confidence(),
sidecar_min_candidates: d_sidecar_min_candidates(),
max_entries: d_max_entries(),
existence_encoding: false,
extraction_timeout_secs: d_extraction_timeout(),
}
}
}
fn d_extraction_timeout() -> u64 {
10
}
fn d_max_entries() -> usize {
5000
}
fn d_memory_backend() -> String {
"local".into()
}
fn d_compose_mode() -> String {
"failover".into()
}
fn d_recall_limit() -> u32 {
5
}
fn d_min_confidence() -> f32 {
0.3
}
fn d_sidecar_min_candidates() -> usize {
3
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryWriteConfig {
#[serde(default = "d_true")]
pub enabled: bool,
#[serde(default = "d_min_salience")]
pub min_salience: f32,
}
impl Default for MemoryWriteConfig {
fn default() -> Self {
Self {
enabled: true,
min_salience: d_min_salience(),
}
}
}
fn d_min_salience() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionConfig {
#[serde(default = "d_summarizer")]
pub summarizer: String,
#[serde(default = "d_model_from_tier")]
pub model_from_tier: String,
#[serde(default = "d_summarize_timeout_ms")]
pub summarize_timeout_ms: u64,
#[serde(default = "d_soft")]
pub soft: f32,
#[serde(default = "d_hard")]
pub hard: f32,
#[serde(default = "d_emergency")]
pub emergency: f32,
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
summarizer: d_summarizer(),
model_from_tier: d_model_from_tier(),
summarize_timeout_ms: d_summarize_timeout_ms(),
soft: d_soft(),
hard: d_hard(),
emergency: d_emergency(),
}
}
}
fn d_summarizer() -> String {
"model".into()
}
fn d_model_from_tier() -> String {
"hard".into()
}
fn d_summarize_timeout_ms() -> u64 {
800
}
fn d_soft() -> f32 {
0.80
}
fn d_hard() -> f32 {
0.90
}
fn d_emergency() -> f32 {
0.95
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(default = "d_output_style")]
pub style: String,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
style: d_output_style(),
}
}
}
fn d_output_style() -> String {
"normal".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentsConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_component_tier")]
pub tier: String,
}
impl Default for ComponentsConfig {
fn default() -> Self {
Self {
enabled: false,
tier: d_component_tier(),
}
}
}
fn d_component_tier() -> String {
"standard".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningConfig {
#[serde(default = "d_true")]
pub enabled: bool,
}
impl Default for LearningConfig {
fn default() -> Self {
Self { enabled: true }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_browser_binary")]
pub binary: String,
#[serde(default = "d_browser_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub bridge: BrowserBridgeConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserBridgeConfig {
#[serde(default = "d_bridge_enabled")]
pub enabled: bool,
#[serde(default = "d_bridge_port")]
pub port: u16,
#[serde(default = "d_bridge_auto_discover")]
pub auto_discover: bool,
}
fn d_bridge_enabled() -> bool {
true
}
fn d_bridge_port() -> u16 {
9222
}
fn d_bridge_auto_discover() -> bool {
true
}
impl Default for BrowserBridgeConfig {
fn default() -> Self {
Self {
enabled: d_bridge_enabled(),
port: d_bridge_port(),
auto_discover: d_bridge_auto_discover(),
}
}
}
fn d_browser_binary() -> String {
"browser-harness".into()
}
fn d_browser_timeout() -> u64 {
30
}
impl Default for BrowserConfig {
fn default() -> Self {
Self {
enabled: false,
binary: d_browser_binary(),
timeout_secs: d_browser_timeout(),
bridge: BrowserBridgeConfig::default(),
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct MaigretConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "d_maigret_path")]
pub path: String,
#[serde(default = "d_maigret_timeout")]
pub timeout_secs: u64,
#[serde(default = "d_maigret_top_sites")]
pub top_sites: u64,
}
fn d_maigret_path() -> String {
"maigret".into()
}
fn d_maigret_timeout() -> u64 {
120
}
fn d_maigret_top_sites() -> u64 {
500
}
impl Default for MaigretConfig {
fn default() -> Self {
Self {
enabled: false,
path: d_maigret_path(),
timeout_secs: d_maigret_timeout(),
top_sites: d_maigret_top_sites(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PerceptionConfig {
#[serde(default)]
pub cron: Vec<String>,
#[serde(default)]
pub webhook_port: u16,
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
if let Some(dir) = path.parent() {
let _ = dotenvy::from_path(dir.join(".env"));
}
let mut cfg = if path.exists() {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config from {}", path.display()))?;
toml::from_str(&text)
.with_context(|| format!("parsing config from {}", path.display()))?
} else {
Self::default()
};
cfg.apply_env_overrides();
cfg.apply_profile();
if cfg.model.frugal {
cfg.frugal_clamps();
}
cfg.validate()?;
Ok(cfg)
}
pub fn workspace_dir(&self) -> Option<std::path::PathBuf> {
let raw = self.agent.workspace.as_ref()?.trim();
if raw.is_empty() {
return None;
}
let path = if let Some(rest) = raw.strip_prefix('~') {
dirs_next::home_dir()
.map(|h| h.join(rest.trim_start_matches('/')))
.unwrap_or_else(|| std::path::PathBuf::from(raw))
} else {
std::path::PathBuf::from(raw)
};
Some(path)
}
pub fn apply_profile(&mut self) {
if self.profile.trim().eq_ignore_ascii_case("lite") {
self.lite_clamps();
}
}
fn lite_clamps(&mut self) {
self.components.tier = "minimal".into(); self.gateway.max_live_sessions = self.gateway.max_live_sessions.min(4);
self.gateway.session_idle_secs = self.gateway.session_idle_secs.min(600);
self.gateway.max_concurrency = self.gateway.max_concurrency.min(2);
}
fn frugal_clamps(&mut self) {
self.agent.context_window = self.agent.context_window.min(20);
self.memory.recall_limit = self.memory.recall_limit.min(3);
self.memory.compaction.summarizer = "heuristic".into(); self.memory.sidecar_min_candidates = usize::MAX; self.agent.max_iterations = self.agent.max_iterations.min(25); self.memory.existence_encoding = true; }
pub fn auto_tune_resources(&mut self) -> Option<String> {
let p = self.profile.trim().to_lowercase();
if !(p.is_empty() || p == "auto") {
return None;
}
let snap = crate::overnight::ResourceSnapshot::capture();
if snap.memory_total_mb == 0 {
return None; }
if snap.memory_total_mb <= 1280 || snap.cpu_count <= 1 {
self.lite_clamps();
return Some(format!(
"low-resource host detected ({} MB RAM, {} CPU) → lite profile (smaller context, fewer recalls, passive learning off)",
snap.memory_total_mb, snap.cpu_count
));
}
None
}
fn apply_env_overrides(&mut self) {
if let Ok(v) = std::env::var("AEGIS_MODEL") {
self.model.default = v;
}
if let Ok(v) = std::env::var("AEGIS_PROVIDER") {
self.model.provider = v;
}
if let Ok(v) = std::env::var("AEGIS_BASE_URL") {
self.model.base_url = Some(v);
}
if let Ok(v) = std::env::var("AEGIS_MAX_TOKENS") {
if let Ok(n) = v.parse() {
self.model.max_tokens = n;
}
}
if let Ok(v) = std::env::var("AEGIS_MAX_ITERATIONS") {
if let Ok(n) = v.parse() {
self.agent.max_iterations = n;
}
}
if std::env::var("AEGIS_YOLO").is_ok() {
self.security.yolo = true;
}
if std::env::var("AEGIS_RECKLESS").is_ok() {
self.security.reckless = true;
self.security.yolo = true;
}
}
fn validate(&self) -> Result<()> {
anyhow::ensure!(self.model.max_tokens > 0, "model.max_tokens must be > 0");
anyhow::ensure!(
self.model.timeout_secs > 0,
"model.timeout_secs must be > 0"
);
anyhow::ensure!(
self.agent.max_iterations > 0,
"agent.max_iterations must be > 0"
);
anyhow::ensure!(
self.agent.context_window >= 4,
"agent.context_window must be >= 4"
);
if self.gateway.simplex.enabled {
let host = self.gateway.simplex.host.trim();
let is_loopback = host == "127.0.0.1"
|| host == "::1"
|| host == "localhost"
|| host
.parse::<std::net::IpAddr>()
.map(|ip| ip.is_loopback())
.unwrap_or(false);
anyhow::ensure!(
is_loopback,
"gateway.simplex.host must be a loopback address (127.0.0.1/::1/localhost); \
the simplex-chat CLI's WebSocket control API has no authentication or \
transport encryption and must never be reachable from outside this host \
(see docs/simplex-aegis-comms-assessment.md §2/§4). Got: {:?}",
self.gateway.simplex.host
);
}
Ok(())
}
pub fn resolve_api_key(&self) -> Result<String> {
if let Some(key) = &self.model.api_key {
if !key.is_empty() {
return Ok(key.clone());
}
}
for var in ["AEGIS_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"] {
if let Ok(key) = std::env::var(var) {
if !key.is_empty() {
return Ok(key);
}
}
}
anyhow::bail!(
"No API key found. Set OPENAI_API_KEY or configure model.api_key in config.toml"
)
}
pub fn resolve_base_url(&self) -> String {
if let Some(url) = &self.model.base_url {
if !url.is_empty() {
return url.clone();
}
}
match self.model.provider.as_str() {
"anthropic" => "https://api.anthropic.com".into(),
"ollama" => "http://localhost:11434/v1".into(),
_ => "https://api.openai.com/v1".into(),
}
}
}
pub fn config_dir() -> PathBuf {
if let Ok(home) = std::env::var("AEGIS_HOME") {
return PathBuf::from(home);
}
let legacy = dirs_next::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".aegis");
if legacy.is_dir() {
return legacy;
}
if let Some(base) = dirs_next::config_dir() {
base.join("aegis")
} else {
legacy
}
}
pub fn config_path() -> PathBuf {
config_dir().join("config.toml")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_default_config() {
let cfg = Config::default();
assert_eq!(cfg.model.default, "gpt-4o-mini");
assert_eq!(cfg.model.provider, "openai");
assert_eq!(cfg.agent.max_iterations, 20);
assert_eq!(cfg.agent.context_window, 50);
assert!(cfg.security.command_approval);
assert!(!cfg.security.yolo);
}
#[test]
fn test_auto_tune_respects_explicit_profile() {
let mut c = Config::default();
c.profile = "default".into();
assert!(c.auto_tune_resources().is_none());
c.profile = "lite".into();
assert!(c.auto_tune_resources().is_none());
assert_eq!(c.agent.context_window, 50, "auto_tune must not clamp explicit profiles");
}
#[test]
fn test_explicit_lite_clamps() {
let mut c = Config::default();
c.profile = "lite".into();
c.apply_profile();
assert_eq!(c.components.tier, "minimal");
assert!(c.gateway.max_live_sessions <= 4);
assert!(c.gateway.max_concurrency <= 2);
assert_eq!(c.agent.context_window, 50, "lite must not shrink context");
assert_eq!(c.memory.recall_limit, 5, "lite must not shrink recall");
assert!(c.learning.enabled, "lite keeps passive learning ON");
assert!(!c.memory.existence_encoding, "lite must not force terse recall");
}
#[test]
fn test_frugal_clamps() {
let mut c = Config::default();
c.frugal_clamps();
assert!(c.agent.context_window <= 20);
assert!(c.memory.recall_limit <= 3);
assert!(c.memory.existence_encoding);
assert_eq!(c.memory.compaction.summarizer, "heuristic");
assert!(c.memory.max_entries >= 5000);
}
#[test]
fn test_load_nonexistent_returns_default() {
let cfg = Config::load(Path::new("/tmp/nonexistent_aegis_config_4294967295.toml")).unwrap();
assert!(!cfg.model.default.is_empty());
assert!(cfg.agent.max_iterations > 0);
}
#[test]
fn test_load_partial_toml() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let mut f = std::fs::File::create(&path).unwrap();
writeln!(f, "[model]\ndefault = \"gpt-4o\"").unwrap();
let cfg = Config::load(&path).unwrap();
assert_eq!(cfg.model.default, "gpt-4o");
assert_eq!(cfg.agent.max_iterations, 20); }
#[test]
fn test_validate_rejects_zero_max_tokens() {
let mut cfg = Config::default();
cfg.model.max_tokens = 0;
assert!(cfg.validate().is_err());
}
#[test]
fn test_validate_rejects_small_context_window() {
let mut cfg = Config::default();
cfg.agent.context_window = 2;
assert!(cfg.validate().is_err());
}
#[test]
fn test_validate_rejects_non_loopback_simplex_host() {
let mut cfg = Config::default();
cfg.gateway.simplex.enabled = true;
cfg.gateway.simplex.host = "0.0.0.0".into();
let err = cfg.validate().unwrap_err();
assert!(
err.to_string().contains("loopback"),
"expected loopback error, got: {err}"
);
}
#[test]
fn test_validate_accepts_loopback_simplex_host() {
let mut cfg = Config::default();
cfg.gateway.simplex.enabled = true;
for host in ["127.0.0.1", "::1", "localhost"] {
cfg.gateway.simplex.host = host.into();
assert!(cfg.validate().is_ok(), "host {host} should be accepted");
}
}
#[test]
fn test_validate_ignores_simplex_host_when_disabled() {
let mut cfg = Config::default();
cfg.gateway.simplex.enabled = false;
cfg.gateway.simplex.host = "0.0.0.0".into();
assert!(cfg.validate().is_ok());
}
#[test]
fn test_gateway_simplex_config_default_is_loopback_and_disabled() {
let cfg = GatewaySimplexConfig::default();
assert!(!cfg.enabled);
assert_eq!(cfg.host, "127.0.0.1");
assert_eq!(cfg.port, 5225);
assert_eq!(cfg.bot_name, "AegisBot");
}
#[test]
fn test_resolve_base_url_defaults() {
let mut cfg = Config::default();
cfg.model.provider = "openai".into();
assert!(cfg.resolve_base_url().contains("openai.com"));
cfg.model.provider = "anthropic".into();
assert!(cfg.resolve_base_url().contains("anthropic.com"));
cfg.model.provider = "ollama".into();
assert!(cfg.resolve_base_url().contains("localhost:11434"));
}
#[test]
fn test_resolve_base_url_custom() {
let mut cfg = Config::default();
cfg.model.base_url = Some("http://my-proxy:8080/v1".into());
assert_eq!(cfg.resolve_base_url(), "http://my-proxy:8080/v1");
}
#[test]
fn test_resolve_api_key_from_env() {
std::env::set_var("AEGIS_API_KEY", "test-key-12345");
let cfg = Config::default();
assert_eq!(cfg.resolve_api_key().unwrap(), "test-key-12345");
std::env::remove_var("AEGIS_API_KEY");
}
#[test]
fn test_env_override() {
std::env::set_var("AEGIS_MODEL", "claude-sonnet");
let mut cfg = Config::default();
cfg.apply_env_overrides();
assert_eq!(cfg.model.default, "claude-sonnet");
std::env::remove_var("AEGIS_MODEL");
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default)]
pub api_key: Option<String>,
}