summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/directives.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
-rw-r--r--makima/src/server/handlers/directives.rs2116
1 files changed, 0 insertions, 2116 deletions
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()
- }
- }
-}