summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/directives.rs440
-rw-r--r--makima/src/server/handlers/mod.rs1
-rw-r--r--makima/src/server/mod.rs15
-rw-r--r--makima/src/server/openapi.rs41
4 files changed, 486 insertions, 11 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
new file mode 100644
index 0000000..a74f8ff
--- /dev/null
+++ b/makima/src/server/handlers/directives.rs
@@ -0,0 +1,440 @@
+//! HTTP handlers for directive CRUD operations.
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use uuid::Uuid;
+
+use crate::db::models::{
+ ChainWithSteps, CreateDirectiveRequest, Directive, DirectiveChain,
+ DirectiveListResponse, DirectiveWithChains, UpdateDirectiveRequest,
+};
+use crate::db::repository::{self, RepositoryError};
+use crate::server::auth::Authenticated;
+use crate::server::messages::ApiError;
+use crate::server::state::SharedState;
+
+/// List all directives for the authenticated user's owner.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives",
+ responses(
+ (status = 200, description = "List of directives", body = DirectiveListResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Directives"
+)]
+pub async fn list_directives(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+) -> 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).await {
+ Ok(directives) => {
+ let total = directives.len() as i64;
+ Json(DirectiveListResponse { directives, 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 by ID with its chains.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID")
+ ),
+ responses(
+ (status = 200, description = "Directive details with chains", body = DirectiveWithChains),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Directives"
+)]
+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();
+ };
+
+ 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) => {
+ tracing::error!("Failed to get directive {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ let chains = match repository::list_chains_for_directive(pool, id).await {
+ Ok(c) => c,
+ Err(e) => {
+ tracing::warn!("Failed to get chains for directive {}: {}", id, e);
+ Vec::new()
+ }
+ };
+
+ Json(DirectiveWithChains { directive, chains }).into_response()
+}
+
+/// Create a new directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives",
+ request_body = CreateDirectiveRequest,
+ responses(
+ (status = 201, description = "Directive created", body = Directive),
+ (status = 400, description = "Invalid request", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "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) => (StatusCode::CREATED, Json(directive)).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()
+ }
+ }
+}
+
+/// Update an existing directive.
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID")
+ ),
+ request_body = UpdateDirectiveRequest,
+ responses(
+ (status = 200, description = "Directive updated", body = Directive),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 409, description = "Version conflict", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Directives"
+)]
+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(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(RepositoryError::VersionConflict { expected, actual }) => (
+ StatusCode::CONFLICT,
+ Json(ApiError::new(
+ "VERSION_CONFLICT",
+ format!(
+ "Version conflict: expected {}, actual {}",
+ expected, actual
+ ),
+ )),
+ )
+ .into_response(),
+ Err(RepositoryError::Database(e)) => {
+ tracing::error!("Failed to update directive {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a directive.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID")
+ ),
+ responses(
+ (status = 204, description = "Directive deleted"),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Directives"
+)]
+pub async fn delete_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::delete_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 delete directive {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// List chains for a directive.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/chains",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID")
+ ),
+ responses(
+ (status = 200, description = "List of chains", body = Vec<DirectiveChain>),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Directives"
+)]
+pub async fn list_chains(
+ 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 directive exists and belongs to owner
+ 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) => {
+ tracing::error!("Failed to get directive {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::list_chains_for_directive(pool, id).await {
+ Ok(chains) => Json(chains).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list chains for directive {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a chain with its steps.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/chains/{chain_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("chain_id" = Uuid, Path, description = "Chain ID")
+ ),
+ responses(
+ (status = 200, description = "Chain with steps", body = ChainWithSteps),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Chain not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Directives"
+)]
+pub async fn get_chain(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, chain_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 directive exists and belongs to owner
+ 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) => {
+ tracing::error!("Failed to get directive {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ // Get the chain and verify it belongs to this directive
+ let chains = match repository::list_chains_for_directive(pool, id).await {
+ Ok(c) => c,
+ Err(e) => {
+ tracing::error!("Failed to list chains: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ let chain = match chains.into_iter().find(|c| c.id == chain_id) {
+ Some(c) => c,
+ None => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Chain not found")),
+ )
+ .into_response();
+ }
+ };
+
+ let steps = match repository::list_steps_for_chain(pool, chain_id).await {
+ Ok(s) => s,
+ Err(e) => {
+ tracing::warn!("Failed to get steps for chain {}: {}", chain_id, e);
+ Vec::new()
+ }
+ };
+
+ Json(ChainWithSteps { chain, steps }).into_response()
+}
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index ae370c9..29cd09f 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -6,6 +6,7 @@ pub mod contract_chat;
pub mod contract_daemon;
pub mod contract_discuss;
pub mod contracts;
+pub mod directives;
pub mod file_ws;
pub mod files;
pub mod history;
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index b7a4156..a429612 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
-use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions};
+use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions};
use crate::server::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -170,6 +170,19 @@ pub fn make_router(state: SharedState) -> Router {
"/contracts/{id}/chat/history",
get(contract_chat::get_contract_chat_history).delete(contract_chat::clear_contract_chat_history),
)
+ // Directive endpoints
+ .route(
+ "/directives",
+ get(directives::list_directives).post(directives::create_directive),
+ )
+ .route(
+ "/directives/{id}",
+ get(directives::get_directive)
+ .put(directives::update_directive)
+ .delete(directives::delete_directive),
+ )
+ .route("/directives/{id}/chains", get(directives::list_chains))
+ .route("/directives/{id}/chains/{chain_id}", get(directives::get_chain))
// Contract supervisor resume endpoints
.route("/contracts/{id}/supervisor/resume", post(mesh_supervisor::resume_supervisor))
.route("/contracts/{id}/supervisor/conversation/rewind", post(mesh_supervisor::rewind_conversation))
diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs
index a70342b..0e6912a 100644
--- a/makima/src/server/openapi.rs
+++ b/makima/src/server/openapi.rs
@@ -4,23 +4,25 @@ use utoipa::OpenApi;
use crate::db::models::{
AddLocalRepositoryRequest, AddRemoteRepositoryRequest, BranchInfo, BranchListResponse,
- BranchTaskRequest, BranchTaskResponse, ChangePhaseRequest, Contract,
- ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, ContractListResponse,
- ContractRepository, ContractSummary, ContractWithRelations, CreateContractRequest,
- CreateFileRequest, CreateManagedRepositoryRequest, CreateTaskRequest, Daemon,
- DaemonDirectoriesResponse, DaemonDirectory, DaemonListResponse, File, FileListResponse,
- FileSummary, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest,
- MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse,
- MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, RepositoryHistoryEntry,
+ BranchTaskRequest, BranchTaskResponse, ChainStep, ChainWithSteps, ChangePhaseRequest,
+ Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent,
+ ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations,
+ CreateContractRequest, CreateDirectiveRequest, CreateFileRequest,
+ CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse,
+ DaemonDirectory, DaemonListResponse, Directive, DirectiveChain, DirectiveListResponse,
+ DirectiveSummary, DirectiveWithChains, File, FileListResponse, FileSummary,
+ MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse,
+ MergeSkipRequest, MergeStartRequest, MergeStatusResponse, MeshChatConversation,
+ MeshChatHistoryResponse, MeshChatMessageRecord, RepositoryHistoryEntry,
RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest, Task,
TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry,
- UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
+ UpdateContractRequest, UpdateDirectiveRequest, UpdateFileRequest, UpdateTaskRequest,
};
use crate::server::auth::{
ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse,
RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse,
};
-use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
+use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, directives, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage};
#[derive(OpenApi)]
@@ -103,6 +105,14 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
repository_history::list_repository_history,
repository_history::get_repository_suggestions,
repository_history::delete_repository_history,
+ // Directive endpoints
+ directives::list_directives,
+ directives::get_directive,
+ directives::create_directive,
+ directives::update_directive,
+ directives::delete_directive,
+ directives::list_chains,
+ directives::get_chain,
),
components(
schemas(
@@ -187,6 +197,16 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
RepositoryHistoryEntry,
RepositoryHistoryListResponse,
RepositorySuggestionsQuery,
+ // Directive schemas
+ Directive,
+ DirectiveSummary,
+ DirectiveListResponse,
+ DirectiveWithChains,
+ DirectiveChain,
+ ChainStep,
+ ChainWithSteps,
+ CreateDirectiveRequest,
+ UpdateDirectiveRequest,
)
),
tags(
@@ -197,6 +217,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
(name = "API Keys", description = "API key management for programmatic access"),
(name = "Users", description = "User account management"),
(name = "Settings", description = "User settings including repository history"),
+ (name = "Directives", description = "Directive management for autonomous goal-driven execution"),
)
)]
pub struct ApiDoc;