diff options
| author | soryu <soryu@soryu.co> | 2026-02-07 00:01:50 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-07 00:01:50 +0000 |
| commit | b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 (patch) | |
| tree | 95543fd150270018e384fbcf9d3df3dc45f052f6 /makima/src/server/handlers | |
| parent | cececbf326e258211ceae7afce716a5d1e46014f (diff) | |
| download | soryu-b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56.tar.gz soryu-b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56.zip | |
Remove directives for reimplementation
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 85 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 2116 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_daemon.rs | 15 | ||||
| -rw-r--r-- | makima/src/server/handlers/mod.rs | 2 |
4 files changed, 2 insertions, 2216 deletions
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 8a6ce0f..a83c72d 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -575,90 +575,7 @@ pub async fn update_contract( }), ).await; - // If contract is part of a directive chain step, update the step status - // and emit an event for the directive engine to process - let pool_for_step = pool.clone(); - let contract_id_for_step = contract.id; - tokio::spawn(async move { - // Look up the step by contract_id - match repository::get_step_by_contract_id(&pool_for_step, contract_id_for_step).await { - Ok(Some(step)) => { - // Get the chain to find the directive_id - let directive_id = match repository::get_directive_chain(&pool_for_step, step.chain_id).await { - Ok(Some(chain)) => chain.directive_id, - Ok(None) => { - tracing::warn!( - chain_id = %step.chain_id, - "Chain not found for step" - ); - return; - } - Err(e) => { - tracing::warn!( - chain_id = %step.chain_id, - error = %e, - "Failed to get chain for step" - ); - return; - } - }; - - // Update step status to 'evaluating' - if let Err(e) = repository::update_step_status(&pool_for_step, step.id, "evaluating").await { - tracing::warn!( - step_id = %step.id, - contract_id = %contract_id_for_step, - error = %e, - "Failed to update step status to evaluating" - ); - } else { - tracing::info!( - step_id = %step.id, - contract_id = %contract_id_for_step, - chain_id = %step.chain_id, - directive_id = %directive_id, - "Contract completed - step transitioned to evaluating" - ); - - // Emit directive event for contract completion - if let Err(e) = repository::emit_directive_event( - &pool_for_step, - directive_id, - Some(step.chain_id), - Some(step.id), - "contract_completed", - "info", - Some(serde_json::json!({ - "contract_id": contract_id_for_step, - "step_id": step.id, - "step_name": step.name - })), - "system", - None, - ).await { - tracing::warn!( - step_id = %step.id, - error = %e, - "Failed to emit contract_completed directive event" - ); - } - } - } - Ok(None) => { - tracing::debug!( - contract_id = %contract_id_for_step, - "Contract not linked to any directive chain step" - ); - } - Err(e) => { - tracing::warn!( - contract_id = %contract_id_for_step, - error = %e, - "Failed to look up step for completed contract" - ); - } - } - }); + // TODO: Directive engine integration (removed for reimplementation) } // Get summary with counts diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs deleted file mode 100644 index 9c65c5e..0000000 --- a/makima/src/server/handlers/directives.rs +++ /dev/null @@ -1,2116 +0,0 @@ -//! API handlers for directives. -//! -//! Provides REST endpoints for managing directives, chains, steps, -//! evaluations, events, verifiers, and approvals. - -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::{ - sse::{Event, Sse}, - IntoResponse, - }, - Json, -}; -use futures::stream; -use serde::{Deserialize, Serialize}; -use std::convert::Infallible; -use std::time::Duration; -use uuid::Uuid; - -use crate::db::models::{ - AddStepRequest, CreateDirectiveRequest, CreateVerifierRequest, ReworkStepRequest, - UpdateCriteriaRequest, UpdateDirectiveRequest, UpdateRequirementsRequest, UpdateStepRequest, - UpdateVerifierRequest, -}; -use crate::db::repository; -use crate::server::auth::Authenticated; -use crate::server::messages::ApiError; -use crate::server::state::SharedState; - -/// Query parameters for listing directives -#[derive(Debug, Deserialize)] -pub struct ListDirectivesQuery { - pub status: Option<String>, -} - -/// Query parameters for listing events -#[derive(Debug, Deserialize)] -pub struct ListEventsQuery { - pub limit: Option<i64>, -} - -/// Query parameters for SSE stream authentication -/// EventSource API cannot set custom headers, so auth is passed via query params -#[derive(Debug, Deserialize)] -pub struct StreamAuthQuery { - pub token: Option<String>, - pub api_key: Option<String>, -} - -/// Query parameters for listing evaluations -#[derive(Debug, Deserialize)] -pub struct ListEvaluationsQuery { - pub limit: Option<i64>, - #[serde(rename = "stepId")] - pub step_id: Option<Uuid>, -} - -/// Response for directive creation -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateDirectiveResponse { - pub id: Uuid, - pub title: String, - pub status: String, -} - -/// Response for approval actions -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApprovalActionRequest { - pub response: Option<String>, -} - -// ============================================================================= -// Directive CRUD -// ============================================================================= - -/// Create a new directive -/// POST /api/v1/directives -pub async fn create_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Json(req): Json<CreateDirectiveRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::create_directive_for_owner(pool, auth.owner_id, req).await { - Ok(directive) => Json(CreateDirectiveResponse { - id: directive.id, - title: directive.title, - status: directive.status, - }) - .into_response(), - Err(e) => { - tracing::error!("Failed to create directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// List directives for the authenticated owner -/// GET /api/v1/directives -pub async fn list_directives( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Query(params): Query<ListDirectivesQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::list_directives_for_owner(pool, auth.owner_id, params.status.as_deref()).await - { - Ok(directives) => { - let total = directives.len() as i64; - Json(serde_json::json!({ - "directives": directives, - "total": total, - })) - .into_response() - } - Err(e) => { - tracing::error!("Failed to list directives: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get a directive with progress details -/// GET /api/v1/directives/:id -pub async fn get_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a directive -/// PUT /api/v1/directives/:id -pub async fn update_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateDirectiveRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, req).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!( - "Version conflict: expected {}, got {}", - expected, actual - ), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Archive a directive -/// DELETE /api/v1/directives/:id -pub async fn archive_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::archive_directive_for_owner(pool, id, auth.owner_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to archive directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Directive Lifecycle -// ============================================================================= - -/// Start a directive (generate chain and begin execution) -/// POST /api/v1/directives/:id/start -pub async fn start_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Start directive via orchestration engine - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.start_directive(id).await { - Ok(planning) => { - // Auto-start the planning task on an available daemon - if let Some(daemon_id) = state.find_alternative_daemon(auth.owner_id, &[]) { - // Update task status to "starting" and assign daemon - let update_req = crate::db::models::UpdateTaskRequest { - status: Some("starting".to_string()), - daemon_id: Some(daemon_id), - ..Default::default() - }; - if let Err(e) = repository::update_task_for_owner( - pool, - planning.task_id, - auth.owner_id, - update_req, - ) - .await - { - tracing::warn!("Failed to update planning task status: {}", e); - } - - let command = crate::server::state::DaemonCommand::SpawnTask { - task_id: planning.task_id, - task_name: planning.task_name, - plan: planning.plan, - repo_url: planning.repository_url, - base_branch: planning.base_branch, - target_branch: None, - parent_task_id: None, - depth: 0, - is_orchestrator: false, - target_repo_path: None, - completion_action: Some("none".to_string()), - continue_from_task_id: None, - copy_files: None, - contract_id: Some(planning.contract_id), - is_supervisor: true, - autonomous_loop: false, - resume_session: false, - conversation_history: None, - patch_data: None, - patch_base_sha: None, - local_only: false, - auto_merge_local: false, - supervisor_worktree_task_id: None, - }; - - if let Err(e) = state.send_daemon_command(daemon_id, command).await { - tracing::warn!( - "Failed to auto-start planning task on daemon {}: {}", - daemon_id, - e - ); - } else { - tracing::info!( - "Auto-started planning task {} on daemon {} for directive {}", - planning.task_id, - daemon_id, - id - ); - } - } else { - tracing::warn!( - "No daemon available to auto-start planning task for directive {}", - id - ); - } - - // Return the updated directive with progress - match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to start directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("START_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Pause a directive -/// POST /api/v1/directives/:id/pause -pub async fn pause_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.pause_directive(id).await { - Ok(()) => match repository::get_directive(pool, id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to pause directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("PAUSE_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Resume a paused directive -/// POST /api/v1/directives/:id/resume -pub async fn resume_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.resume_directive(id).await { - Ok(()) => match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to resume directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("RESUME_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Stop a directive (cannot be resumed) -/// POST /api/v1/directives/:id/stop -pub async fn stop_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.stop_directive(id).await { - Ok(()) => match repository::get_directive(pool, id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to stop directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("STOP_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Chain Management -// ============================================================================= - -/// Get current chain for a directive -/// GET /api/v1/directives/:id/chain -pub async fn get_chain( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => { - match repository::list_chain_steps(pool, chain.id).await { - Ok(steps) => Json(serde_json::json!({ - "chain": chain, - "steps": steps, - })) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get chain: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get chain graph for DAG visualization -/// GET /api/v1/directives/:id/chain/graph -pub async fn get_chain_graph( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Get current chain - let chain = match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => chain, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - match repository::get_chain_graph(pool, chain.id).await { - Ok(graph) => Json(graph).into_response(), - Err(e) => { - tracing::error!("Failed to get chain graph: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Regenerate chain (force replan) -/// POST /api/v1/directives/:id/chain/replan -pub async fn replan_chain( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.regenerate_chain(id, "Manual replan requested").await { - Ok(new_chain_id) => Json(serde_json::json!({ - "chainId": new_chain_id, - "message": "Chain regenerated successfully", - })) - .into_response(), - Err(e) => { - tracing::error!("Failed to replan chain: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("REPLAN_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Step Management -// ============================================================================= - -/// Add a step to the current chain -/// POST /api/v1/directives/:id/chain/steps -pub async fn add_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<AddStepRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Get current chain - let chain = match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => chain, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - match repository::create_chain_step(pool, chain.id, req).await { - Ok(step) => (StatusCode::CREATED, Json(step)).into_response(), - Err(e) => { - tracing::error!("Failed to add step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get step details -/// GET /api/v1/directives/:id/chain/steps/:step_id -pub async fn get_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a step -/// PUT /api/v1/directives/:id/chain/steps/:step_id -pub async fn update_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, - Json(req): Json<UpdateStepRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_chain_step(pool, step_id, req).await { - Ok(step) => Json(step).into_response(), - Err(e) => { - tracing::error!("Failed to update step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Delete a step -/// DELETE /api/v1/directives/:id/chain/steps/:step_id -pub async fn delete_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::delete_chain_step(pool, step_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to delete step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Skip a step -/// POST /api/v1/directives/:id/chain/steps/:step_id/skip -pub async fn skip_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_step_status(pool, step_id, "skipped").await { - Ok(step) => Json(step).into_response(), - Err(e) => { - tracing::error!("Failed to skip step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Evaluations -// ============================================================================= - -/// List evaluations for a directive -/// GET /api/v1/directives/:id/evaluations -pub async fn list_evaluations( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Query(params): Query<ListEvaluationsQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let result = if let Some(step_id) = params.step_id { - repository::list_step_evaluations(pool, step_id).await - } else { - repository::list_directive_evaluations(pool, id, params.limit).await - }; - - match result { - Ok(evaluations) => Json(evaluations).into_response(), - Err(e) => { - tracing::error!("Failed to list evaluations: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Events -// ============================================================================= - -/// List events for a directive -/// GET /api/v1/directives/:id/events -pub async fn list_events( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Query(params): Query<ListEventsQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_directive_events(pool, id, params.limit).await { - Ok(events) => Json(events).into_response(), - Err(e) => { - tracing::error!("Failed to list events: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// SSE stream of events for a directive -/// GET /api/v1/directives/:id/events/stream -/// -/// EventSource API cannot set custom headers, so authentication is accepted -/// via query parameters: ?token=<jwt> or ?api_key=<key> -pub async fn stream_events( - State(state): State<SharedState>, - Path(id): Path<Uuid>, - Query(auth_params): Query<StreamAuthQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Authenticate via query params (EventSource cannot set headers) - let auth = if let Some(ref token) = auth_params.token { - // JWT token - let verifier = match state.jwt_verifier.as_ref() { - Some(v) => v, - None => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("AUTH_NOT_CONFIGURED", "Authentication not configured")), - ) - .into_response() - } - }; - let claims = match verifier.verify(token) { - Ok(c) => c, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("INVALID_TOKEN", "Invalid authentication token")), - ) - .into_response() - } - }; - match crate::server::auth::resolve_owner_id_public(pool, claims.sub, claims.email.as_deref()).await { - Ok(owner_id) => crate::server::auth::AuthenticatedUser { - user_id: claims.sub, - owner_id, - auth_source: crate::server::auth::AuthSource::Jwt, - email: claims.email, - }, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("USER_NOT_FOUND", "User not found")), - ) - .into_response() - } - } - } else if let Some(ref api_key) = auth_params.api_key { - // API key - match crate::server::auth::validate_api_key_public(pool, api_key).await { - Ok((user_id, owner_id)) => crate::server::auth::AuthenticatedUser { - user_id, - owner_id, - auth_source: crate::server::auth::AuthSource::ApiKey, - email: None, - }, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("INVALID_API_KEY", "Invalid or revoked API key")), - ) - .into_response() - } - } - } else { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("MISSING_TOKEN", "Authentication required via ?token= or ?api_key= query parameter")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Create SSE stream that polls for new events - let pool_clone = pool.clone(); - let stream = stream::unfold( - (pool_clone, id, None::<chrono::DateTime<chrono::Utc>>), - move |(pool, directive_id, last_seen)| async move { - // Wait a bit before next poll - tokio::time::sleep(Duration::from_secs(1)).await; - - // Get recent events - let events = repository::list_directive_events(&pool, directive_id, Some(10)) - .await - .unwrap_or_default(); - - // Filter to only new events - let new_events: Vec<_> = events - .into_iter() - .filter(|e| last_seen.map(|ls| e.created_at > ls).unwrap_or(true)) - .collect(); - - let new_last_seen = new_events.first().map(|e| e.created_at).or(last_seen); - - // Convert to SSE events - let sse_events: Vec<Result<Event, Infallible>> = new_events - .into_iter() - .map(|e| { - Ok(Event::default() - .event("directive_event") - .data(serde_json::to_string(&e).unwrap_or_default())) - }) - .collect(); - - Some((stream::iter(sse_events), (pool, directive_id, new_last_seen))) - }, - ); - - use futures::StreamExt; - Sse::new(stream.flatten()) - .keep_alive( - axum::response::sse::KeepAlive::new() - .interval(Duration::from_secs(15)) - .text("keepalive"), - ) - .into_response() -} - -// ============================================================================= -// Verifiers -// ============================================================================= - -/// List verifiers for a directive -/// GET /api/v1/directives/:id/verifiers -pub async fn list_verifiers( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_directive_verifiers(pool, id).await { - Ok(verifiers) => Json(verifiers).into_response(), - Err(e) => { - tracing::error!("Failed to list verifiers: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Add a verifier to a directive -/// POST /api/v1/directives/:id/verifiers -pub async fn add_verifier( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<CreateVerifierRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::create_directive_verifier( - pool, - id, - &req.name, - &req.verifier_type, - req.command.as_deref(), - req.working_directory.as_deref(), - false, // auto_detect - vec![], // detect_files - req.weight.unwrap_or(1.0), - req.required.unwrap_or(false), - ) - .await - { - Ok(verifier) => (StatusCode::CREATED, Json(verifier)).into_response(), - Err(e) => { - tracing::error!("Failed to add verifier: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a verifier -/// PUT /api/v1/directives/:id/verifiers/:verifier_id -pub async fn update_verifier( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, verifier_id)): Path<(Uuid, Uuid)>, - Json(req): Json<UpdateVerifierRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_directive_verifier( - pool, - verifier_id, - req.enabled, - req.command.as_deref(), - req.weight, - req.required, - ) - .await - { - Ok(verifier) => Json(verifier).into_response(), - Err(e) => { - tracing::error!("Failed to update verifier: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Approvals -// ============================================================================= - -/// List pending approvals for a directive -/// GET /api/v1/directives/:id/approvals -pub async fn list_approvals( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_pending_approvals(pool, id).await { - Ok(approvals) => Json(approvals).into_response(), - Err(e) => { - tracing::error!("Failed to list approvals: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Approve a pending approval request -/// POST /api/v1/directives/:id/approvals/:approval_id/approve -pub async fn approve_request( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, approval_id)): Path<(Uuid, Uuid)>, - Json(req): Json<ApprovalActionRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine - .on_approval_resolved(approval_id, true, auth.owner_id) - .await - { - Ok(()) => { - match repository::resolve_approval( - pool, - approval_id, - "approved", - req.response.as_deref(), - auth.owner_id, - ) - .await - { - Ok(approval) => Json(approval).into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to process approval: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("APPROVAL_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Deny a pending approval request -/// POST /api/v1/directives/:id/approvals/:approval_id/deny -pub async fn deny_request( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, approval_id)): Path<(Uuid, Uuid)>, - Json(req): Json<ApprovalActionRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine - .on_approval_resolved(approval_id, false, auth.owner_id) - .await - { - Ok(()) => { - match repository::resolve_approval( - pool, - approval_id, - "denied", - req.response.as_deref(), - auth.owner_id, - ) - .await - { - Ok(approval) => Json(approval).into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to process denial: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("DENIAL_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Step Evaluation & Rework -// ============================================================================= - -/// Force re-evaluation of a step -/// POST /api/v1/directives/:id/steps/:step_id/evaluate -pub async fn evaluate_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Set step to evaluating status - match repository::update_step_status(pool, step_id, "evaluating").await { - Ok(_) => {} - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Trigger evaluation via engine - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.on_contract_completed(step_id).await { - Ok(()) => { - // Return updated step - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to evaluate step: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("EVALUATE_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Trigger manual rework for a step -/// POST /api/v1/directives/:id/steps/:step_id/rework -pub async fn rework_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, - Json(req): Json<ReworkStepRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Set step to rework status and increment rework count - match repository::update_step_status(pool, step_id, "rework").await { - Ok(_) => {} - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let _ = repository::increment_step_rework_count(pool, step_id).await; - - // Emit rework event - let _ = repository::emit_directive_event( - pool, - id, - None, - Some(step_id), - "step_rework", - "info", - Some(serde_json::json!({ - "step_id": step_id, - "instructions": req.instructions, - "initiated_by": "user", - })), - "user", - Some(auth.owner_id), - ) - .await; - - // Return updated step - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } -} - -// ============================================================================= -// Auto-detect Verifiers -// ============================================================================= - -/// Auto-detect verifiers for a directive based on repository content -/// POST /api/v1/directives/:id/verifiers/auto-detect -pub async fn auto_detect_verifiers( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Get repository path - let repo_path = directive - .local_path - .as_ref() - .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::path::PathBuf::from(".")); - - // Auto-detect verifiers - let detected = crate::orchestration::auto_detect_verifiers(&repo_path).await; - - // Save detected verifiers to the database - let mut created = Vec::new(); - for verifier in &detected { - let info = verifier.info(); - match repository::create_directive_verifier( - pool, - id, - &info.name, - &info.verifier_type, - Some(&info.command), - info.working_directory.as_deref(), - true, // auto_detect - info.detect_files.clone(), - info.weight, - info.required, - ) - .await - { - Ok(v) => created.push(v), - Err(e) => { - tracing::warn!("Failed to create detected verifier '{}': {}", info.name, e); - } - } - } - - Json(serde_json::json!({ - "detected": created.len(), - "verifiers": created, - })) - .into_response() -} - -// ============================================================================= -// Requirements & Criteria -// ============================================================================= - -/// Update directive requirements -/// PUT /api/v1/directives/:id/requirements -pub async fn update_requirements( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateRequirementsRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check to get current version - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Build update request with just requirements - let update = UpdateDirectiveRequest { - title: None, - goal: None, - requirements: Some(serde_json::to_value(&req.requirements).unwrap_or_default()), - acceptance_criteria: None, - constraints: None, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!("Version conflict: expected {}, got {}", expected, actual), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update requirements: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update directive acceptance criteria -/// PUT /api/v1/directives/:id/criteria -pub async fn update_criteria( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateCriteriaRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check to get current version - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Build update request with just acceptance criteria - let update = UpdateDirectiveRequest { - title: None, - goal: None, - requirements: None, - acceptance_criteria: Some( - serde_json::to_value(&req.acceptance_criteria).unwrap_or_default(), - ), - constraints: None, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!("Version conflict: expected {}, got {}", expected, actual), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update criteria: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Spec Generation -// ============================================================================= - -/// Generate a specification from the directive's goal using LLM -/// POST /api/v1/directives/:id/generate-spec -pub async fn generate_spec( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Use the planner to generate a spec from the goal - let planner = crate::orchestration::ChainPlanner::new(pool.clone()); - match planner.generate_spec(&directive).await { - Ok(spec) => { - // Update the directive with the generated spec - let update = UpdateDirectiveRequest { - title: spec.title, - goal: None, - requirements: Some(spec.requirements), - acceptance_criteria: Some(spec.acceptance_criteria), - constraints: spec.constraints, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(updated) => Json(updated).into_response(), - Err(e) => { - tracing::error!("Failed to save generated spec: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - } - Err(e) => { - tracing::error!("Failed to generate spec: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("SPEC_GENERATION_FAILED", &e.to_string())), - ) - .into_response() - } - } -} diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index 9938145..beb4c15 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -1303,20 +1303,7 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re }), ).await; - // Check if this task's contract is a directive orchestrator - if let Some(contract_id) = updated_task.contract_id { - if let Ok(Some(directive)) = repository::get_directive_by_orchestrator_contract_id( - &pool, contract_id - ).await { - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - if let Err(e) = engine.on_planning_complete(directive.id, success).await { - tracing::error!( - "Failed to handle planning completion for directive {}: {}", - directive.id, e - ); - } - } - } + // TODO: Directive engine integration (removed for reimplementation) } Ok(None) => { tracing::warn!( diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index d3fabf7..ae370c9 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -1,8 +1,6 @@ //! HTTP and WebSocket request handlers. pub mod api_keys; -// pub mod chains; // Removed - replaced by directives -pub mod directives; pub mod chat; pub mod contract_chat; pub mod contract_daemon; |
