use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use super::{Guard, GuardAction, GuardContext, GuardResult, Severity};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InputInjectionCapabilityConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_allowed_input_types")]
pub allowed_input_types: Vec<String>,
#[serde(default)]
pub require_postcondition_probe: bool,
}
fn default_enabled() -> bool {
true
}
fn default_allowed_input_types() -> Vec<String> {
vec![
"keyboard".to_string(),
"mouse".to_string(),
"touch".to_string(),
]
}
impl Default for InputInjectionCapabilityConfig {
fn default() -> Self {
Self {
enabled: true,
allowed_input_types: default_allowed_input_types(),
require_postcondition_probe: false,
}
}
}
pub struct InputInjectionCapabilityGuard {
name: String,
enabled: bool,
allowed_types: HashSet<String>,
require_postcondition_probe: bool,
}
impl InputInjectionCapabilityGuard {
pub fn new() -> Self {
Self::with_config(InputInjectionCapabilityConfig::default())
}
pub fn with_config(config: InputInjectionCapabilityConfig) -> Self {
let enabled = config.enabled;
let require_postcondition_probe = config.require_postcondition_probe;
let allowed_types: HashSet<_> = config.allowed_input_types.into_iter().collect();
Self {
name: "input_injection_capability".to_string(),
enabled,
allowed_types,
require_postcondition_probe,
}
}
}
impl Default for InputInjectionCapabilityGuard {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Guard for InputInjectionCapabilityGuard {
fn name(&self) -> &str {
&self.name
}
fn handles(&self, action: &GuardAction<'_>) -> bool {
if !self.enabled {
return false;
}
matches!(action, GuardAction::Custom("input.inject", _))
}
async fn check(&self, action: &GuardAction<'_>, _context: &GuardContext) -> GuardResult {
if !self.enabled {
return GuardResult::allow(&self.name);
}
let data = match action {
GuardAction::Custom("input.inject", data) => *data,
_ => return GuardResult::allow(&self.name),
};
if let Some(input_type) = data
.get("input_type")
.or_else(|| data.get("inputType"))
.and_then(|v| v.as_str())
{
if !self.allowed_types.contains(input_type) {
return GuardResult::block(
&self.name,
Severity::Error,
format!("Input type '{}' is not allowed by policy", input_type),
)
.with_details(serde_json::json!({
"input_type": input_type,
"allowed_types": self.allowed_types.iter().collect::<Vec<_>>(),
"reason": "input_type_not_allowed",
}));
}
} else {
return GuardResult::block(
&self.name,
Severity::Error,
"Missing required input_type field for input injection action",
)
.with_details(serde_json::json!({
"reason": "missing_input_type",
"allowed_types": self.allowed_types.iter().collect::<Vec<_>>(),
}));
}
if self.require_postcondition_probe {
let has_probe = data
.get("postcondition_probe_hash")
.or_else(|| data.get("postconditionProbeHash"))
.and_then(|v| v.as_str())
.is_some_and(|s| !s.is_empty());
if !has_probe {
return GuardResult::block(
&self.name,
Severity::Error,
"Postcondition probe hash is required but not provided",
)
.with_details(serde_json::json!({
"reason": "missing_postcondition_probe",
"require_postcondition_probe": true,
}));
}
}
GuardResult::allow(&self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handles_input_inject() {
let guard = InputInjectionCapabilityGuard::new();
let data = serde_json::json!({});
assert!(guard.handles(&GuardAction::Custom("input.inject", &data)));
}
#[test]
fn test_does_not_handle_other_actions() {
let guard = InputInjectionCapabilityGuard::new();
let data = serde_json::json!({});
assert!(!guard.handles(&GuardAction::Custom("remote.clipboard", &data)));
assert!(!guard.handles(&GuardAction::Custom("remote.session.connect", &data)));
assert!(!guard.handles(&GuardAction::FileAccess("/tmp/file")));
}
#[tokio::test]
async fn test_allows_known_input_type() {
let guard = InputInjectionCapabilityGuard::new();
let context = GuardContext::new();
let data = serde_json::json!({"input_type": "keyboard"});
let result = guard
.check(&GuardAction::Custom("input.inject", &data), &context)
.await;
assert!(result.allowed);
}
#[tokio::test]
async fn test_denies_unknown_input_type() {
let guard = InputInjectionCapabilityGuard::new();
let context = GuardContext::new();
let data = serde_json::json!({"input_type": "gamepad"});
let result = guard
.check(&GuardAction::Custom("input.inject", &data), &context)
.await;
assert!(!result.allowed);
}
#[tokio::test]
async fn test_requires_postcondition_probe() {
let config = InputInjectionCapabilityConfig {
require_postcondition_probe: true,
..Default::default()
};
let guard = InputInjectionCapabilityGuard::with_config(config);
let context = GuardContext::new();
let data = serde_json::json!({"input_type": "keyboard"});
let result = guard
.check(&GuardAction::Custom("input.inject", &data), &context)
.await;
assert!(!result.allowed);
let data = serde_json::json!({
"input_type": "keyboard",
"postcondition_probe_hash": "sha256:abc123"
});
let result = guard
.check(&GuardAction::Custom("input.inject", &data), &context)
.await;
assert!(result.allowed);
}
#[tokio::test]
async fn test_denies_without_input_type_field() {
let guard = InputInjectionCapabilityGuard::new();
let context = GuardContext::new();
let data = serde_json::json!({"action": "click"});
let result = guard
.check(&GuardAction::Custom("input.inject", &data), &context)
.await;
assert!(!result.allowed);
}
}