Expose approval requests in admin logs
This commit is contained in:
@@ -19,7 +19,10 @@ use crate::{
|
||||
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
||||
capabilities::get_capabilities,
|
||||
imports::{create_openapi_import, preview_openapi_import},
|
||||
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
|
||||
observability::{
|
||||
get_agent_usage, get_approval, get_log, get_operation_usage, get_usage, list_approvals,
|
||||
list_logs,
|
||||
},
|
||||
operations::{
|
||||
analyze_operation_quality, archive_operation, create_operation, create_version,
|
||||
delete_operation, export_operation, generate_draft, get_operation,
|
||||
@@ -123,6 +126,8 @@ pub fn build_app(state: AppState) -> Router {
|
||||
.route("/export", get(export_workspace))
|
||||
.route("/logs", get(list_logs))
|
||||
.route("/logs/{log_id}", get(get_log))
|
||||
.route("/approvals", get(list_approvals))
|
||||
.route("/approvals/{approval_id}", get(get_approval))
|
||||
.route("/usage", get(get_usage))
|
||||
.route("/usage/operations/{operation_id}", get(get_operation_usage))
|
||||
.route("/usage/agents/{agent_id}", get(get_agent_usage));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crank_core::{
|
||||
AgentId, AgentStatus, AuthConfig, AuthKind, ExecutionMode, ExportMode, GeneratedDraft,
|
||||
InvocationLevel, InvocationSource, InvocationStatus, OperationSecurityLevel, OperationStatus,
|
||||
PlatformApiKeyKind, PlatformApiKeyScope, Protocol, SecretKind, Target, UsagePeriod,
|
||||
WizardState, WorkspaceId, WorkspaceStatus,
|
||||
AgentId, AgentStatus, ApprovalRequestStatus, AuthConfig, AuthKind, ExecutionMode, ExportMode,
|
||||
GeneratedDraft, InvocationLevel, InvocationSource, InvocationStatus, OperationSecurityLevel,
|
||||
OperationStatus, PlatformApiKeyKind, PlatformApiKeyScope, Protocol, SecretKind, Target,
|
||||
UsagePeriod, WizardState, WorkspaceId, WorkspaceStatus,
|
||||
};
|
||||
use crank_mapping::MappingSet;
|
||||
use crank_registry::{
|
||||
@@ -250,6 +250,12 @@ pub struct LogsQuery {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ApprovalsQuery {
|
||||
pub status: Option<ApprovalRequestStatus>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UsageRequestQuery {
|
||||
pub period: Option<UsagePeriod>,
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde_json::{Value, json};
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
routes::access::WorkspacePath,
|
||||
service::{LogsQuery, UsageRequestQuery},
|
||||
service::{ApprovalsQuery, LogsQuery, UsageRequestQuery},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,12 @@ pub struct WorkspaceLogPath {
|
||||
pub log_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct WorkspaceApprovalPath {
|
||||
pub workspace_id: String,
|
||||
pub approval_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct WorkspaceOperationUsagePath {
|
||||
pub workspace_id: String,
|
||||
@@ -55,6 +61,32 @@ pub async fn get_log(
|
||||
Ok(Json(json!(item)))
|
||||
}
|
||||
|
||||
pub async fn list_approvals(
|
||||
Path(path): Path<WorkspacePath>,
|
||||
Query(query): Query<ApprovalsQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let items = state
|
||||
.service
|
||||
.list_approvals(&path.workspace_id.as_str().into(), query)
|
||||
.await?;
|
||||
Ok(Json(json!({ "items": items })))
|
||||
}
|
||||
|
||||
pub async fn get_approval(
|
||||
Path(path): Path<WorkspaceApprovalPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let item = state
|
||||
.service
|
||||
.get_approval(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.approval_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(item)))
|
||||
}
|
||||
|
||||
pub async fn get_usage(
|
||||
Path(path): Path<WorkspacePath>,
|
||||
Query(query): Query<UsageRequestQuery>,
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
use crank_core::{AgentId, InvocationLogId, OperationId, UsagePeriod, WorkspaceId};
|
||||
use crank_registry::{InvocationLogRecord, ListInvocationLogsQuery, UsageQuery, UsageRollupRecord};
|
||||
use crank_core::{
|
||||
AgentId, ApprovalRequestId, ApprovalRequestStatus, InvocationLogId, OperationId, UsagePeriod,
|
||||
WorkspaceId,
|
||||
};
|
||||
use crank_registry::{
|
||||
ApprovalRequestRecord, ExpireApprovalRequest, InvocationLogRecord, ListApprovalRequestsQuery,
|
||||
ListInvocationLogsQuery, UsageQuery, UsageRollupRecord,
|
||||
};
|
||||
use serde_json::json;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
service::{AdminService, LogsQuery, UsageOverviewResponse, UsageRequestQuery, usage_window},
|
||||
service::{
|
||||
AdminService, ApprovalsQuery, LogsQuery, UsageOverviewResponse, UsageRequestQuery,
|
||||
usage_window,
|
||||
},
|
||||
};
|
||||
|
||||
impl AdminService {
|
||||
@@ -52,6 +62,76 @@ impl AdminService {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_approvals(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
query: ApprovalsQuery,
|
||||
) -> Result<Vec<ApprovalRequestRecord>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let records = self
|
||||
.registry
|
||||
.list_approval_requests(ListApprovalRequestsQuery {
|
||||
workspace_id,
|
||||
status: query.status,
|
||||
limit: query.limit.unwrap_or(50).clamp(1, 200),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut normalized = Vec::with_capacity(records.len());
|
||||
for record in records {
|
||||
let record = self.normalize_approval_record(record).await?;
|
||||
if query.status.is_none() || record.approval.status == query.status.unwrap() {
|
||||
normalized.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_approval(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
approval_id: &ApprovalRequestId,
|
||||
) -> Result<ApprovalRequestRecord, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let record = self
|
||||
.registry
|
||||
.get_approval_request(workspace_id, approval_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found_with_context(
|
||||
format!("approval request {} was not found", approval_id.as_str()),
|
||||
json!({ "approval_id": approval_id.as_str() }),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.normalize_approval_record(record).await
|
||||
}
|
||||
|
||||
async fn normalize_approval_record(
|
||||
&self,
|
||||
record: ApprovalRequestRecord,
|
||||
) -> Result<ApprovalRequestRecord, ApiError> {
|
||||
if record.approval.status != ApprovalRequestStatus::Pending
|
||||
|| record.approval.expires_at > OffsetDateTime::now_utc()
|
||||
{
|
||||
return Ok(record);
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.registry
|
||||
.expire_approval_request(ExpireApprovalRequest {
|
||||
workspace_id: &record.approval.workspace_id,
|
||||
agent_id: &record.approval.agent_id,
|
||||
approval_id: &record.approval.id,
|
||||
expired_at: OffsetDateTime::now_utc(),
|
||||
})
|
||||
.await?
|
||||
.unwrap_or(record))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_usage_overview(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user