Skip to main content

secureops_core/
context.rs

1//! The `AuditContext` trait — dependency injection for every environment touch.
2//!
3//! Port of the `AuditContext` interface in `src/types.ts`. Checks receive
4//! `&dyn AuditContext` and never touch the filesystem directly, so they stay
5//! unit-testable against an in-memory mock. The real `tokio::fs`-backed impl
6//! lives in the `secureops-fs` crate (Ring 0/1); the daemon (Ring 2) supplies
7//! its own. Keeping I/O behind this trait is what lets `core` and `checks` stay
8//! I/O-free per PRODUCT.md A.4.
9
10use crate::config::{DockerComposeConfig, OpenClawConfig};
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13
14/// File info for auditing (permissions, content, existence, size).
15#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
16#[serde(rename_all = "camelCase")]
17pub struct FileInfo {
18    pub path: String,
19    #[serde(skip_serializing_if = "Option::is_none", default)]
20    pub permissions: Option<u32>,
21    #[serde(skip_serializing_if = "Option::is_none", default)]
22    pub content: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none", default)]
24    pub exists: Option<bool>,
25    #[serde(skip_serializing_if = "Option::is_none", default)]
26    pub size: Option<u64>,
27}
28
29/// Channel configuration (Slack/Discord-style routing surface).
30#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
31#[serde(rename_all = "camelCase")]
32pub struct ChannelConfig {
33    pub name: String,
34    #[serde(skip_serializing_if = "Option::is_none", default)]
35    pub dm_policy: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none", default)]
37    pub group_policy: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none", default)]
39    pub allowlist: Option<Vec<String>>,
40}
41
42/// Skill metadata used by the supply-chain / skill-scan checks.
43#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct SkillMetadata {
46    pub name: String,
47    #[serde(skip_serializing_if = "Option::is_none", default)]
48    pub source: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none", default)]
50    pub github_account_age: Option<i64>,
51    #[serde(skip_serializing_if = "Option::is_none", default)]
52    pub installed_at: Option<String>,
53}
54
55/// Dependency-injected view of the host the agent runs on.
56///
57/// Sync getters expose already-loaded config/metadata; async methods perform
58/// the actual filesystem reads (mocked in tests, `tokio::fs` in production).
59#[async_trait]
60pub trait AuditContext: Send + Sync {
61    fn state_dir(&self) -> &str;
62    fn config(&self) -> &OpenClawConfig;
63    fn platform(&self) -> &str;
64    fn deployment_mode(&self) -> &str;
65    fn openclaw_version(&self) -> &str;
66
67    async fn file_info(&self, path: &str) -> FileInfo;
68    async fn read_file(&self, path: &str) -> Option<String>;
69    async fn list_dir(&self, path: &str) -> Vec<String>;
70    async fn file_exists(&self, path: &str) -> bool;
71    /// Unix mode bits (e.g. `0o600`), or `None` when unavailable.
72    async fn get_file_permissions(&self, path: &str) -> Option<u32>;
73
74    fn channels(&self) -> &[ChannelConfig] {
75        &[]
76    }
77    fn skills(&self) -> &[SkillMetadata] {
78        &[]
79    }
80    fn docker_compose(&self) -> Option<&DockerComposeConfig> {
81        None
82    }
83    fn session_logs(&self) -> &[String] {
84        &[]
85    }
86    fn connection_logs(&self) -> &[String] {
87        &[]
88    }
89}