Expose approval requests in admin logs
CI / UI Checks (push) Has been cancelled
CI / Frontend E2E (push) Has been cancelled
CI / Deployment Manifests (push) Has been cancelled
CI / Deploy (push) Has been cancelled
CI / Rust Checks (push) Has been cancelled

This commit is contained in:
github-ops
2026-06-24 13:51:54 +00:00
parent 7aad3b1228
commit 8b8f2fc6c5
14 changed files with 699 additions and 42 deletions
+6 -1
View File
@@ -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));
+10 -4
View File
@@ -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>,
+33 -1
View File
@@ -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>,
+83 -3
View File
@@ -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,