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