Skip to main content

roboticus_core/config/
runtime_core.rs

1#[derive(Debug, Clone, Serialize, Deserialize)]
2pub struct A2aConfig {
3    #[serde(default = "default_true")]
4    pub enabled: bool,
5    #[serde(default = "default_a2a_max_msg_size")]
6    pub max_message_size: usize,
7    #[serde(default = "default_a2a_rate_limit")]
8    pub rate_limit_per_peer: u32,
9    #[serde(default = "default_a2a_session_timeout")]
10    pub session_timeout_seconds: u64,
11    #[serde(default = "default_true")]
12    pub require_on_chain_identity: bool,
13    #[serde(default = "default_a2a_nonce_ttl")]
14    pub nonce_ttl_seconds: u64,
15}
16
17impl Default for A2aConfig {
18    fn default() -> Self {
19        Self {
20            enabled: true,
21            max_message_size: default_a2a_max_msg_size(),
22            rate_limit_per_peer: default_a2a_rate_limit(),
23            session_timeout_seconds: default_a2a_session_timeout(),
24            require_on_chain_identity: true,
25            nonce_ttl_seconds: default_a2a_nonce_ttl(),
26        }
27    }
28}
29
30fn default_a2a_max_msg_size() -> usize {
31    65536
32}
33fn default_a2a_rate_limit() -> u32 {
34    10
35}
36fn default_a2a_session_timeout() -> u64 {
37    3600
38}
39fn default_a2a_nonce_ttl() -> u64 {
40    7200
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SkillsConfig {
45    #[serde(default = "default_skills_dir")]
46    pub skills_dir: PathBuf,
47    #[serde(default = "default_script_timeout")]
48    pub script_timeout_seconds: u64,
49    #[serde(default = "default_script_max_output")]
50    pub script_max_output_bytes: usize,
51    #[serde(default = "default_interpreters")]
52    pub allowed_interpreters: Vec<String>,
53    #[serde(default = "default_true")]
54    pub sandbox_env: bool,
55    #[serde(default = "default_true")]
56    pub hot_reload: bool,
57    /// Maximum virtual memory (bytes) a skill script process may allocate.
58    /// Enforced via RLIMIT_AS on Unix. None = no limit. Default: 256 MiB.
59    #[serde(default = "default_script_max_memory")]
60    pub script_max_memory_bytes: Option<u64>,
61    /// Whether sandboxed scripts are allowed outbound network access.
62    /// When false the runner attempts platform-specific network isolation
63    /// (macOS sandbox-exec, Linux unshare). Default: false (deny by default).
64    #[serde(default)]
65    pub network_allowed: bool,
66    /// Optional workspace root exposed to scripts as $ROBOTICUS_WORKSPACE.
67    /// Scripts are confined to skills_dir for their own code, but may read/write
68    /// within this workspace path. If None, no workspace path is exposed.
69    #[serde(default)]
70    pub workspace_dir: Option<PathBuf>,
71}
72
73impl Default for SkillsConfig {
74    fn default() -> Self {
75        Self {
76            skills_dir: default_skills_dir(),
77            script_timeout_seconds: default_script_timeout(),
78            script_max_output_bytes: default_script_max_output(),
79            allowed_interpreters: default_interpreters(),
80            sandbox_env: true,
81            hot_reload: true,
82            script_max_memory_bytes: default_script_max_memory(),
83            network_allowed: false,
84            workspace_dir: None,
85        }
86    }
87}
88
89fn default_skills_dir() -> PathBuf {
90    dirs_next().join("skills")
91}
92fn default_script_timeout() -> u64 {
93    30
94}
95fn default_script_max_output() -> usize {
96    1_048_576
97}
98fn default_script_max_memory() -> Option<u64> {
99    Some(256 * 1024 * 1024) // 256 MiB
100}
101fn default_interpreters() -> Vec<String> {
102    #[cfg(windows)]
103    {
104        vec![
105            "bash".into(),
106            "python".into(),
107            "python3".into(),
108            "node".into(),
109        ]
110    }
111    #[cfg(not(windows))]
112    {
113        vec!["bash".into(), "python3".into(), "node".into()]
114    }
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118pub struct VoiceChannelConfig {
119    #[serde(default)]
120    pub enabled: bool,
121    #[serde(default)]
122    pub stt_model: Option<String>,
123    #[serde(default)]
124    pub tts_model: Option<String>,
125    #[serde(default)]
126    pub tts_voice: Option<String>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ChannelsConfig {
131    #[serde(default)]
132    pub telegram: Option<TelegramConfig>,
133    #[serde(default)]
134    pub whatsapp: Option<WhatsAppConfig>,
135    #[serde(default)]
136    pub discord: Option<DiscordConfig>,
137    #[serde(default)]
138    pub signal: Option<SignalConfig>,
139    #[serde(default)]
140    pub matrix: Option<MatrixConfig>,
141    #[serde(default)]
142    pub email: EmailConfig,
143    #[serde(default)]
144    pub voice: VoiceChannelConfig,
145    /// Sender IDs (chat IDs, phone numbers) trusted with Creator-level authority.
146    /// Messages from senders not in this list get External authority.
147    /// Empty list means all senders are treated as External.
148    #[serde(default)]
149    pub trusted_sender_ids: Vec<String>,
150    /// Estimated latency threshold (in seconds) above which a thinking indicator
151    /// (🤖🧠…) is sent before LLM inference on any chat channel. Set to 0 to
152    /// always send, or a very large value to effectively disable. Default: 30.
153    #[serde(default = "default_thinking_threshold")]
154    pub thinking_threshold_seconds: u64,
155    /// Optional list of channels that should receive a direct startup
156    /// announcement (for example: ["telegram", "whatsapp", "signal"]).
157    /// If unset/empty/false/"none"/"null", startup announcements are disabled.
158    #[serde(default)]
159    pub startup_announcements: Option<StartupAnnouncementsConfig>,
160}
161
162impl Default for ChannelsConfig {
163    fn default() -> Self {
164        Self {
165            telegram: None,
166            whatsapp: None,
167            discord: None,
168            signal: None,
169            matrix: None,
170            email: EmailConfig::default(),
171            voice: VoiceChannelConfig::default(),
172            trusted_sender_ids: Vec::new(),
173            thinking_threshold_seconds: default_thinking_threshold(),
174            startup_announcements: None,
175        }
176    }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(untagged)]
181pub enum StartupAnnouncementsConfig {
182    Flag(bool),
183    Text(String),
184    Channels(Vec<String>),
185}
186
187impl ChannelsConfig {
188    pub fn startup_announcement_channels(&self) -> Vec<String> {
189        fn normalize_channel(s: &str) -> Option<String> {
190            let v = s.trim().to_ascii_lowercase();
191            if v.is_empty() || v == "none" || v == "null" || v == "false" {
192                None
193            } else {
194                Some(v)
195            }
196        }
197
198        let mut out = match &self.startup_announcements {
199            None => Vec::new(),
200            Some(StartupAnnouncementsConfig::Flag(_)) => Vec::new(),
201            Some(StartupAnnouncementsConfig::Text(v)) => {
202                normalize_channel(v).map(|s| vec![s]).unwrap_or_default()
203            }
204            Some(StartupAnnouncementsConfig::Channels(v)) => {
205                v.iter().filter_map(|s| normalize_channel(s)).collect()
206            }
207        };
208        out.sort();
209        out.dedup();
210        out
211    }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct TelegramConfig {
216    #[serde(default = "default_true")]
217    pub enabled: bool,
218    #[serde(default)]
219    pub token_env: String,
220    #[serde(default)]
221    pub token_ref: Option<String>,
222    #[serde(default)]
223    pub allowed_chat_ids: Vec<i64>,
224    #[serde(default = "default_poll_timeout")]
225    pub poll_timeout_seconds: u64,
226    #[serde(default)]
227    pub webhook_mode: bool,
228    #[serde(default)]
229    pub webhook_path: Option<String>,
230    #[serde(default)]
231    pub webhook_secret: Option<String>,
232}
233
234fn default_thinking_threshold() -> u64 {
235    30
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct WhatsAppConfig {
240    #[serde(default)]
241    pub enabled: bool,
242    #[serde(default)]
243    pub token_env: String,
244    #[serde(default)]
245    pub token_ref: Option<String>,
246    #[serde(default)]
247    pub phone_number_id: String,
248    #[serde(default)]
249    pub verify_token: String,
250    #[serde(default)]
251    pub allowed_numbers: Vec<String>,
252    /// App secret for webhook X-Hub-Signature-256 verification (HMAC-SHA256).
253    #[serde(default)]
254    pub app_secret: Option<String>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct DiscordConfig {
259    #[serde(default = "default_true")]
260    pub enabled: bool,
261    #[serde(default)]
262    pub token_env: String,
263    #[serde(default)]
264    pub token_ref: Option<String>,
265    #[serde(default)]
266    pub application_id: String,
267    #[serde(default)]
268    pub allowed_guild_ids: Vec<String>,
269}
270
271/// Signal channel adapter configuration. Uses signal-cli's JSON-RPC daemon
272/// as a local relay for sending and receiving messages.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct SignalConfig {
275    #[serde(default)]
276    pub enabled: bool,
277    /// Phone number registered with signal-cli (e.g. "+15551234567").
278    #[serde(default)]
279    pub phone_number: String,
280    /// Base URL of the signal-cli JSON-RPC daemon (default: http://localhost:8080).
281    #[serde(default = "default_signal_daemon_url")]
282    pub daemon_url: String,
283    /// Contacts (phone numbers) allowed to talk to the agent. Empty = allow all.
284    #[serde(default)]
285    pub allowed_numbers: Vec<String>,
286}
287
288fn default_signal_daemon_url() -> String {
289    "http://localhost:8080".into()
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct MatrixConfig {
294    #[serde(default)]
295    pub enabled: bool,
296    /// Homeserver URL (e.g. `https://matrix.example.com`).
297    #[serde(default)]
298    pub homeserver_url: String,
299    /// Environment variable name holding the access token.
300    #[serde(default)]
301    pub access_token_env: String,
302    /// Room IDs the agent is allowed to participate in. Empty = allow all.
303    #[serde(default)]
304    pub allowed_rooms: Vec<String>,
305    /// Automatically join rooms when invited.
306    #[serde(default)]
307    pub auto_join: bool,
308    /// Sync timeout in seconds (long-poll duration).
309    #[serde(default = "default_matrix_sync_timeout")]
310    pub sync_timeout_seconds: u64,
311    /// Enable Olm/Megolm end-to-end encryption.
312    #[serde(default)]
313    pub encryption_enabled: bool,
314    /// Directory for device keys and session state. Defaults to
315    /// `~/.roboticus/matrix_crypto`.
316    #[serde(default)]
317    pub device_store_path: Option<PathBuf>,
318    /// Device display name advertised to other Matrix clients.
319    #[serde(default = "default_matrix_device_name")]
320    pub device_display_name: String,
321}
322
323fn default_matrix_sync_timeout() -> u64 {
324    30
325}
326
327fn default_matrix_device_name() -> String {
328    "Roboticus".into()
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct EmailConfig {
333    #[serde(default)]
334    pub enabled: bool,
335    #[serde(default)]
336    pub imap_host: String,
337    #[serde(default = "default_imap_port")]
338    pub imap_port: u16,
339    #[serde(default)]
340    pub smtp_host: String,
341    #[serde(default = "default_smtp_port")]
342    pub smtp_port: u16,
343    #[serde(default)]
344    pub username: String,
345    #[serde(default)]
346    pub password_env: String,
347    #[serde(default)]
348    pub from_address: String,
349    #[serde(default)]
350    pub allowed_senders: Vec<String>,
351    #[serde(default = "default_poll_interval")]
352    pub poll_interval_seconds: u64,
353    /// Environment variable name holding the OAuth2 access token (for Gmail XOAUTH2).
354    #[serde(default)]
355    pub oauth2_token_env: String,
356    /// Prefer XOAUTH2 authentication over password-based login.
357    #[serde(default)]
358    pub use_oauth2: bool,
359    /// Use IMAP IDLE for push notifications when the server supports it (default: true).
360    #[serde(default = "default_imap_idle_enabled")]
361    pub imap_idle_enabled: bool,
362}
363
364fn default_imap_idle_enabled() -> bool {
365    true
366}
367
368impl Default for EmailConfig {
369    fn default() -> Self {
370        Self {
371            enabled: false,
372            imap_host: String::new(),
373            imap_port: default_imap_port(),
374            smtp_host: String::new(),
375            smtp_port: default_smtp_port(),
376            username: String::new(),
377            password_env: String::new(),
378            from_address: String::new(),
379            allowed_senders: Vec::new(),
380            poll_interval_seconds: default_poll_interval(),
381            oauth2_token_env: String::new(),
382            use_oauth2: false,
383            imap_idle_enabled: default_imap_idle_enabled(),
384        }
385    }
386}
387
388fn default_imap_port() -> u16 {
389    993
390}
391fn default_smtp_port() -> u16 {
392    587
393}
394fn default_poll_interval() -> u64 {
395    30
396}
397
398fn default_poll_timeout() -> u64 {
399    30
400}
401