diff options
Diffstat (limited to 'makima/src/server/handlers/mesh_red_team.rs')
| -rw-r--r-- | makima/src/server/handlers/mesh_red_team.rs | 497 |
1 files changed, 0 insertions, 497 deletions
diff --git a/makima/src/server/handlers/mesh_red_team.rs b/makima/src/server/handlers/mesh_red_team.rs deleted file mode 100644 index c5af60e..0000000 --- a/makima/src/server/handlers/mesh_red_team.rs +++ /dev/null @@ -1,497 +0,0 @@ -//! 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<Uuid>, - /// File path related to the issue (optional) - pub file_path: Option<String>, - /// Additional context about the issue - pub context: Option<String>, -} - -/// 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<Uuid>, -} - -/// 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<Uuid>, - pub file_path: Option<String>, - pub context: Option<String>, - pub delivered: bool, - pub created_at: chrono::DateTime<chrono::Utc>, -} - -// ============================================================================= -// 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<ApiError>)> { - 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<Uuid>, - 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<SharedState>, - headers: HeaderMap, - Json(request): Json<RedTeamNotifyRequest>, -) -> 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<SharedState>, - 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() -} |
