Files
crank/apps/admin-api/src/routes/agents.rs
T

342 lines
10 KiB
Rust

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<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, 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<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<ToolSearchPreviewPayload>,
) -> Result<Json<Value>, 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<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<AgentPayload>,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiError> {
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<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<UpdateAgentPayload>,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, 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<WorkspaceAgentVersionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<AgentCatalogPayload>,
) -> Result<Json<Value>, 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<crank_registry::AgentStateExpectation, ApiError> {
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<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<PublishPayload>,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, 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<WorkspaceAgentPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<PlatformApiKeyPayload>,
) -> Result<Json<Value>, 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<WorkspaceAgentPlatformApiKeyPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
) -> Result<axum::http::StatusCode, ApiError> {
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<WorkspaceAgentPlatformApiKeyPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
) -> Result<axum::http::StatusCode, ApiError> {
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)
}