1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(default)]
8pub struct Config {
9 pub provider: ProviderConfig,
10 pub embeddings: EmbeddingsConfig,
11 pub agent: AgentConfig,
12 pub model: String,
13 pub system_prompt: String,
14 pub workspace: PathBuf,
15 pub storage: StorageConfig,
16 pub runtime: RuntimeConfig,
17 pub hosting: HostingConfig,
18 pub observability: ObservabilityConfig,
19 pub channel: ChannelConfig,
20 pub gateway: GatewayConfig,
21 pub policy: PolicyConfig,
22 pub plugin_layer: PluginLayerConfig,
23 pub group_chat: GroupChatConfig,
24 pub toolsets: ToolsetConfig,
25 pub memory: MemoryIdeasConfig,
26 #[serde(default)]
27 pub zkr: ZkrConfig,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(default)]
32pub struct ProviderConfig {
33 pub name: String,
34 pub api_key: Option<String>,
35 pub base_url: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(default)]
40pub struct EmbeddingsConfig {
41 pub enabled: bool,
42 pub provider: String,
43 pub api_key: Option<String>,
44 pub model: Option<String>,
45 pub base_url: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(default)]
50pub struct AgentConfig {
51 pub max_rounds: usize,
53 pub max_history_messages: usize,
55 pub max_tool_result_chars: usize,
57 pub max_context_chars: usize,
59 pub fast_model: String,
61 pub heavy_model: String,
63 pub permissions: PermissionRulesConfig,
65 pub permission_profile: String,
67}
68
69impl Default for AgentConfig {
70 fn default() -> Self {
71 Self {
72 max_rounds: 50,
73 max_history_messages: 10,
74 max_tool_result_chars: 20_000,
75 max_context_chars: 150_000,
76 fast_model: "claude-haiku-4-5-20251001".to_string(),
77 heavy_model: "claude-sonnet-4-6".to_string(),
78 permissions: PermissionRulesConfig::default(),
79 permission_profile: "auto".to_string(),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90#[serde(default)]
91pub struct PermissionRulesConfig {
92 pub deny: Vec<String>,
94 pub allow: Vec<String>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(default)]
100pub struct RuntimeConfig {
101 pub kind: String, pub docker_image: Option<String>,
103 pub memory_limit_mb: Option<u64>,
104 pub state_path: Option<PathBuf>,
105 pub self_update: SelfUpdateConfig,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(default)]
110pub struct SelfUpdateConfig {
111 pub enabled: bool,
112 pub interval_secs: u64,
113 pub remote: String,
114 pub branch: String,
115 pub restart_service: Option<String>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(default)]
120pub struct StorageConfig {
121 pub backend: String, pub root: PathBuf,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(default)]
127pub struct HostingConfig {
128 pub enabled: bool,
129 pub tenant_root: PathBuf,
130 pub session_timeout_minutes: u64,
131 pub default_channel: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(default)]
136pub struct ObservabilityConfig {
137 pub service_name: String,
138 pub environment: String,
139 pub json_logs: bool,
140 pub trace_header_name: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(default)]
145pub struct ChannelConfig {
146 pub kind: String, pub token: Option<String>,
148 pub allowed_chat_ids: Vec<String>,
150 pub allowed_sender_ids: Vec<String>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(default)]
156pub struct GatewayConfig {
157 pub bind: String,
158 pub auth_token: Option<String>,
159 pub enable_admin_api: bool,
160 pub request_body_limit_kb: usize,
161 pub request_timeout_secs: u64,
162 pub rate_limit_per_minute: usize,
163 pub trusted_proxies: Vec<String>,
164 pub allowed_origins: Vec<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(default)]
169pub struct PolicyConfig {
170 pub allow_shell: bool,
171 pub allow_dynamic_tools: bool,
172 pub allow_plugin_shell: bool,
173 pub allow_plugin_git: bool,
174 pub allow_computer_use: bool,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(default)]
179pub struct PluginLayerConfig {
180 pub enabled: bool,
181 #[serde(default = "default_manifest_path")]
182 pub manifest_path: PathBuf,
183 #[serde(default)]
185 pub host_plugin_roots: Vec<PathBuf>,
186 pub hook_events: Vec<String>,
187 pub allow_core_fallback: bool,
188 pub layered_overrides: Vec<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(default)]
193pub struct GroupChatConfig {
194 pub enable_ambient_questions: bool,
195 pub rolling_memory_namespace: String,
196 pub rolling_memory_max_chars: usize,
197 pub rolling_memory_recent_turns: usize,
198 pub ambient_question_window: usize,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, Default)]
202#[serde(default)]
203pub struct ToolsetConfig {
204 pub enabled: Vec<String>,
205 pub disabled: Vec<String>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(default)]
210pub struct MemoryIdeasConfig {
211 pub principal_id: Option<String>,
213 pub inject_context: bool,
215 pub graph_recall_limit: usize,
216 pub heartbeat_chat_id: Option<String>,
218 pub dream_on_heartbeat: bool,
220}
221
222impl Default for MemoryIdeasConfig {
223 fn default() -> Self {
224 Self {
225 principal_id: None,
226 inject_context: true,
227 graph_recall_limit: 5,
228 heartbeat_chat_id: None,
229 dream_on_heartbeat: false,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(default)]
236pub struct ZkrConfig {
237 pub enabled: bool,
238 pub database: PathBuf,
239 pub tenant_id: String,
240 pub person_id: String,
241 pub auto_capture: bool,
242 pub inject_recall: bool,
243 pub recall_limit: u32,
244 pub self_improve: bool,
245}
246
247impl Default for ZkrConfig {
248 fn default() -> Self {
249 Self {
250 enabled: true,
251 database: PathBuf::from(".apollo/zkr.db"),
252 tenant_id: "apollo".to_string(),
253 person_id: "local".to_string(),
254 auto_capture: true,
255 inject_recall: true,
256 recall_limit: 5,
257 self_improve: true,
258 }
259 }
260}
261
262pub fn apply_permission_profile(cfg: &mut Config, profile: &str) {
264 let p = profile.trim().to_ascii_lowercase().replace(['-', ' '], "_");
265 match p.as_str() {
266 "full" => {
267 cfg.agent.permission_profile = "full".to_string();
268 cfg.policy.allow_shell = true;
269 cfg.policy.allow_dynamic_tools = true;
270 cfg.policy.allow_computer_use = true;
271 cfg.toolsets = ToolsetConfig::default();
272 cfg.agent.permissions = PermissionRulesConfig::default();
273 }
274 "auto" => {
275 cfg.agent.permission_profile = "auto".to_string();
276 cfg.policy = PolicyConfig::default();
277 cfg.toolsets = ToolsetConfig::default();
278 cfg.agent.permissions = PermissionRulesConfig::default();
279 }
280 "prompt" => {
281 cfg.agent.permission_profile = "prompt".to_string();
282 cfg.policy = PolicyConfig::default();
283 cfg.toolsets = ToolsetConfig::default();
284 cfg.agent.permissions = PermissionRulesConfig::default();
285 }
286 "tools_only" | "tools" => {
287 cfg.agent.permission_profile = "tools_only".to_string();
288 cfg.policy.allow_shell = false;
289 cfg.policy.allow_dynamic_tools = false;
290 cfg.policy.allow_computer_use = false;
291 cfg.toolsets.enabled = vec![
292 "web".to_string(),
293 "memory".to_string(),
294 "sessions".to_string(),
295 ];
296 cfg.toolsets.disabled = vec![
297 "browser".to_string(),
298 "vibemania".to_string(),
299 "create_tool".to_string(),
300 "mcp".to_string(),
301 ];
302 cfg.agent.permissions = PermissionRulesConfig::default();
303 }
304 _ => {
305 cfg.agent.permission_profile = "auto".to_string();
306 cfg.policy = PolicyConfig::default();
307 cfg.toolsets = ToolsetConfig::default();
308 cfg.agent.permissions = PermissionRulesConfig::default();
309 }
310 }
311}
312
313impl Config {
314 pub fn load(path: &str) -> anyhow::Result<Self> {
315 let content = std::fs::read_to_string(path)?;
316 let config: Config = serde_json::from_str(&content)?;
317 Ok(config)
318 }
319
320 pub fn default_config() -> Self {
321 Self {
322 provider: ProviderConfig::default(),
323 embeddings: EmbeddingsConfig::default(),
324 agent: AgentConfig::default(),
325 model: "claude-sonnet-4-6".to_string(),
326 system_prompt: "You are a helpful AI assistant.".to_string(),
327 workspace: PathBuf::from("."),
328 storage: StorageConfig::default(),
329 runtime: RuntimeConfig::default(),
330 hosting: HostingConfig::default(),
331 observability: ObservabilityConfig::default(),
332 channel: ChannelConfig::default(),
333 gateway: GatewayConfig::default(),
334 policy: PolicyConfig::default(),
335 plugin_layer: PluginLayerConfig::default(),
336 group_chat: GroupChatConfig::default(),
337 toolsets: ToolsetConfig::default(),
338 memory: MemoryIdeasConfig::default(),
339 zkr: ZkrConfig::default(),
340 }
341 }
342}
343
344impl Default for Config {
345 fn default() -> Self {
346 Self::default_config()
347 }
348}
349
350impl Default for ProviderConfig {
351 fn default() -> Self {
352 Self {
353 name: "anthropic".to_string(),
354 api_key: None,
355 base_url: None,
356 }
357 }
358}
359
360impl Default for EmbeddingsConfig {
361 fn default() -> Self {
362 Self {
363 enabled: false,
364 provider: "noop".to_string(),
365 api_key: None,
366 model: None,
367 base_url: None,
368 }
369 }
370}
371
372impl Default for RuntimeConfig {
373 fn default() -> Self {
374 Self {
375 kind: "native".to_string(),
376 docker_image: None,
377 memory_limit_mb: None,
378 state_path: None,
379 self_update: SelfUpdateConfig::default(),
380 }
381 }
382}
383
384impl Default for SelfUpdateConfig {
385 fn default() -> Self {
386 Self {
387 enabled: false,
388 interval_secs: 900,
389 remote: "origin".to_string(),
390 branch: "main".to_string(),
391 restart_service: Some("apollo".to_string()),
392 }
393 }
394}
395
396impl Default for StorageConfig {
397 fn default() -> Self {
398 Self {
399 backend: "surreal".to_string(),
400 root: PathBuf::from(".apollo"),
401 }
402 }
403}
404
405impl Default for HostingConfig {
406 fn default() -> Self {
407 Self {
408 enabled: true,
409 tenant_root: PathBuf::from(".apollo/tenants"),
410 session_timeout_minutes: 120,
411 default_channel: "gateway".to_string(),
412 }
413 }
414}
415
416impl Default for ObservabilityConfig {
417 fn default() -> Self {
418 Self {
419 service_name: "apollo".to_string(),
420 environment: "development".to_string(),
421 json_logs: false,
422 trace_header_name: "traceparent".to_string(),
423 }
424 }
425}
426
427impl Default for ChannelConfig {
428 fn default() -> Self {
429 Self {
430 kind: "cli".to_string(),
431 token: None,
432 allowed_chat_ids: Vec::new(),
433 allowed_sender_ids: Vec::new(),
434 }
435 }
436}
437
438impl Default for GatewayConfig {
439 fn default() -> Self {
440 Self {
441 bind: "127.0.0.1:8080".to_string(),
442 auth_token: None,
443 enable_admin_api: false,
444 request_body_limit_kb: 512,
445 request_timeout_secs: 60,
446 rate_limit_per_minute: 120,
447 trusted_proxies: Vec::new(),
448 allowed_origins: Vec::new(),
449 }
450 }
451}
452
453impl Default for PolicyConfig {
454 fn default() -> Self {
455 Self {
456 allow_shell: true,
457 allow_dynamic_tools: true,
458 allow_plugin_shell: true,
459 allow_plugin_git: true,
460 allow_computer_use: true,
461 }
462 }
463}
464
465fn default_manifest_path() -> PathBuf {
466 PathBuf::from("plugins/manifest.json")
467}
468
469impl Default for PluginLayerConfig {
470 fn default() -> Self {
471 Self {
472 enabled: true,
473 manifest_path: default_manifest_path(),
474 host_plugin_roots: Vec::new(),
475 hook_events: vec![
476 "before_message".to_string(),
477 "after_message".to_string(),
478 "before_tool".to_string(),
479 "after_tool".to_string(),
480 ],
481 allow_core_fallback: true,
482 layered_overrides: vec!["system_prompt".to_string(), "toolsets".to_string()],
483 }
484 }
485}
486
487impl Default for GroupChatConfig {
488 fn default() -> Self {
489 Self {
490 enable_ambient_questions: true,
491 rolling_memory_namespace: "group_memory".to_string(),
492 rolling_memory_max_chars: 6_000,
493 rolling_memory_recent_turns: 16,
494 ambient_question_window: 24,
495 }
496 }
497}
498
499#[cfg(test)]
500mod permission_profile_tests {
501 use super::*;
502
503 #[test]
504 fn full_enables_shell_and_resets_toolsets() {
505 let mut cfg = Config::default();
506 cfg.policy.allow_shell = false;
507 cfg.toolsets.enabled = vec!["browser".into()];
508 apply_permission_profile(&mut cfg, "full");
509 assert_eq!(cfg.agent.permission_profile, "full");
510 assert!(cfg.policy.allow_shell);
511 assert!(cfg.policy.allow_dynamic_tools);
512 assert!(cfg.toolsets.enabled.is_empty());
513 }
514
515 #[test]
516 fn tools_only_disables_shell_and_limits_toolsets() {
517 let mut cfg = Config::default();
518 apply_permission_profile(&mut cfg, "tools-only");
519 assert_eq!(cfg.agent.permission_profile, "tools_only");
520 assert!(!cfg.policy.allow_shell);
521 assert!(!cfg.policy.allow_dynamic_tools);
522 assert_eq!(
523 cfg.toolsets.enabled,
524 vec![
525 "web".to_string(),
526 "memory".to_string(),
527 "sessions".to_string()
528 ]
529 );
530 assert!(cfg.toolsets.disabled.contains(&"browser".to_string()));
531 }
532
533 #[test]
534 fn unknown_profile_falls_back_to_auto_defaults() {
535 let mut cfg = Config::default();
536 cfg.policy.allow_shell = false;
537 apply_permission_profile(&mut cfg, "nope");
538 assert_eq!(cfg.agent.permission_profile, "auto");
539 assert!(cfg.policy.allow_shell);
540 }
541}