use serde::{Deserialize, Serialize};
use crate::error::WasmGuardError;
pub const VERDICT_ALLOW: i32 = 0;
pub const VERDICT_DENY: i32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardRequest {
pub tool_name: String,
pub server_id: String,
pub agent_id: String,
pub arguments: serde_json::Value,
#[serde(default)]
pub scopes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extracted_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extracted_target: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub filesystem_roots: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub matched_grant_index: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GuardVerdict {
Allow,
Deny { reason: Option<String> },
}
impl GuardVerdict {
#[must_use]
pub fn is_allow(&self) -> bool {
matches!(self, Self::Allow)
}
#[must_use]
pub fn is_deny(&self) -> bool {
matches!(self, Self::Deny { .. })
}
}
pub trait WasmGuardAbi: Send + Sync {
fn load_module(&mut self, wasm_bytes: &[u8], fuel_limit: u64) -> Result<(), WasmGuardError>;
fn evaluate(&mut self, request: &GuardRequest) -> Result<GuardVerdict, WasmGuardError>;
fn backend_name(&self) -> &str;
fn last_fuel_consumed(&self) -> Option<u64> {
None
}
fn clear_instance_pre_cache(&mut self) {}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuestDenyResponse {
pub reason: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guard_request_serializes_with_enrichment() {
let req = GuardRequest {
tool_name: "read_file".to_string(),
server_id: "fs-server".to_string(),
agent_id: "agent-42".to_string(),
arguments: serde_json::json!({"path": "/etc/passwd"}),
scopes: vec!["fs-server:read_file".to_string()],
action_type: Some("file_access".to_string()),
extracted_path: Some("/etc/passwd".to_string()),
extracted_target: None,
filesystem_roots: vec!["/home".to_string(), "/tmp".to_string()],
matched_grant_index: Some(0),
};
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["action_type"], "file_access");
assert_eq!(json["extracted_path"], "/etc/passwd");
assert!(
json.get("extracted_target").is_none(),
"None fields with skip_serializing_if should be absent"
);
assert_eq!(json["filesystem_roots"][0], "/home");
assert_eq!(json["filesystem_roots"][1], "/tmp");
assert_eq!(json["matched_grant_index"], 0);
}
#[test]
fn guard_request_deserializes_without_enrichment() {
let json = serde_json::json!({
"tool_name": "test_tool",
"server_id": "test_server",
"agent_id": "agent-1",
"arguments": {"key": "value"},
"scopes": ["test_server:test_tool"]
});
let req: GuardRequest = serde_json::from_value(json).unwrap();
assert_eq!(req.tool_name, "test_tool");
assert!(
req.action_type.is_none(),
"action_type should default to None"
);
assert!(
req.extracted_path.is_none(),
"extracted_path should default to None"
);
assert!(
req.extracted_target.is_none(),
"extracted_target should default to None"
);
assert!(
req.filesystem_roots.is_empty(),
"filesystem_roots should default to empty Vec"
);
assert!(
req.matched_grant_index.is_none(),
"matched_grant_index should default to None"
);
}
#[test]
fn guard_request_no_session_metadata() {
let req = GuardRequest {
tool_name: "t".to_string(),
server_id: "s".to_string(),
agent_id: "a".to_string(),
arguments: serde_json::Value::Null,
scopes: vec![],
action_type: None,
extracted_path: None,
extracted_target: None,
filesystem_roots: Vec::new(),
matched_grant_index: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(
!json.contains("session_metadata"),
"session_metadata should not appear in serialized output"
);
}
}