//! 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()
}