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 email: EmailConfig,
141    #[serde(default)]
142    pub voice: VoiceChannelConfig,
143    /// Sender IDs (chat IDs, phone numbers) trusted with Creator-level authority.
144    /// Messages from senders not in this list get External authority.
145    /// Empty list means all senders are treated as External.
146    #[serde(default)]
147    pub trusted_sender_ids: Vec<String>,
148    /// Estimated latency threshold (in seconds) above which a thinking indicator
149    /// (🤖🧠…) is sent before LLM inference on any chat channel. Set to 0 to
150    /// always send, or a very large value to effectively disable. Default: 30.
151    #[serde(default = "default_thinking_threshold")]
152    pub thinking_threshold_seconds: u64,
153    /// Optional list of channels that should receive a direct startup
154    /// announcement (for example: ["telegram", "whatsapp", "signal"]).
155    /// If unset/empty/false/"none"/"null", startup announcements are disabled.
156    #[serde(default)]
157    pub startup_announcements: Option<StartupAnnouncementsConfig>,
158}
159
160impl Default for ChannelsConfig {
161    fn default() -> Self {
162        Self {
163            telegram: None,
164            whatsapp: None,
165            discord: None,
166            signal: None,
167            email: EmailConfig::default(),
168            voice: VoiceChannelConfig::default(),
169            trusted_sender_ids: Vec::new(),
170            thinking_threshold_seconds: default_thinking_threshold(),
171            startup_announcements: None,
172        }
173    }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(untagged)]
178pub enum StartupAnnouncementsConfig {
179    Flag(bool),
180    Text(String),
181    Channels(Vec<String>),
182}
183
184impl ChannelsConfig {
185    pub fn startup_announcement_channels(&self) -> Vec<String> {
186        fn normalize_channel(s: &str) -> Option<String> {
187            let v = s.trim().to_ascii_lowercase();
188            if v.is_empty() || v == "none" || v == "null" || v == "false" {
189                None
190            } else {
191                Some(v)
192            }
193        }
194
195        let mut out = match &self.startup_announcements {
196            None => Vec::new(),
197            Some(StartupAnnouncementsConfig::Flag(_)) => Vec::new(),
198            Some(StartupAnnouncementsConfig::Text(v)) => {
199                normalize_channel(v).map(|s| vec![s]).unwrap_or_default()
200            }
201            Some(StartupAnnouncementsConfig::Channels(v)) => {
202                v.iter().filter_map(|s| normalize_channel(s)).collect()
203            }
204        };
205        out.sort();
206        out.dedup();
207        out
208    }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct TelegramConfig {
213    #[serde(default = "default_true")]
214    pub enabled: bool,
215    #[serde(default)]
216    pub token_env: String,
217    #[serde(default)]
218    pub token_ref: Option<String>,
219    #[serde(default)]
220    pub allowed_chat_ids: Vec<i64>,
221    #[serde(default = "default_poll_timeout")]
222    pub poll_timeout_seconds: u64,
223    #[serde(default)]
224    pub webhook_mode: bool,
225    #[serde(default)]
226    pub webhook_path: Option<String>,
227    #[serde(default)]
228    pub webhook_secret: Option<String>,
229}
230
231fn default_thinking_threshold() -> u64 {
232    30
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct WhatsAppConfig {
237    #[serde(default)]
238    pub enabled: bool,
239    #[serde(default)]
240    pub token_env: String,
241    #[serde(default)]
242    pub token_ref: Option<String>,
243    #[serde(default)]
244    pub phone_number_id: String,
245    #[serde(default)]
246    pub verify_token: String,
247    #[serde(default)]
248    pub allowed_numbers: Vec<String>,
249    /// App secret for webhook X-Hub-Signature-256 verification (HMAC-SHA256).
250    #[serde(default)]
251    pub app_secret: Option<String>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct DiscordConfig {
256    #[serde(default = "default_true")]
257    pub enabled: bool,
258    #[serde(default)]
259    pub token_env: String,
260    #[serde(default)]
261    pub token_ref: Option<String>,
262    #[serde(default)]
263    pub application_id: String,
264    #[serde(default)]
265    pub allowed_guild_ids: Vec<String>,
266}
267
268/// Signal channel adapter configuration. Uses signal-cli's JSON-RPC daemon
269/// as a local relay for sending and receiving messages.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct SignalConfig {
272    #[serde(default)]
273    pub enabled: bool,
274    /// Phone number registered with signal-cli (e.g. "+15551234567").
275    #[serde(default)]
276    pub phone_number: String,
277    /// Base URL of the signal-cli JSON-RPC daemon (default: http://127.0.0.1:8080).
278    #[serde(default = "default_signal_daemon_url")]
279    pub daemon_url: String,
280    /// Contacts (phone numbers) allowed to talk to the agent. Empty = allow all.
281    #[serde(default)]
282    pub allowed_numbers: Vec<String>,
283}
284
285fn default_signal_daemon_url() -> String {
286    "http://127.0.0.1:8080".into()
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct EmailConfig {
291    #[serde(default)]
292    pub enabled: bool,
293    #[serde(default)]
294    pub imap_host: String,
295    #[serde(default = "default_imap_port")]
296    pub imap_port: u16,
297    #[serde(default)]
298    pub smtp_host: String,
299    #[serde(default = "default_smtp_port")]
300    pub smtp_port: u16,
301    #[serde(default)]
302    pub username: String,
303    #[serde(default)]
304    pub password_env: String,
305    #[serde(default)]
306    pub from_address: String,
307    #[serde(default)]
308    pub allowed_senders: Vec<String>,
309    #[serde(default = "default_poll_interval")]
310    pub poll_interval_seconds: u64,
311    /// Environment variable name holding the OAuth2 access token (for Gmail XOAUTH2).
312    #[serde(default)]
313    pub oauth2_token_env: String,
314    /// Prefer XOAUTH2 authentication over password-based login.
315    #[serde(default)]
316    pub use_oauth2: bool,
317    /// Use IMAP IDLE for push notifications when the server supports it (default: true).
318    #[serde(default = "default_imap_idle_enabled")]
319    pub imap_idle_enabled: bool,
320}
321
322fn default_imap_idle_enabled() -> bool {
323    true
324}
325
326impl Default for EmailConfig {
327    fn default() -> Self {
328        Self {
329            enabled: false,
330            imap_host: String::new(),
331            imap_port: default_imap_port(),
332            smtp_host: String::new(),
333            smtp_port: default_smtp_port(),
334            username: String::new(),
335            password_env: String::new(),
336            from_address: String::new(),
337            allowed_senders: Vec::new(),
338            poll_interval_seconds: default_poll_interval(),
339            oauth2_token_env: String::new(),
340            use_oauth2: false,
341            imap_idle_enabled: default_imap_idle_enabled(),
342        }
343    }
344}
345
346fn default_imap_port() -> u16 {
347    993
348}
349fn default_smtp_port() -> u16 {
350    587
351}
352fn default_poll_interval() -> u64 {
353    30
354}
355
356fn default_poll_timeout() -> u64 {
357    30
358}
359