From 151e9d87e117b7980e6aad522ac8f3633eeca87a Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 2 Feb 2026 02:34:50 +0000 Subject: Make makima more opinionated and structured --- makima/src/server/handlers/contract_chat.rs | 10 +- makima/src/server/handlers/contracts.rs | 5 - makima/src/server/handlers/mesh.rs | 4 - makima/src/server/handlers/mesh_chat.rs | 1 - makima/src/server/handlers/mesh_daemon.rs | 62 +++ makima/src/server/handlers/mesh_red_team.rs | 497 ---------------------- makima/src/server/handlers/mesh_supervisor.rs | 306 +------------ makima/src/server/handlers/mod.rs | 1 - makima/src/server/handlers/templates.rs | 419 +----------------- makima/src/server/handlers/transcript_analysis.rs | 4 - makima/src/server/mod.rs | 18 +- 11 files changed, 77 insertions(+), 1250 deletions(-) delete mode 100644 makima/src/server/handlers/mesh_red_team.rs (limited to 'makima/src/server') diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index b025485..2d54894 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1362,7 +1362,6 @@ async fn handle_contract_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, @@ -1460,7 +1459,6 @@ async fn handle_contract_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, @@ -2213,8 +2211,7 @@ async fn handle_contract_request( continue_from_task_id: previous_task_id, copy_files: None, is_supervisor: false, - is_red_team: false, - checkpoint_sha: None, + checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, supervisor_worktree_task_id: None, // Not spawned by supervisor @@ -2612,8 +2609,6 @@ async fn handle_contract_request( phase_guard: None, local_only: None, auto_merge_local: None, - red_team_enabled: None, - red_team_prompt: None, template_id: None, }; @@ -2736,8 +2731,7 @@ async fn handle_contract_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, - checkpoint_sha: None, + checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, supervisor_worktree_task_id: None, // Not spawned by supervisor diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 01b4610..8c8cabf 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -363,7 +363,6 @@ pub async fn create_contract( continue_from_task_id: None, copy_files: None, is_supervisor: true, - is_red_team: false, checkpoint_sha: None, priority: 0, merge_mode: None, @@ -438,7 +437,6 @@ pub async fn create_contract( supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, @@ -462,7 +460,6 @@ pub async fn create_contract( supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, @@ -593,7 +590,6 @@ pub async fn update_contract( supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, @@ -1523,7 +1519,6 @@ pub async fn change_phase( supervisor_task_id: updated_contract.supervisor_task_id, local_only: updated_contract.local_only, auto_merge_local: updated_contract.auto_merge_local, - red_team_enabled: updated_contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index af77b56..fe9ffc0 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -2613,7 +2613,6 @@ pub async fn reassign_task( plan: updated_plan.clone(), parent_task_id: task.parent_task_id, is_supervisor: task.is_supervisor, - is_red_team: task.is_red_team, priority: task.priority, repository_url: task.repository_url.clone(), base_branch: task.base_branch.clone(), @@ -3390,7 +3389,6 @@ pub async fn fork_task( plan: req.new_task_plan.clone(), parent_task_id: None, // Forked tasks are independent is_supervisor: false, - is_red_team: false, priority: task.priority, repository_url: task.repository_url.clone(), base_branch: task.base_branch.clone(), @@ -3549,7 +3547,6 @@ pub async fn resume_from_checkpoint( plan: req.plan, parent_task_id: None, is_supervisor: false, - is_red_team: false, priority: task.priority, repository_url: task.repository_url.clone(), base_branch: task.base_branch.clone(), @@ -3886,7 +3883,6 @@ pub async fn branch_task( plan: req.message, parent_task_id: None, is_supervisor: false, - is_red_team: false, priority: source_task.priority, repository_url: source_task.repository_url.clone(), base_branch: source_task.base_branch.clone(), diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs index eee899f..a6a3a3c 100644 --- a/makima/src/server/handlers/mesh_chat.rs +++ b/makima/src/server/handlers/mesh_chat.rs @@ -1017,7 +1017,6 @@ async fn handle_mesh_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index 34e2cc3..cb929ea 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -1870,6 +1870,68 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re } } } + } else if let (Some(patch_b64), Some(base_sha)) = (&patch_data, &patch_base_sha) { + // Ephemeral patch-only checkpoint (no git commit) + // Store patch directly in checkpoint_patches without a task_checkpoint + if let Some(pool) = state.db_pool.as_ref() { + match base64::engine::general_purpose::STANDARD.decode(patch_b64) { + Ok(patch_bytes) => { + let files_count = patch_files_count.unwrap_or(0); + // Default TTL: 7 days (168 hours) + let ttl_hours = 168i64; + match repository::create_checkpoint_patch( + pool, + task_id, + None, // No checkpoint_id for ephemeral patches + base_sha, + &patch_bytes, + files_count, + ttl_hours, + ).await { + Ok(patch) => { + tracing::info!( + task_id = %task_id, + patch_id = %patch.id, + patch_size = patch_bytes.len(), + files_count = files_count, + "Ephemeral patch stored for recovery" + ); + + state.broadcast_task_output(TaskOutputNotification { + task_id, + owner_id: Some(owner_id), + message_type: "system".to_string(), + content: format!( + "✓ Patch saved: {} ({} files)", + message, + files_count + ), + tool_name: None, + tool_input: None, + is_error: Some(false), + cost_usd: None, + duration_ms: None, + is_partial: false, + }); + } + Err(e) => { + tracing::warn!( + task_id = %task_id, + error = %e, + "Failed to store ephemeral patch" + ); + } + } + } + Err(e) => { + tracing::warn!( + task_id = %task_id, + error = %e, + "Failed to decode ephemeral patch base64 data" + ); + } + } + } } } else { // Broadcast failure 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, - /// 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() -} diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index a29b666..43388a8 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -37,10 +37,6 @@ pub struct SpawnTaskRequest { pub checkpoint_sha: Option, /// Repository URL for the task (optional - if not provided, will be looked up from contract). pub repository_url: Option, - /// If true, create a separate worktree for the task (requires merge after). - /// If false (default), the task shares the supervisor's worktree. - #[serde(default)] - pub use_own_worktree: bool, } /// Request to wait for task completion. @@ -610,8 +606,8 @@ pub async fn spawn_task( } // Create task request - // Share supervisor's worktree by default; separate worktree only when explicitly requested - let supervisor_worktree_task_id = if request.use_own_worktree { None } else { Some(supervisor_id) }; + // All tasks share the supervisor's worktree + let supervisor_worktree_task_id = Some(supervisor_id); let create_req = CreateTaskRequest { name: request.name.clone(), @@ -621,7 +617,6 @@ pub async fn spawn_task( contract_id: Some(request.contract_id), parent_task_id: request.parent_task_id, is_supervisor: false, - is_red_team: false, checkpoint_sha: request.checkpoint_sha.clone(), merge_mode: Some("manual".to_string()), priority: 0, @@ -733,8 +728,8 @@ pub async fn spawn_task( patch_base_sha: None, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - // Share supervisor's worktree by default; separate worktree only when explicitly requested - supervisor_worktree_task_id: if request.use_own_worktree { None } else { Some(supervisor_id) }, + // All tasks share the supervisor's worktree + supervisor_worktree_task_id: Some(supervisor_id), }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -762,66 +757,6 @@ pub async fn spawn_task( updated_by: "supervisor".to_string(), }); - // Check if we should spawn a red team task - // Conditions: - // 1. This is not a supervisor task - // 2. This is not already a red team task - // 3. Contract has red_team_enabled = true - // 4. No red team task exists for this contract yet - if !updated_task.is_supervisor && !updated_task.is_red_team && contract.red_team_enabled { - if let Some(contract_id) = updated_task.contract_id { - // Check if a red team task already exists - match repository::get_red_team_task_for_contract(pool, contract_id).await { - Ok(None) => { - // No red team task exists, spawn one - tracing::info!( - contract_id = %contract_id, - work_task_id = %updated_task.id, - "Spawning red team task for contract (first work task started)" - ); - match spawn_red_team_task( - pool, - &state, - contract_id, - owner_id, - &contract.name, - &contract.phase, - contract.red_team_prompt.as_deref(), - ).await { - Ok(red_team_task) => { - tracing::info!( - contract_id = %contract_id, - red_team_task_id = %red_team_task.id, - "Red team task spawned successfully" - ); - } - Err(e) => { - // Log error but don't fail the work task spawn - tracing::error!( - contract_id = %contract_id, - error = %e, - "Failed to spawn red team task" - ); - } - } - } - Ok(Some(existing)) => { - tracing::debug!( - contract_id = %contract_id, - red_team_task_id = %existing.id, - "Red team task already exists for contract" - ); - } - Err(e) => { - tracing::error!( - contract_id = %contract_id, - error = %e, - "Error checking for existing red team task" - ); - } - } - } - } } break; } @@ -2583,239 +2518,6 @@ pub async fn rewind_conversation( .into_response() } -// ============================================================================= -// Red Team Task Spawning -// ============================================================================= - -/// Generate the system prompt/plan for a red team task. -/// -/// This creates detailed instructions for the red team monitor, including -/// what to look for, severity levels, and how to report issues. -pub fn generate_red_team_plan( - contract_name: &str, - contract_phase: &str, - custom_prompt: Option<&str>, -) -> String { - let custom_criteria = if let Some(prompt) = custom_prompt { - format!( - r#" - -## Custom Review Criteria - -The contract owner has specified additional review criteria: -{} -"#, - prompt - ) - } else { - String::new() - }; - - format!( - r#"# Red Team Monitor - -You are an adversarial quality reviewer for a software development contract. Your role is to monitor work task outputs in real-time and flag potential issues BEFORE they compound into larger problems. - -## Your Mission - -Monitor all task outputs and verify: -1. **Plan Adherence**: Are tasks following the implementation plan? -2. **Code Quality**: Does the code meet repository standards? -3. **Contract Requirements**: Does the implementation match the specification? -4. **Best Practices**: Are there obvious anti-patterns or issues? - -## Access Available - -You have read-only access to: -- Task outputs (streamed in real-time) -- Task diffs (code changes) -- Contract specifications and plan documents -- Repository configuration files (CONTRIBUTING.md, linting configs, etc.) - -## How to Monitor - -1. **Subscribe to task outputs**: You'll receive outputs from all work tasks -2. **Analyze code changes**: Request diffs for completed tasks -3. **Cross-reference**: Compare outputs against the plan and specifications -4. **Report issues**: Use `makima red-team notify` when you detect problems - -## When to Notify - -NOTIFY the supervisor when you observe: -- **Critical**: Security vulnerabilities, data loss risks, breaking changes -- **High**: Significant deviations from the plan, major code quality issues -- **Medium**: Missing tests, suboptimal implementations, minor standard violations -- **Low**: Style inconsistencies, documentation gaps (use sparingly) - -## What NOT to Do - -- Do NOT nitpick minor style issues (that's what linters are for) -- Do NOT block progress for trivial concerns -- Do NOT write code or make changes yourself -- Do NOT notify for things that are already in progress and being addressed -- Do NOT create duplicate notifications for the same issue - -## Notification Format - -When notifying, always include: -1. A clear, concise description of the issue -2. The severity level (critical/high/medium/low) -3. The related task ID if applicable -4. The specific file or code location if known -5. Why this matters (reference to plan, spec, or standards) - -## Example Notification - -``` -makima red-team notify "Task is implementing authentication with plaintext password storage, which contradicts the security requirements in the specification document" \ - --severity critical \ - --task \ - --file "src/auth/user.rs" \ - --context "Specification section 3.2 requires bcrypt hashing for all passwords" -``` -{} -## Contract Context - -Contract: {} -Phase: {} - -Focus your monitoring on outputs that relate to the active work tasks. Prioritize issues that could affect the success of the contract or introduce technical debt. -"#, - custom_criteria, contract_name, contract_phase - ) -} - -/// Spawn a red team task for a contract. -/// -/// This creates a red team monitor task that will observe work task outputs -/// and can notify the supervisor about potential issues. -pub async fn spawn_red_team_task( - pool: &sqlx::PgPool, - state: &SharedState, - contract_id: Uuid, - owner_id: Uuid, - contract_name: &str, - contract_phase: &str, - red_team_prompt: Option<&str>, -) -> Result { - // Generate the red team plan/prompt - let plan = generate_red_team_plan(contract_name, contract_phase, red_team_prompt); - - // Create task request - let create_req = CreateTaskRequest { - name: "Red Team Monitor".to_string(), - description: Some("Adversarial review task monitoring work task outputs".to_string()), - plan, - contract_id: Some(contract_id), - parent_task_id: None, - is_supervisor: false, - is_red_team: true, - priority: 0, - repository_url: None, // Red team doesn't need a repo - base_branch: None, - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, // Red team uses its own working area - }; - - // Create task in DB - let task = repository::create_task_for_owner(pool, owner_id, create_req) - .await - .map_err(|e| format!("Failed to create red team task: {}", e))?; - - tracing::info!( - contract_id = %contract_id, - red_team_task_id = %task.id, - "Created red team task for contract" - ); - - // Find a daemon to run the red team task - for entry in state.daemon_connections.iter() { - let daemon = entry.value(); - if daemon.owner_id == owner_id { - // Update task with daemon assignment - let update_req = UpdateTaskRequest { - status: Some("starting".to_string()), - daemon_id: Some(daemon.id), - version: Some(task.version), - ..Default::default() - }; - - match repository::update_task_for_owner(pool, task.id, owner_id, update_req).await { - Ok(Some(updated_task)) => { - // Send spawn command to daemon - let cmd = DaemonCommand::SpawnTask { - task_id: updated_task.id, - task_name: updated_task.name.clone(), - plan: updated_task.plan.clone(), - repo_url: None, // Red team doesn't need a repo - base_branch: None, - target_branch: None, - parent_task_id: None, - depth: 0, - is_orchestrator: false, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - contract_id: Some(contract_id), - is_supervisor: false, - autonomous_loop: false, - resume_session: false, - conversation_history: None, - patch_data: None, - patch_base_sha: None, - local_only: true, // Red team is always local-only - auto_merge_local: false, // Red team doesn't auto-merge - supervisor_worktree_task_id: None, - }; - - if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { - tracing::warn!( - error = %e, - daemon_id = %daemon.id, - red_team_task_id = %task.id, - "Failed to send red team spawn command" - ); - // Rollback - let rollback_req = UpdateTaskRequest { - status: Some("pending".to_string()), - clear_daemon_id: true, - ..Default::default() - }; - let _ = repository::update_task_for_owner(pool, task.id, owner_id, rollback_req).await; - } else { - tracing::info!( - red_team_task_id = %task.id, - daemon_id = %daemon.id, - "Red team task spawn command sent" - ); - return Ok(updated_task); - } - } - Ok(None) => { - tracing::warn!(red_team_task_id = %task.id, "Red team task not found when updating daemon_id"); - } - Err(e) => { - tracing::error!(red_team_task_id = %task.id, error = %e, "Failed to update red team task with daemon_id"); - } - } - break; - } - } - - // Return the task even if we couldn't start it on a daemon - // It will remain pending and can be started later - Ok(task) -} - // ============================================================================= // Supervisor State Persistence Helpers (Task 3.3) // ============================================================================= diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index 8af2a37..a14c4f7 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -13,7 +13,6 @@ pub mod mesh; pub mod mesh_chat; pub mod mesh_daemon; pub mod mesh_merge; -pub mod mesh_red_team; pub mod mesh_supervisor; pub mod mesh_ws; pub mod repository_history; diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 0cc5657..aa97876 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -1,27 +1,19 @@ //! Contract types API handler. +//! Only returns built-in contract types (simple, specification, execute). use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, }; use serde::Serialize; use utoipa::ToSchema; -use uuid::Uuid; -use crate::db::models::{ - ContractTypeTemplateRecord, ContractTypeTemplateSummary, CreateTemplateRequest, - UpdateTemplateRequest, -}; -use crate::db::repository; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; -use crate::server::auth::{Authenticated, MaybeAuthenticated}; -use crate::server::state::SharedState; // ============================================================================= -// Contract Type Templates (Workflow Definitions) +// Contract Type Templates (Built-in Only) // ============================================================================= /// Response for listing contract types @@ -31,14 +23,7 @@ pub struct ListContractTypesResponse { pub contract_types: Vec, } -/// Response for a single custom template -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct TemplateResponse { - pub template: ContractTypeTemplateSummary, -} - -/// List all available contract type templates (built-in + custom) +/// List all available contract type templates (built-in only) #[utoipa::path( get, path = "/api/v1/contract-types", @@ -47,404 +32,12 @@ pub struct TemplateResponse { ), tag = "templates" )] -pub async fn list_contract_types( - State(state): State, - MaybeAuthenticated(auth): MaybeAuthenticated, -) -> impl IntoResponse { - // Start with built-in types - let mut contract_types = templates::all_contract_types(); - - // If authenticated, also fetch custom templates for this owner - if let Some(user) = auth { - if let Some(ref pool) = state.db_pool { - if let Ok(custom_templates) = - repository::list_templates_for_owner(pool, user.owner_id).await - { - for template in custom_templates { - contract_types.push(template_record_to_api(&template)); - } - } - } - } - +pub async fn list_contract_types() -> impl IntoResponse { + // Only return built-in types (simple, specification, execute) + let contract_types = templates::all_contract_types(); ( StatusCode::OK, Json(ListContractTypesResponse { contract_types }), ) .into_response() } - -/// Create a new custom contract type template -#[utoipa::path( - post, - path = "/api/v1/contract-types", - request_body = CreateTemplateRequest, - responses( - (status = 201, description = "Template created successfully", body = TemplateResponse), - (status = 400, description = "Invalid request"), - (status = 401, description = "Unauthorized"), - (status = 409, description = "Template with this name already exists") - ), - tag = "templates" -)] -pub async fn create_template( - State(state): State, - Authenticated(auth): Authenticated, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - // Validate request - if req.name.trim().is_empty() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": "Template name cannot be empty" - })), - ) - .into_response(); - } - - if req.phases.is_empty() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": "Template must have at least one phase" - })), - ) - .into_response(); - } - - // Validate default_phase is in the phases list - if !req.phases.iter().any(|p| p.id == req.default_phase) { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": format!("Default phase '{}' is not in the phases list", req.default_phase) - })), - ) - .into_response(); - } - - // Check that template name doesn't conflict with built-in types - let builtin_names = ["simple", "specification", "execute"]; - if builtin_names.contains(&req.name.to_lowercase().as_str()) { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "Cannot create a template with the same name as a built-in type" - })), - ) - .into_response(); - } - - match repository::create_template_for_owner(pool, auth.owner_id, req).await { - Ok(template) => ( - StatusCode::CREATED, - Json(serde_json::json!({ - "template": template_record_to_summary(&template) - })), - ) - .into_response(), - Err(e) => { - // Check for unique constraint violation - let error_str = e.to_string(); - if error_str.contains("unique") || error_str.contains("duplicate") { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "A template with this name already exists" - })), - ) - .into_response(); - } - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to create template: {}", e) - })), - ) - .into_response() - } - } -} - -/// Get a specific contract type template by ID -#[utoipa::path( - get, - path = "/api/v1/contract-types/{id}", - params( - ("id" = Uuid, Path, description = "Template ID") - ), - responses( - (status = 200, description = "Template retrieved successfully", body = TemplateResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Template not found") - ), - tag = "templates" -)] -pub async fn get_template( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - match repository::get_template_for_owner(pool, id, auth.owner_id).await { - Ok(Some(template)) => ( - StatusCode::OK, - Json(serde_json::json!({ - "template": template_record_to_summary(&template) - })), - ) - .into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "code": "NOT_FOUND", - "message": "Template not found" - })), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to get template: {}", e) - })), - ) - .into_response(), - } -} - -/// Update a contract type template -#[utoipa::path( - put, - path = "/api/v1/contract-types/{id}", - params( - ("id" = Uuid, Path, description = "Template ID") - ), - request_body = UpdateTemplateRequest, - responses( - (status = 200, description = "Template updated successfully", body = TemplateResponse), - (status = 400, description = "Invalid request"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Template not found"), - (status = 409, description = "Version conflict") - ), - tag = "templates" -)] -pub async fn update_template( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - // Validate phases if provided - if let Some(ref phases) = req.phases { - if phases.is_empty() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": "Template must have at least one phase" - })), - ) - .into_response(); - } - - // If default_phase is also provided, validate it's in the phases - if let Some(ref default_phase) = req.default_phase { - if !phases.iter().any(|p| &p.id == default_phase) { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": format!("Default phase '{}' is not in the phases list", default_phase) - })), - ) - .into_response(); - } - } - } - - // Check that template name doesn't conflict with built-in types - if let Some(ref name) = req.name { - let builtin_names = ["simple", "specification", "execute"]; - if builtin_names.contains(&name.to_lowercase().as_str()) { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "Cannot rename template to a built-in type name" - })), - ) - .into_response(); - } - } - - match repository::update_template_for_owner(pool, id, auth.owner_id, req).await { - Ok(Some(template)) => ( - StatusCode::OK, - Json(serde_json::json!({ - "template": template_record_to_summary(&template) - })), - ) - .into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "code": "NOT_FOUND", - "message": "Template not found" - })), - ) - .into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "VERSION_CONFLICT", - "message": format!("Version conflict: expected {}, found {}", expected, actual), - "expectedVersion": expected, - "actualVersion": actual - })), - ) - .into_response(), - Err(e) => { - let error_str = e.to_string(); - if error_str.contains("unique") || error_str.contains("duplicate") { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "A template with this name already exists" - })), - ) - .into_response(); - } - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to update template: {}", e) - })), - ) - .into_response() - } - } -} - -/// Delete a contract type template -#[utoipa::path( - delete, - path = "/api/v1/contract-types/{id}", - params( - ("id" = Uuid, Path, description = "Template ID") - ), - responses( - (status = 204, description = "Template deleted successfully"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Template not found") - ), - tag = "templates" -)] -pub async fn delete_template( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - match repository::delete_template_for_owner(pool, id, auth.owner_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "code": "NOT_FOUND", - "message": "Template not found" - })), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to delete template: {}", e) - })), - ) - .into_response(), - } -} - -// ============================================================================= -// Helper Functions -// ============================================================================= - -/// Convert a database template record to the API template format -fn template_record_to_api(template: &ContractTypeTemplateRecord) -> ContractTypeTemplate { - ContractTypeTemplate { - id: template.id.to_string(), - name: template.name.clone(), - description: template.description.clone().unwrap_or_default(), - phases: template.phases.iter().map(|p| p.id.clone()).collect(), - default_phase: template.default_phase.clone(), - is_builtin: false, - } -} - -/// Convert a database template record to the summary format -fn template_record_to_summary(template: &ContractTypeTemplateRecord) -> ContractTypeTemplateSummary { - ContractTypeTemplateSummary { - id: template.id, - name: template.name.clone(), - description: template.description.clone(), - phases: template.phases.clone(), - default_phase: template.default_phase.clone(), - is_builtin: false, - version: template.version, - created_at: template.created_at, - } -} diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs index d987d08..62c65a6 100644 --- a/makima/src/server/handlers/transcript_analysis.rs +++ b/makima/src/server/handlers/transcript_analysis.rs @@ -280,8 +280,6 @@ pub async fn create_contract_from_analysis( phase_guard: None, local_only: None, auto_merge_local: None, - red_team_enabled: None, - red_team_prompt: None, template_id: None, }; @@ -362,7 +360,6 @@ pub async fn create_contract_from_analysis( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, priority: match item.priority.as_deref() { Some("high") => 10, @@ -537,7 +534,6 @@ pub async fn update_contract_from_analysis( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, priority: 0, merge_mode: None, diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index e5415ae..b351ac1 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_red_team, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -132,9 +132,6 @@ pub fn make_router(state: SharedState) -> Router { .route("/mesh/supervisor/questions", post(mesh_supervisor::ask_question)) .route("/mesh/questions", get(mesh_supervisor::list_pending_questions)) .route("/mesh/questions/{question_id}/answer", post(mesh_supervisor::answer_question)) - // Red team endpoints (for red team tasks to notify supervisors) - .route("/mesh/red-team/notify", post(mesh_red_team::notify_supervisor)) - .route("/mesh/red-team/status", get(mesh_red_team::get_status)) // Mesh WebSocket endpoints .route("/mesh/tasks/subscribe", get(mesh_ws::task_subscription_handler)) .route("/mesh/daemons/connect", get(mesh_daemon::daemon_handler)) @@ -216,17 +213,8 @@ pub fn make_router(state: SharedState) -> Router { ) // Timeline endpoint (unified history for user) .route("/timeline", get(history::get_timeline)) - // Contract type templates (workflow definitions) - .route( - "/contract-types", - get(templates::list_contract_types).post(templates::create_template), - ) - .route( - "/contract-types/{id}", - get(templates::get_template) - .put(templates::update_template) - .delete(templates::delete_template), - ) + // Contract type templates (built-in only) + .route("/contract-types", get(templates::list_contract_types)) // Settings endpoints .route( "/settings/repository-history", -- cgit v1.2.3