Skip to main content

oxios_kernel/tools/builtin/
security_tool.rs

1//! Security tool — wraps `SecurityApi` audit methods behind the `AgentTool` interface.
2//!
3//! Provides agents with security audit query capabilities.
4//! Actions: verify_chain, query_audit, audit_count.
5//!
6//! ## Example
7//!
8//! ```json
9//! { "action": "verify_chain" }
10//! { "action": "query_audit", "from_seq": 0, "to_seq": 100 }
11//! { "action": "audit_count" }
12//! ```
13
14use std::sync::Arc;
15
16use async_trait::async_trait;
17use oxi_sdk::observability::AuditTrail;
18use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
19use serde_json::{json, Value};
20use tokio::sync::oneshot;
21
22use crate::kernel_handle::KernelHandle;
23
24/// Agent tool for security audit operations.
25///
26/// Wraps the audit-related methods of the `SecurityApi` domain. Allows agents
27/// to verify audit chain integrity, query audit entries, and check entry count.
28///
29/// ## Actions
30///
31/// | Action          | Description                   | Required params | Optional params           |
32/// |-----------------|-------------------------------|-----------------|---------------------------|
33/// | `verify_chain`  | Verify audit chain integrity  | —               | —                         |
34/// | `query_audit`   | Query audit entries by range  | —               | `from_seq`, `to_seq`      |
35/// | `audit_count`   | Get total audit entry count   | —               | —                         |
36pub struct SecurityTool {
37    audit_trail: Arc<AuditTrail>,
38}
39
40impl SecurityTool {
41    /// Create a new `SecurityTool` from a `KernelHandle`.
42    ///
43    /// Extracts the `AuditTrail` Arc from the kernel's Security API.
44    pub fn from_kernel(kernel: &KernelHandle) -> Self {
45        Self {
46            audit_trail: kernel.security.audit_trail.clone(),
47        }
48    }
49}
50
51impl std::fmt::Debug for SecurityTool {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("SecurityTool").finish()
54    }
55}
56
57#[async_trait]
58impl AgentTool for SecurityTool {
59    fn name(&self) -> &str {
60        "security"
61    }
62
63    fn label(&self) -> &str {
64        "Security"
65    }
66
67    fn description(&self) -> &'static str {
68        "Query security audit trail — verify chain integrity, list entries, check count. \
69         Actions: verify_chain, query_audit, audit_count."
70    }
71
72    fn parameters_schema(&self) -> Value {
73        json!({
74            "type": "object",
75            "properties": {
76                "action": {
77                    "type": "string",
78                    "enum": ["verify_chain", "query_audit", "audit_count"],
79                    "description": "Security operation to perform"
80                },
81                "from_seq": {
82                    "type": "integer",
83                    "description": "Start sequence number for query_audit (default: 0)"
84                },
85                "to_seq": {
86                    "type": "integer",
87                    "description": "End sequence number for query_audit (default: latest)"
88                }
89            },
90            "required": ["action"]
91        })
92    }
93
94    async fn execute(
95        &self,
96        _tool_call_id: &str,
97        params: Value,
98        _signal: Option<oneshot::Receiver<()>>,
99        _ctx: &ToolContext,
100    ) -> Result<AgentToolResult, String> {
101        let action = params
102            .get("action")
103            .and_then(|v| v.as_str())
104            .ok_or_else(|| "Missing required parameter: action".to_string())?;
105
106        match action {
107            "verify_chain" => match self.audit_trail.verify() {
108                Ok(valid) => Ok(AgentToolResult::success(
109                    serde_json::to_string(&json!({
110                        "chain_integrity": valid,
111                        "status": if valid { "intact" } else { "TAMPERED" },
112                    }))
113                    .unwrap_or_default(),
114                )),
115                Err(e) => Ok(AgentToolResult::error(format!(
116                    "Chain verification failed: {e:?}"
117                ))),
118            },
119
120            "query_audit" => {
121                let from_seq = params["from_seq"].as_u64().unwrap_or(0);
122                let to_seq = params["to_seq"].as_u64().unwrap_or(u64::MAX);
123
124                let entries = self.audit_trail.entries(from_seq, to_seq);
125                if entries.is_empty() {
126                    return Ok(AgentToolResult::success(
127                        "No audit entries found in the specified range.",
128                    ));
129                }
130
131                let display: Vec<Value> = entries
132                    .iter()
133                    .map(|entry| {
134                        json!({
135                            "seq": entry.seq,
136                            "actor": entry.actor,
137                            "action": format!("{:?}", entry.action),
138                            "resource": entry.resource,
139                            "timestamp": entry.timestamp,
140                        })
141                    })
142                    .collect();
143
144                Ok(AgentToolResult::success(
145                    serde_json::to_string_pretty(&json!({
146                        "entries": display,
147                        "count": display.len(),
148                    }))
149                    .unwrap_or_default(),
150                ))
151            }
152
153            "audit_count" => {
154                let count = self.audit_trail.len();
155                Ok(AgentToolResult::success(
156                    serde_json::to_string(&json!({ "audit_entry_count": count }))
157                        .unwrap_or_default(),
158                ))
159            }
160
161            other => Err(format!(
162                "Unknown security action '{other}'. Valid: verify_chain, query_audit, audit_count"
163            )),
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_schema_structure() {
174        let schema = json!({
175            "type": "object",
176            "properties": {
177                "action": {
178                    "type": "string",
179                    "enum": ["verify_chain", "query_audit", "audit_count"]
180                },
181                "from_seq": { "type": "integer" },
182                "to_seq": { "type": "integer" }
183            },
184            "required": ["action"]
185        });
186
187        let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
188        assert_eq!(actions.len(), 3);
189        assert!(actions.iter().any(|a| a == "verify_chain"));
190        assert!(actions.iter().any(|a| a == "query_audit"));
191        assert!(actions.iter().any(|a| a == "audit_count"));
192    }
193}