//! HTTP handlers for red team mesh operations. //! //! These endpoints are used by red team tasks (via the makima CLI) to notify //! supervisors of potential issues and query their own status. use axum::{ extract::State, http::{HeaderMap, StatusCode}, response::IntoResponse, Json, }; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; use crate::db::repository; use crate::server::handlers::mesh::{extract_auth, AuthSource}; use crate::server::messages::ApiError; use crate::server::state::{DaemonCommand, SharedState}; // ============================================================================= // Request/Response Types // ============================================================================= /// Severity level for red team notifications. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum RedTeamSeverity { /// Informational notice - minor issue or suggestion Info, /// Warning - potential problem that should be reviewed Warning, /// Critical - serious issue requiring immediate attention Critical, } impl Default for RedTeamSeverity { fn default() -> Self { Self::Warning } } impl std::fmt::Display for RedTeamSeverity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Info => write!(f, "INFO"), Self::Warning => write!(f, "WARNING"), Self::Critical => write!(f, "CRITICAL"), } } } /// Request to notify the supervisor of a potential issue. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RedTeamNotifyRequest { /// The issue description/message to send to the supervisor pub message: String, /// Severity level of the issue #[serde(default)] pub severity: RedTeamSeverity, /// ID of the task being reviewed (optional - if not provided, assumes general contract concern) pub related_task_id: Option, /// File path related to the issue (optional) pub file_path: Option, /// Additional context about the issue pub context: Option, } /// Response from the notify endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RedTeamNotifyResponse { /// Unique ID for this notification pub notification_id: Uuid, /// Whether the notification was successfully delivered to the supervisor pub delivered: bool, /// The supervisor task ID that received the notification pub supervisor_task_id: Option, } /// Response from the status endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RedTeamStatusResponse { /// Contract ID being monitored pub contract_id: Uuid, /// Red team task ID pub red_team_task_id: Uuid, /// Current task status pub status: String, /// Number of notifications sent so far pub notifications_sent: i64, } /// Red team notification record stored in database. #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RedTeamNotification { pub id: Uuid, pub red_team_task_id: Uuid, pub contract_id: Uuid, pub message: String, pub severity: String, pub related_task_id: Option, pub file_path: Option, pub context: Option, pub delivered: bool, pub created_at: chrono::DateTime, } // ============================================================================= // Helper Functions // ============================================================================= /// Verify the request comes from a red team task and extract ownership info. /// /// Returns (task_id, owner_id, contract_id) on success. async fn verify_red_team_auth( state: &SharedState, headers: &HeaderMap, ) -> Result<(Uuid, Uuid, Uuid), (StatusCode, Json)> { let auth = extract_auth(state, headers); let task_id = match auth { AuthSource::ToolKey(task_id) => task_id, _ => { return Err(( StatusCode::UNAUTHORIZED, Json(ApiError::new( "UNAUTHORIZED", "Red team endpoints require tool key auth", )), )); } }; // Get the task to verify it's a red team task and get owner_id let pool = state.db_pool.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) })?; let task = repository::get_task(pool, task_id) .await .map_err(|e| { tracing::error!(error = %e, "Failed to get red team task"); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", "Failed to verify red team task")), ) })? .ok_or_else(|| { ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Task not found")), ) })?; // Verify task is a red team task // NOTE: This requires the is_red_team field to be added to the Task struct. // For now, we check if the task name contains "red-team" or "red_team" as a fallback. let is_red_team = task.name.to_lowercase().contains("red-team") || task.name.to_lowercase().contains("red_team") || task.name.to_lowercase().contains("redteam"); if !is_red_team { return Err(( StatusCode::FORBIDDEN, Json(ApiError::new( "NOT_RED_TEAM", "Only red team tasks can use these endpoints", )), )); } // Red team tasks must be associated with a contract let contract_id = task.contract_id.ok_or_else(|| { ( StatusCode::BAD_REQUEST, Json(ApiError::new( "NO_CONTRACT", "Red team task must be associated with a contract", )), ) })?; Ok((task_id, task.owner_id, contract_id)) } /// Format an alert message for the supervisor. /// /// Creates a formatted alert with clear visual markers to grab attention. fn format_alert_message( severity: &RedTeamSeverity, message: &str, related_task_id: Option, file_path: Option<&str>, context: Option<&str>, ) -> String { let severity_marker = match severity { RedTeamSeverity::Info => "â„šī¸", RedTeamSeverity::Warning => "âš ī¸", RedTeamSeverity::Critical => "🚨", }; let border = match severity { RedTeamSeverity::Info => "─".repeat(60), RedTeamSeverity::Warning => "━".repeat(60), RedTeamSeverity::Critical => "═".repeat(60), }; let mut alert = format!( r#" {} {} [RED TEAM ALERT] - {} {} Issue: {} "#, border, severity_marker, severity, border, message ); if let Some(task_id) = related_task_id { alert.push_str(&format!("\nRelated Task: {}\n", task_id)); } if let Some(path) = file_path { alert.push_str(&format!("File: {}\n", path)); } if let Some(ctx) = context { alert.push_str(&format!("\nContext:\n{}\n", ctx)); } // Add action suggestions based on severity let actions = match severity { RedTeamSeverity::Info => { "Suggested Actions:\n- Review when convenient\n- Consider if changes are needed" } RedTeamSeverity::Warning => { "Suggested Actions:\n- Review the flagged item soon\n- Check if this deviates from the contract\n- Consider pausing related work until reviewed" } RedTeamSeverity::Critical => { "Suggested Actions:\n- STOP related work immediately\n- Review the flagged item urgently\n- Verify compliance with contract requirements\n- Consider reverting recent changes if necessary" } }; alert.push_str(&format!("\n{}\n{}\n", actions, border)); alert } // ============================================================================= // Handlers // ============================================================================= /// Notify the supervisor of a potential issue. /// /// POST /api/v1/mesh/red-team/notify /// /// This endpoint allows red team tasks to alert supervisors about issues they've /// identified during code review. The notification is sent as a message to the /// supervisor task. #[utoipa::path( post, path = "/api/v1/mesh/red-team/notify", request_body = RedTeamNotifyRequest, responses( (status = 200, description = "Notification sent", body = RedTeamNotifyResponse), (status = 401, description = "Unauthorized - tool key required"), (status = 403, description = "Forbidden - not a red team task"), (status = 404, description = "Task not found"), (status = 503, description = "Database not available"), (status = 500, description = "Internal server error"), ), security( ("tool_key" = []) ), tag = "Mesh Red Team" )] pub async fn notify_supervisor( State(state): State, headers: HeaderMap, Json(request): Json, ) -> impl IntoResponse { let (red_team_task_id, owner_id, contract_id) = match verify_red_team_auth(&state, &headers).await { Ok(ids) => ids, Err(e) => return e.into_response(), }; let pool = state.db_pool.as_ref().unwrap(); // Generate notification ID let notification_id = Uuid::new_v4(); // Get the contract to find the supervisor task let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Contract not found")), ) .into_response(); } Err(e) => { tracing::error!(error = %e, "Failed to get contract"); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", "Failed to get contract")), ) .into_response(); } }; let supervisor_task_id = contract.supervisor_task_id; // Format the alert message let alert_message = format_alert_message( &request.severity, &request.message, request.related_task_id, request.file_path.as_deref(), request.context.as_deref(), ); // Record the notification in the database as a history event let event_data = serde_json::json!({ "notification_id": notification_id.to_string(), "red_team_task_id": red_team_task_id.to_string(), "severity": request.severity.to_string(), "message": request.message, "related_task_id": request.related_task_id.map(|id| id.to_string()), "file_path": request.file_path, "context": request.context, }); let _ = repository::record_history_event( pool, owner_id, Some(contract_id), Some(red_team_task_id), "red_team_alert", Some(&request.severity.to_string().to_lowercase()), Some(&request.message), event_data, ) .await; // Try to send the message to the supervisor let mut delivered = false; if let Some(sup_task_id) = supervisor_task_id { // Get the supervisor task to find its daemon if let Ok(Some(supervisor_task)) = repository::get_task(pool, sup_task_id).await { if let Some(daemon_id) = supervisor_task.daemon_id { // Send the alert message to the supervisor let cmd = DaemonCommand::SendMessage { task_id: sup_task_id, message: alert_message.clone(), }; if let Err(e) = state.send_daemon_command(daemon_id, cmd).await { tracing::warn!( error = %e, supervisor_task_id = %sup_task_id, daemon_id = %daemon_id, "Failed to send red team alert to supervisor" ); } else { delivered = true; tracing::info!( notification_id = %notification_id, red_team_task_id = %red_team_task_id, supervisor_task_id = %sup_task_id, severity = %request.severity, "Red team alert delivered to supervisor" ); } } else { tracing::warn!( supervisor_task_id = %sup_task_id, "Supervisor task has no assigned daemon - alert not delivered" ); } } } else { tracing::warn!( contract_id = %contract_id, "Contract has no supervisor task - alert not delivered" ); } ( StatusCode::OK, Json(RedTeamNotifyResponse { notification_id, delivered, supervisor_task_id, }), ) .into_response() } /// Get the status of the red team task. /// /// GET /api/v1/mesh/red-team/status /// /// Returns information about the current red team task including the contract /// being monitored and notification statistics. #[utoipa::path( get, path = "/api/v1/mesh/red-team/status", responses( (status = 200, description = "Red team status", body = RedTeamStatusResponse), (status = 401, description = "Unauthorized - tool key required"), (status = 403, description = "Forbidden - not a red team task"), (status = 404, description = "Task not found"), (status = 503, description = "Database not available"), (status = 500, description = "Internal server error"), ), security( ("tool_key" = []) ), tag = "Mesh Red Team" )] pub async fn get_status( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { let (red_team_task_id, owner_id, contract_id) = match verify_red_team_auth(&state, &headers).await { Ok(ids) => ids, Err(e) => return e.into_response(), }; let pool = state.db_pool.as_ref().unwrap(); // Get the red team task status let task = match repository::get_task(pool, red_team_task_id).await { Ok(Some(t)) => t, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Red team task not found")), ) .into_response(); } Err(e) => { tracing::error!(error = %e, "Failed to get red team task"); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", "Failed to get task")), ) .into_response(); } }; // Count notifications sent by this red team task // Query history_events for red_team_alert events from this task let notifications_sent = match sqlx::query_scalar::<_, i64>( r#" SELECT COUNT(*) FROM history_events WHERE owner_id = $1 AND contract_id = $2 AND task_id = $3 AND event_type = 'red_team_alert' "#, ) .bind(owner_id) .bind(contract_id) .bind(red_team_task_id) .fetch_one(pool) .await { Ok(count) => count, Err(e) => { tracing::warn!(error = %e, "Failed to count red team notifications"); 0 } }; ( StatusCode::OK, Json(RedTeamStatusResponse { contract_id, red_team_task_id, status: task.status, notifications_sent, }), ) .into_response() }