Skip to main content

secureops_core/
config.rs

1//! The OpenClaw configuration tree that SecureOps audits.
2//!
3//! Faithful port of the config interfaces in `src/types.ts`. Every field is
4//! optional (matching the TS `?` optionals) and skipped from JSON when `None`,
5//! so a round-trip through this model never injects keys the TS tool wouldn't.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Gateway configuration.
11#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
12#[serde(rename_all = "camelCase", default)]
13pub struct GatewayConfig {
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub bind: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub port: Option<u16>,
18    /// Legacy flat auth token (pre-2026 configs).
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub auth_token: Option<String>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub auth: Option<GatewayAuth>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub tls: Option<TlsConfig>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub mdns: Option<MdnsConfig>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub control_ui: Option<ControlUiConfig>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub trusted_proxies: Option<Vec<String>>,
31}
32
33#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
34#[serde(rename_all = "camelCase", default)]
35pub struct GatewayAuth {
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub mode: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub token: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub password: Option<String>,
42}
43
44#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
45#[serde(rename_all = "camelCase", default)]
46pub struct TlsConfig {
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub enabled: Option<bool>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub cert: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub key: Option<String>,
53}
54
55#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
56#[serde(rename_all = "camelCase", default)]
57pub struct MdnsConfig {
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub mode: Option<String>,
60}
61
62#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
63#[serde(rename_all = "camelCase", default)]
64pub struct ControlUiConfig {
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub dangerously_disable_device_auth: Option<bool>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub allow_insecure_auth: Option<bool>,
69}
70
71/// Execution configuration.
72#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
73#[serde(rename_all = "camelCase", default)]
74pub struct ExecConfig {
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub approvals: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub auto_approve: Option<Vec<String>>,
79}
80
81/// Sandbox configuration.
82#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
83#[serde(rename_all = "camelCase", default)]
84pub struct SandboxConfig {
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub mode: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub scope: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub workspace_access: Option<String>,
91}
92
93/// Tools configuration.
94#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
95#[serde(rename_all = "camelCase", default)]
96pub struct ToolsConfig {
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub exec: Option<ToolsExec>,
99}
100
101#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
102#[serde(rename_all = "camelCase", default)]
103pub struct ToolsExec {
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub host: Option<String>,
106}
107
108/// Session configuration.
109#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
110#[serde(rename_all = "camelCase", default)]
111pub struct SessionConfig {
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub dm_scope: Option<String>,
114}
115
116/// Logging configuration.
117#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
118#[serde(rename_all = "camelCase", default)]
119pub struct LoggingConfig {
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub redact_sensitive: Option<String>,
122}
123
124/// Failure mode for graceful degradation (directive G4).
125#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
126#[serde(rename_all = "snake_case")]
127pub enum FailureMode {
128    BlockAll,
129    SafeMode,
130    ReadOnly,
131}
132
133/// Risk profile names for per-workload security (directive G8).
134#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
135#[serde(rename_all = "lowercase")]
136pub enum RiskProfile {
137    Strict,
138    Standard,
139    Permissive,
140}
141
142/// SecureOps-specific configuration block.
143#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
144#[serde(rename_all = "camelCase", default)]
145pub struct SecureOpsConfig {
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub monitors: Option<MonitorsToggle>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub cost: Option<CostLimits>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub memory: Option<MemorySettings>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub skills: Option<SkillsSettings>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub network: Option<NetworkSettings>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub failure_mode: Option<FailureMode>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub risk_profile: Option<RiskProfile>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub risk_profiles: Option<HashMap<String, RiskProfileDef>>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub behavioral: Option<BehavioralSettings>,
164}
165
166#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
167#[serde(rename_all = "camelCase", default)]
168pub struct MonitorsToggle {
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub credentials: Option<bool>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub memory: Option<bool>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub skills: Option<bool>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub cost: Option<bool>,
177}
178
179#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
180#[serde(rename_all = "camelCase", default)]
181pub struct CostLimits {
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub hourly_limit_usd: Option<f64>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub daily_limit_usd: Option<f64>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub monthly_limit_usd: Option<f64>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub circuit_breaker_enabled: Option<bool>,
190}
191
192#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
193#[serde(rename_all = "camelCase", default)]
194pub struct MemorySettings {
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub integrity_checks: Option<bool>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub prompt_injection_scan: Option<bool>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub quarantine_enabled: Option<bool>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub trust_levels: Option<bool>,
203}
204
205#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
206#[serde(rename_all = "camelCase", default)]
207pub struct SkillsSettings {
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub block_unaudited: Option<bool>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub scan_on_install: Option<bool>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub ioc_check_enabled: Option<bool>,
214}
215
216#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
217#[serde(rename_all = "camelCase", default)]
218pub struct NetworkSettings {
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub egress_allowlist_enabled: Option<bool>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub egress_allowlist: Option<Vec<String>>,
223}
224
225#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
226#[serde(rename_all = "camelCase", default)]
227pub struct RiskProfileDef {
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub failure_mode: Option<FailureMode>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub approval_required: Option<bool>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub allowed_tools: Option<Vec<String>>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub blocked_tools: Option<Vec<String>>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub max_cost_per_session: Option<f64>,
238}
239
240#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
241#[serde(rename_all = "camelCase", default)]
242pub struct BehavioralSettings {
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub baseline_enabled: Option<bool>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub deviation_threshold: Option<f64>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub window_minutes: Option<u64>,
249}
250
251/// Full OpenClaw configuration.
252#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
253#[serde(rename_all = "camelCase", default)]
254pub struct OpenClawConfig {
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub gateway: Option<GatewayConfig>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub exec: Option<ExecConfig>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub sandbox: Option<SandboxConfig>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub tools: Option<ToolsConfig>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub session: Option<SessionConfig>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub logging: Option<LoggingConfig>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub secureops: Option<SecureOpsConfig>,
269}
270
271impl OpenClawConfig {
272    /// Active failure mode (directive G4), defaulting to `block_all`
273    /// (port of `getFailureMode`).
274    pub fn failure_mode(&self) -> FailureMode {
275        self.secureops
276            .as_ref()
277            .and_then(|s| s.failure_mode)
278            .unwrap_or(FailureMode::BlockAll)
279    }
280
281    /// Active risk profile (directive G8), defaulting to `standard`
282    /// (port of `getRiskProfile`).
283    pub fn risk_profile(&self) -> RiskProfile {
284        self.secureops
285            .as_ref()
286            .and_then(|s| s.risk_profile)
287            .unwrap_or(RiskProfile::Standard)
288    }
289}
290
291// ---- Docker compose model (audited by the supply-chain / docker checks) ----
292
293#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
294#[serde(rename_all = "snake_case", default)]
295pub struct DockerServiceConfig {
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub read_only: Option<bool>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub cap_drop: Option<Vec<String>>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub security_opt: Option<Vec<String>>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub networks: Option<Vec<String>>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub network_mode: Option<String>,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub volumes: Option<Vec<String>>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub deploy: Option<DockerDeploy>,
310}
311
312#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
313#[serde(rename_all = "snake_case", default)]
314pub struct DockerDeploy {
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub resources: Option<DockerResources>,
317}
318
319#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
320#[serde(rename_all = "snake_case", default)]
321pub struct DockerResources {
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub limits: Option<DockerLimits>,
324}
325
326#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
327#[serde(rename_all = "snake_case", default)]
328pub struct DockerLimits {
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub memory: Option<String>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub cpus: Option<String>,
333}
334
335#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
336#[serde(rename_all = "snake_case", default)]
337pub struct DockerNetwork {
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub driver: Option<String>,
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub internal: Option<bool>,
342}
343
344#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
345#[serde(rename_all = "snake_case", default)]
346pub struct DockerComposeConfig {
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub services: Option<HashMap<String, DockerServiceConfig>>,
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub networks: Option<HashMap<String, DockerNetwork>>,
351}