summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/mesh_red_team.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/mesh_red_team.rs')
-rw-r--r--makima/src/server/handlers/mesh_red_team.rs497
1 files changed, 497 insertions, 0 deletions
diff --git a/makima/src/server/handlers/mesh_red_team.rs b/makima/src/server/handlers/mesh_red_team.rs
new file mode 100644
index 0000000..c5af60e
--- /dev/null
+++ b/makima/src/server/handlers/mesh_red_team.rs
@@ -0,0 +1,497 @@
+//! 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()
+}