chio_guards/
input_injection.rs1use std::collections::HashSet;
28
29use serde::{Deserialize, Serialize};
30use serde_json::Value;
31
32use chio_kernel::{Guard, GuardContext, GuardDecision, KernelError, Verdict};
33
34pub fn default_allowed_input_types() -> Vec<String> {
36 vec![
37 "keyboard".to_string(),
38 "mouse".to_string(),
39 "touch".to_string(),
40 ]
41}
42
43#[derive(Clone, Debug, Deserialize, Serialize)]
45#[serde(deny_unknown_fields)]
46pub struct InputInjectionCapabilityConfig {
47 #[serde(default = "default_true")]
49 pub enabled: bool,
50 #[serde(default = "default_allowed_input_types")]
52 pub allowed_input_types: Vec<String>,
53 #[serde(default)]
56 pub require_postcondition_probe: bool,
57 #[serde(default = "default_true")]
63 pub strict: bool,
64}
65
66fn default_true() -> bool {
67 true
68}
69
70impl Default for InputInjectionCapabilityConfig {
71 fn default() -> Self {
72 Self {
73 enabled: true,
74 allowed_input_types: default_allowed_input_types(),
75 require_postcondition_probe: false,
76 strict: true,
77 }
78 }
79}
80
81pub struct InputInjectionCapabilityGuard {
83 enabled: bool,
84 allowed_types: HashSet<String>,
85 require_postcondition_probe: bool,
86 strict: bool,
87}
88
89impl InputInjectionCapabilityGuard {
90 pub fn new() -> Self {
92 Self::with_config(InputInjectionCapabilityConfig::default())
93 }
94
95 pub fn with_config(config: InputInjectionCapabilityConfig) -> Self {
97 Self {
98 enabled: config.enabled,
99 allowed_types: config.allowed_input_types.into_iter().collect(),
100 require_postcondition_probe: config.require_postcondition_probe,
101 strict: config.strict,
102 }
103 }
104
105 fn is_injection(tool_name: &str, arguments: &Value) -> bool {
107 if tool_name == "input.inject" || tool_name == "input_inject" {
108 return true;
109 }
110 for key in ["action_type", "actionType", "custom_type", "customType"] {
111 if let Some(v) = arguments.get(key).and_then(|v| v.as_str()) {
112 if v == "input.inject" {
113 return true;
114 }
115 }
116 }
117 arguments
121 .get("input_type")
122 .or_else(|| arguments.get("inputType"))
123 .and_then(|v| v.as_str())
124 .is_some()
125 && (tool_name == "keyboard"
126 || tool_name == "mouse"
127 || tool_name == "touch"
128 || tool_name == "input")
129 }
130
131 fn input_type(arguments: &Value) -> Option<&str> {
133 arguments
134 .get("input_type")
135 .or_else(|| arguments.get("inputType"))
136 .and_then(|v| v.as_str())
137 }
138
139 fn has_postcondition_probe(arguments: &Value) -> bool {
141 arguments
142 .get("postcondition_probe_hash")
143 .or_else(|| arguments.get("postconditionProbeHash"))
144 .and_then(|v| v.as_str())
145 .is_some_and(|s| !s.is_empty())
146 }
147}
148
149impl Default for InputInjectionCapabilityGuard {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl Guard for InputInjectionCapabilityGuard {
156 fn name(&self) -> &str {
157 "input-injection-capability"
158 }
159
160 fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError> {
161 if !self.enabled {
162 return Ok(GuardDecision::allow());
163 }
164
165 if !Self::is_injection(&ctx.request.tool_name, &ctx.request.arguments) {
166 return Ok(GuardDecision::allow());
167 }
168
169 match Self::input_type(&ctx.request.arguments) {
171 Some(it) => {
172 if !self.allowed_types.contains(it) {
173 return Ok(GuardDecision::deny(Vec::new()));
174 }
175 }
176 None => {
177 return Ok(GuardDecision::from_verdict(if self.strict {
179 Verdict::Deny
180 } else {
181 Verdict::Allow
182 }));
183 }
184 }
185
186 if self.require_postcondition_probe
188 && !Self::has_postcondition_probe(&ctx.request.arguments)
189 {
190 return Ok(GuardDecision::deny(Vec::new()));
191 }
192
193 Ok(GuardDecision::allow())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn detects_explicit_input_inject_tool() {
203 let args = serde_json::json!({"input_type": "keyboard"});
204 assert!(InputInjectionCapabilityGuard::is_injection(
205 "input.inject",
206 &args
207 ));
208 }
209
210 #[test]
211 fn detects_action_type_argument() {
212 let args = serde_json::json!({"action_type": "input.inject", "input_type": "mouse"});
213 assert!(InputInjectionCapabilityGuard::is_injection(
214 "generic", &args
215 ));
216 }
217
218 #[test]
219 fn ignores_unrelated_tools() {
220 let args = serde_json::json!({"path": "/tmp/x"});
221 assert!(!InputInjectionCapabilityGuard::is_injection(
222 "read_file",
223 &args
224 ));
225 }
226
227 #[test]
228 fn input_type_accepts_camel_case() {
229 let args = serde_json::json!({"inputType": "keyboard"});
230 assert_eq!(
231 InputInjectionCapabilityGuard::input_type(&args),
232 Some("keyboard")
233 );
234 }
235
236 #[test]
237 fn postcondition_probe_detected_both_cases() {
238 let snake = serde_json::json!({"postcondition_probe_hash": "sha256:abc"});
239 let camel = serde_json::json!({"postconditionProbeHash": "sha256:def"});
240 assert!(InputInjectionCapabilityGuard::has_postcondition_probe(
241 &snake
242 ));
243 assert!(InputInjectionCapabilityGuard::has_postcondition_probe(
244 &camel
245 ));
246 }
247
248 #[test]
249 fn postcondition_probe_empty_string_is_missing() {
250 let empty = serde_json::json!({"postcondition_probe_hash": ""});
251 assert!(!InputInjectionCapabilityGuard::has_postcondition_probe(
252 &empty
253 ));
254 }
255}