Prompt injection detection (simplified - only pattern matching) (#4237)

Signed-off-by: Dorien Koelemeijer <dkoelemeijer@squareup.com>

merging as looks good, lets keep an eye on it.
This commit is contained in:
dorien-koelemeijer
2025-09-10 10:41:30 +10:00
committed by GitHub
parent 9bb1bb530c
commit 916ba902dc
20 changed files with 2043 additions and 133 deletions
+2
View File
@@ -1,7 +1,9 @@
pub mod permission_confirmation;
pub mod permission_inspector;
pub mod permission_judge;
pub mod permission_store;
pub use permission_confirmation::{Permission, PermissionConfirmation};
pub use permission_inspector::PermissionInspector;
pub use permission_judge::detect_read_only_tools;
pub use permission_store::ToolPermissionStore;
@@ -0,0 +1,213 @@
use crate::agents::platform_tools::PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME;
use crate::config::permission::PermissionLevel;
use crate::config::PermissionManager;
use crate::conversation::message::{Message, ToolRequest};
use crate::permission::permission_judge::PermissionCheckResult;
use crate::tool_inspection::{InspectionAction, InspectionResult, ToolInspector};
use anyhow::Result;
use async_trait::async_trait;
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::Mutex;
/// Permission Inspector that handles tool permission checking
pub struct PermissionInspector {
mode: Arc<Mutex<String>>,
readonly_tools: HashSet<String>,
regular_tools: HashSet<String>,
pub permission_manager: Arc<Mutex<PermissionManager>>,
}
impl PermissionInspector {
pub fn new(
mode: String,
readonly_tools: HashSet<String>,
regular_tools: HashSet<String>,
) -> Self {
Self {
mode: Arc::new(Mutex::new(mode)),
readonly_tools,
regular_tools,
permission_manager: Arc::new(Mutex::new(PermissionManager::default())),
}
}
pub fn with_permission_manager(
mode: String,
readonly_tools: HashSet<String>,
regular_tools: HashSet<String>,
permission_manager: Arc<Mutex<PermissionManager>>,
) -> Self {
Self {
mode: Arc::new(Mutex::new(mode)),
readonly_tools,
regular_tools,
permission_manager,
}
}
/// Update the mode of this permission inspector
pub async fn update_mode(&self, new_mode: String) {
let mut mode = self.mode.lock().await;
*mode = new_mode;
}
/// Process inspection results into permission decisions
/// This method takes all inspection results and converts them into a PermissionCheckResult
/// that can be used by the agent to determine which tools to approve, deny, or ask for approval
pub fn process_inspection_results(
&self,
remaining_requests: &[ToolRequest],
inspection_results: &[InspectionResult],
) -> PermissionCheckResult {
use crate::tool_inspection::apply_inspection_results_to_permissions;
// Start with permission inspector's decisions as the baseline
let mut permission_check_result = PermissionCheckResult {
approved: vec![],
needs_approval: vec![],
denied: vec![],
};
// Apply permission inspector results first (baseline behavior)
let permission_results: Vec<_> = inspection_results
.iter()
.filter(|result| result.inspector_name == "permission")
.collect();
for request in remaining_requests {
// Find the permission decision for this request
if let Some(permission_result) = permission_results
.iter()
.find(|result| result.tool_request_id == request.id)
{
match permission_result.action {
InspectionAction::Allow => {
permission_check_result.approved.push(request.clone());
}
InspectionAction::Deny => {
permission_check_result.denied.push(request.clone());
}
InspectionAction::RequireApproval(_) => {
permission_check_result.needs_approval.push(request.clone());
}
}
} else {
// If no permission result found, default to needs approval for safety
permission_check_result.needs_approval.push(request.clone());
}
}
// Apply security and other inspector results as overrides
let non_permission_results: Vec<_> = inspection_results
.iter()
.filter(|result| result.inspector_name != "permission")
.cloned()
.collect();
if !non_permission_results.is_empty() {
permission_check_result = apply_inspection_results_to_permissions(
permission_check_result,
&non_permission_results,
);
}
permission_check_result
}
}
#[async_trait]
impl ToolInspector for PermissionInspector {
fn name(&self) -> &'static str {
"permission"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
async fn inspect(
&self,
tool_requests: &[ToolRequest],
_messages: &[Message],
) -> Result<Vec<InspectionResult>> {
let mut results = Vec::new();
let permission_manager = self.permission_manager.lock().await;
let mode = self.mode.lock().await;
for request in tool_requests {
if let Ok(tool_call) = &request.tool_call {
let tool_name = &tool_call.name;
// Handle different modes
let action = if *mode == "chat" {
// In chat mode, all tools are skipped (handled elsewhere)
continue;
} else if *mode == "auto" {
// In auto mode, all tools are approved
InspectionAction::Allow
} else {
// Smart mode - check permissions
// 1. Check user-defined permission first
if let Some(level) = permission_manager.get_user_permission(tool_name) {
match level {
PermissionLevel::AlwaysAllow => InspectionAction::Allow,
PermissionLevel::NeverAllow => InspectionAction::Deny,
PermissionLevel::AskBefore => InspectionAction::RequireApproval(None),
}
}
// 2. Check if it's a readonly or regular tool (both pre-approved)
else if self.readonly_tools.contains(tool_name)
|| self.regular_tools.contains(tool_name)
{
InspectionAction::Allow
}
// 4. Special case for extension management
else if tool_name == PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME {
InspectionAction::RequireApproval(Some(
"Extension management requires approval for security".to_string(),
))
}
// 5. Default: require approval for unknown tools
else {
InspectionAction::RequireApproval(None)
}
};
let reason = match &action {
InspectionAction::Allow => {
if *mode == "auto" {
"Auto mode - all tools approved".to_string()
} else if self.readonly_tools.contains(tool_name) {
"Tool marked as read-only".to_string()
} else if self.regular_tools.contains(tool_name) {
"Tool pre-approved".to_string()
} else {
"User permission allows this tool".to_string()
}
}
InspectionAction::Deny => "User permission denies this tool".to_string(),
InspectionAction::RequireApproval(_) => {
if tool_name == PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME {
"Extension management requires user approval".to_string()
} else {
"Tool requires user approval".to_string()
}
}
};
results.push(InspectionResult {
tool_request_id: request.id.clone(),
action,
reason,
confidence: 1.0, // Permission decisions are definitive
inspector_name: self.name().to_string(),
finding_id: None,
});
}
}
Ok(results)
}
}
@@ -155,7 +155,7 @@ pub async fn detect_read_only_tools(
}
}
// Define return structure
/// Result of permission checking for tool requests
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PermissionCheckResult {
pub approved: Vec<ToolRequest>,