use axum::{ Extension, Json, extract::{Path, State}, http::{HeaderMap, header}, response::IntoResponse, }; use serde::Deserialize; use serde_json::{Value, json}; use crate::{ auth::AuthenticatedSession, error::ApiError, request_context::RequestContext, service::{ AdminAuditContext, AgentCatalogPayload, AgentPayload, PlatformApiKeyPayload, PublishPayload, ToolSearchPreviewPayload, UpdateAgentPayload, }, state::AppState, }; #[derive(Deserialize)] pub struct WorkspacePath { pub workspace_id: String, } #[derive(Deserialize)] pub struct WorkspaceAgentPath { pub workspace_id: String, pub agent_id: String, } #[derive(Deserialize)] pub struct WorkspaceAgentVersionPath { pub workspace_id: String, pub agent_id: String, pub version: u32, } #[derive(Deserialize)] pub struct WorkspaceAgentPlatformApiKeyPath { pub workspace_id: String, pub agent_id: String, pub key_id: String, } pub async fn list_agents( Path(path): Path, State(state): State, ) -> Result, ApiError> { let items = state .service .list_agents(&path.workspace_id.as_str().into()) .await?; Ok(Json(json!({ "items": items }))) } pub async fn preview_tool_search( Path(path): Path, State(state): State, Json(payload): Json, ) -> Result, ApiError> { let items = state .service .preview_tool_search(&path.workspace_id.as_str().into(), payload) .await?; Ok(Json(json!({"items": items}))) } pub async fn create_agent( Path(path): Path, State(state): State, Json(payload): Json, ) -> Result, ApiError> { let created = state .service .create_agent(&path.workspace_id.as_str().into(), payload) .await?; Ok(Json(json!(created))) } pub async fn get_agent( Path(path): Path, State(state): State, ) -> Result { let agent = state .service .get_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), ) .await?; let etag = crate::service::AdminService::agent_state_etag(&agent); let mut headers = HeaderMap::new(); headers.insert( header::ETAG, header::HeaderValue::from_str(&etag) .map_err(|_| ApiError::internal("invalid agent etag"))?, ); Ok((headers, Json(json!(agent)))) } pub async fn update_agent( Path(path): Path, State(state): State, headers: HeaderMap, Json(payload): Json, ) -> Result, ApiError> { let expected_state = require_agent_precondition(&state, &path, &headers).await?; let updated = state .service .update_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), payload, Some(&expected_state), ) .await?; Ok(Json(json!(updated))) } pub async fn delete_agent( Path(path): Path, State(state): State, headers: HeaderMap, ) -> Result, ApiError> { let expected_state = require_agent_precondition(&state, &path, &headers).await?; let deleted = state .service .delete_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), Some(&expected_state), ) .await?; Ok(Json(json!(deleted))) } pub async fn get_agent_version( Path(path): Path, State(state): State, ) -> Result, ApiError> { let version = state .service .get_agent_version( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), path.version, ) .await?; Ok(Json(json!(version))) } pub async fn save_agent_bindings( Path(path): Path, State(state): State, headers: HeaderMap, Json(payload): Json, ) -> Result, ApiError> { let expected_state = require_agent_precondition(&state, &path, &headers).await?; let record = state .service .save_agent_bindings( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), payload, Some(&expected_state), ) .await?; Ok(Json(json!(record))) } async fn require_agent_precondition( state: &AppState, path: &WorkspaceAgentPath, headers: &HeaderMap, ) -> Result { let agent = state .service .get_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), ) .await?; let expected = crate::service::AdminService::agent_state_etag(&agent); let provided = headers .get(header::IF_MATCH) .and_then(|value| value.to_str().ok()) .ok_or_else(|| { ApiError::precondition_required_with_context( "If-Match is required for Agent mutation", json!({ "error_code": "agent_precondition_required", "current_version": agent.current_draft_version, "latest_published_version": agent.latest_published_version, "catalog_revision": agent.catalog_revision, "recovery": "reload" }), ) })?; if provided != expected { return Err(ApiError::conflict_with_context( "Agent state changed; reload before retrying", json!({ "error_code": "agent_stale_revision", "current_version": agent.current_draft_version, "latest_published_version": agent.latest_published_version, "catalog_revision": agent.catalog_revision, "recovery": "reload" }), )); } crate::service::AdminService::agent_state_expectation(&agent) } pub async fn publish_agent( Path(path): Path, State(state): State, headers: HeaderMap, Json(payload): Json, ) -> Result, ApiError> { let expected_state = require_agent_precondition(&state, &path, &headers).await?; let published = state .service .publish_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), payload.version, Some(&expected_state), ) .await?; Ok(Json(json!(published))) } pub async fn unpublish_agent( Path(path): Path, State(state): State, headers: HeaderMap, ) -> Result, ApiError> { let expected_state = require_agent_precondition(&state, &path, &headers).await?; let updated = state .service .unpublish_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), Some(&expected_state), ) .await?; Ok(Json(json!(updated))) } pub async fn archive_agent( Path(path): Path, State(state): State, headers: HeaderMap, ) -> Result, ApiError> { let expected_state = require_agent_precondition(&state, &path, &headers).await?; let updated = state .service .archive_agent( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), Some(&expected_state), ) .await?; Ok(Json(json!(updated))) } pub async fn list_agent_platform_api_keys( Path(path): Path, State(state): State, ) -> Result, ApiError> { let items = state .service .list_agent_platform_api_keys( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), ) .await?; Ok(Json(json!({ "items": items }))) } pub async fn create_agent_platform_api_key( Path(path): Path, State(state): State, Extension(session): Extension, Extension(request_context): Extension, Json(payload): Json, ) -> Result, ApiError> { let audit_context = AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation); let created = state .service .create_agent_platform_api_key( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), payload, Some(&audit_context), ) .await?; Ok(Json(json!(created))) } pub async fn revoke_agent_platform_api_key( Path(path): Path, State(state): State, Extension(session): Extension, Extension(request_context): Extension, ) -> Result { let audit_context = AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation); state .service .revoke_agent_platform_api_key( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), &path.key_id.as_str().into(), Some(&audit_context), ) .await?; Ok(axum::http::StatusCode::NO_CONTENT) } pub async fn delete_agent_platform_api_key( Path(path): Path, State(state): State, Extension(session): Extension, Extension(request_context): Extension, ) -> Result { let audit_context = AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation); state .service .delete_agent_platform_api_key( &path.workspace_id.as_str().into(), &path.agent_id.as_str().into(), &path.key_id.as_str().into(), Some(&audit_context), ) .await?; Ok(axum::http::StatusCode::NO_CONTENT) }