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    /// Parse `openclaw.json` content, falling back to [`OpenClawConfig::default`]
273    /// on missing or malformed input - the shared parse-or-default contract used
274    /// by every loader (CLI, napi, harden, daemon). Pass the file content (or
275    /// an empty string when the file is absent); the result is identical to the
276    /// TS tool's `try { JSON.parse(...) } catch { {} }`.
277    pub fn from_json_or_default(content: &str) -> Self {
278        serde_json::from_str(content).unwrap_or_default()
279    }
280
281    /// Active failure mode (directive G4), defaulting to `block_all`
282    /// (port of `getFailureMode`).
283    pub fn failure_mode(&self) -> FailureMode {
284        self.secureops
285            .as_ref()
286            .and_then(|s| s.failure_mode)
287            .unwrap_or(FailureMode::BlockAll)
288    }
289
290    /// Active risk profile (directive G8), defaulting to `standard`
291    /// (port of `getRiskProfile`).
292    pub fn risk_profile(&self) -> RiskProfile {
293        self.secureops
294            .as_ref()
295            .and_then(|s| s.risk_profile)
296            .unwrap_or(RiskProfile::Standard)
297    }
298}
299
300// ---- Docker compose model (audited by the supply-chain / docker checks) ----
301
302#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
303#[serde(rename_all = "snake_case", default)]
304pub struct DockerServiceConfig {
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub read_only: Option<bool>,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub cap_drop: Option<Vec<String>>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub security_opt: Option<Vec<String>>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub networks: Option<Vec<String>>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub network_mode: Option<String>,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub volumes: Option<Vec<String>>,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub deploy: Option<DockerDeploy>,
319}
320
321#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
322#[serde(rename_all = "snake_case", default)]
323pub struct DockerDeploy {
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub resources: Option<DockerResources>,
326}
327
328#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
329#[serde(rename_all = "snake_case", default)]
330pub struct DockerResources {
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub limits: Option<DockerLimits>,
333}
334
335#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
336#[serde(rename_all = "snake_case", default)]
337pub struct DockerLimits {
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub memory: Option<String>,
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub cpus: Option<String>,
342}
343
344#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
345#[serde(rename_all = "snake_case", default)]
346pub struct DockerNetwork {
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub driver: Option<String>,
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub internal: Option<bool>,
351}
352
353#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
354#[serde(rename_all = "snake_case", default)]
355pub struct DockerComposeConfig {
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub services: Option<HashMap<String, DockerServiceConfig>>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub networks: Option<HashMap<String, DockerNetwork>>,
360}