gemini_adk_rs/plugin/
security.rs1use async_trait::async_trait;
4
5use gemini_genai_rs::prelude::FunctionCall;
6
7use super::{Plugin, PluginResult};
8use crate::context::InvocationContext;
9
10#[derive(Debug, Clone)]
12pub enum PolicyOutcome {
13 Allow,
15 Confirm(String),
17 Deny(String),
19}
20
21pub trait PolicyEngine: Send + Sync + 'static {
26 fn evaluate(&self, tool_name: &str, args: &serde_json::Value) -> PolicyOutcome;
28}
29
30pub struct SecurityPlugin {
37 engine: Box<dyn PolicyEngine>,
38}
39
40impl SecurityPlugin {
41 pub fn new(engine: impl PolicyEngine + 'static) -> Self {
43 Self {
44 engine: Box::new(engine),
45 }
46 }
47}
48
49#[async_trait]
50impl Plugin for SecurityPlugin {
51 fn name(&self) -> &str {
52 "security"
53 }
54
55 async fn before_tool(&self, call: &FunctionCall, _ctx: &InvocationContext) -> PluginResult {
56 match self.engine.evaluate(&call.name, &call.args) {
57 PolicyOutcome::Allow => {
58 tracing::debug!(tool = %call.name, "[plugin:security] Tool call allowed");
59 PluginResult::Continue
60 }
61 PolicyOutcome::Confirm(msg) => {
62 tracing::warn!(tool = %call.name, reason = %msg, "[plugin:security] Tool call requires confirmation");
63 PluginResult::Deny(format!("Confirmation required: {msg}"))
64 }
65 PolicyOutcome::Deny(reason) => {
66 tracing::warn!(tool = %call.name, reason = %reason, "[plugin:security] Tool call denied");
67 PluginResult::Deny(reason)
68 }
69 }
70 }
71}
72
73pub struct DenyListPolicy {
75 blocked_tools: Vec<String>,
76}
77
78impl DenyListPolicy {
79 pub fn new(blocked_tools: Vec<String>) -> Self {
81 Self { blocked_tools }
82 }
83}
84
85impl PolicyEngine for DenyListPolicy {
86 fn evaluate(&self, tool_name: &str, _args: &serde_json::Value) -> PolicyOutcome {
87 if self.blocked_tools.iter().any(|t| t == tool_name) {
88 PolicyOutcome::Deny(format!("Tool '{tool_name}' is blocked by policy"))
89 } else {
90 PolicyOutcome::Allow
91 }
92 }
93}
94
95pub struct AllowAllPolicy;
97
98impl PolicyEngine for AllowAllPolicy {
99 fn evaluate(&self, _tool_name: &str, _args: &serde_json::Value) -> PolicyOutcome {
100 PolicyOutcome::Allow
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn deny_list_policy_blocks() {
110 let policy = DenyListPolicy::new(vec!["dangerous_tool".into()]);
111 let result = policy.evaluate("dangerous_tool", &serde_json::json!({}));
112 assert!(matches!(result, PolicyOutcome::Deny(_)));
113 }
114
115 #[test]
116 fn deny_list_policy_allows() {
117 let policy = DenyListPolicy::new(vec!["dangerous_tool".into()]);
118 let result = policy.evaluate("safe_tool", &serde_json::json!({}));
119 assert!(matches!(result, PolicyOutcome::Allow));
120 }
121
122 #[test]
123 fn allow_all_policy() {
124 let policy = AllowAllPolicy;
125 let result = policy.evaluate("anything", &serde_json::json!({}));
126 assert!(matches!(result, PolicyOutcome::Allow));
127 }
128
129 #[tokio::test]
130 async fn security_plugin_denies_blocked_tool() {
131 use tokio::sync::broadcast;
132
133 let policy = DenyListPolicy::new(vec!["rm_rf".into()]);
134 let plugin = SecurityPlugin::new(policy);
135
136 let (evt_tx, _) = broadcast::channel(16);
137 let writer: std::sync::Arc<dyn gemini_genai_rs::session::SessionWriter> =
138 std::sync::Arc::new(crate::test_helpers::MockWriter);
139 let session = crate::agent_session::AgentSession::from_writer(writer, evt_tx);
140 let ctx = InvocationContext::new(session);
141
142 let call = FunctionCall {
143 name: "rm_rf".into(),
144 args: serde_json::json!({}),
145 id: None,
146 };
147
148 let result = plugin.before_tool(&call, &ctx).await;
149 assert!(result.is_deny());
150 }
151
152 #[tokio::test]
153 async fn security_plugin_allows_safe_tool() {
154 use tokio::sync::broadcast;
155
156 let policy = DenyListPolicy::new(vec!["rm_rf".into()]);
157 let plugin = SecurityPlugin::new(policy);
158
159 let (evt_tx, _) = broadcast::channel(16);
160 let writer: std::sync::Arc<dyn gemini_genai_rs::session::SessionWriter> =
161 std::sync::Arc::new(crate::test_helpers::MockWriter);
162 let session = crate::agent_session::AgentSession::from_writer(writer, evt_tx);
163 let ctx = InvocationContext::new(session);
164
165 let call = FunctionCall {
166 name: "get_weather".into(),
167 args: serde_json::json!({}),
168 id: None,
169 };
170
171 let result = plugin.before_tool(&call, &ctx).await;
172 assert!(result.is_continue());
173 }
174}