Files
crank/crates/crank-community-mcp/src/approval_execution.rs
T
bsodfather 8318e4b560
CI / Rust Checks (push) Successful in 5m7s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 3m9s
CI / Deploy (push) Successful in 1m41s
Усилить безопасность и надёжность выполнения операций
2026-07-11 14:08:07 +03:00

238 lines
7.7 KiB
Rust

use std::{sync::Arc, time::Instant};
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use crank_core::{ApprovalRequestStatus, InvocationLevel, InvocationSource, InvocationStatus};
use crank_registry::{ApprovalRequestRecord, FinishApprovalRequest};
use crank_runtime::{RuntimeExecutionRequest, RuntimeRequestContext};
use serde_json::json;
use time::OffsetDateTime;
use tracing::warn;
use crate::{
app::{
AgentRoutePath, AppState, InvocationRecord, build_request_preview, persist_invocation,
resolve_operation_auth, runtime_operation,
},
tool_error::runtime_error_code,
};
const RECOVERY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
const RECOVERY_GRACE: time::Duration = time::Duration::seconds(5);
const EXECUTION_LEASE: time::Duration = time::Duration::minutes(6);
pub(super) fn spawn_approval_recovery(state: Arc<AppState>) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(RECOVERY_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
recover_approved_requests(&state).await;
}
});
}
async fn recover_approved_requests(state: &Arc<AppState>) {
for _ in 0..32 {
let now = OffsetDateTime::now_utc();
let approval = match state
.registry
.claim_next_recoverable_approval_request(
now,
now - RECOVERY_GRACE,
now - EXECUTION_LEASE,
)
.await
{
Ok(Some(approval)) => approval,
Ok(None) => break,
Err(error) => {
warn!(error = %error, "approval recovery query failed");
break;
}
};
let Some(path) = approval_agent_path(state, &approval).await else {
continue;
};
if execute_approved_request(state, &path, approval)
.await
.is_err()
{
warn!("recovered approval execution did not finish");
}
}
}
async fn approval_agent_path(
state: &Arc<AppState>,
approval: &ApprovalRequestRecord,
) -> Option<AgentRoutePath> {
let workspace = match state
.registry
.get_workspace(&approval.approval.workspace_id)
.await
{
Ok(Some(workspace)) => workspace,
Ok(None) => return None,
Err(error) => {
warn!(error = %error, "approval workspace lookup failed");
return None;
}
};
let agent = match state
.registry
.get_agent_summary(&approval.approval.workspace_id, &approval.approval.agent_id)
.await
{
Ok(Some(agent)) => agent,
Ok(None) => return None,
Err(error) => {
warn!(error = %error, "approval agent lookup failed");
return None;
}
};
Some(AgentRoutePath {
workspace_slug: workspace.workspace.slug,
agent_slug: agent.slug,
})
}
pub(super) async fn execute_approved_request(
state: &Arc<AppState>,
path: &AgentRoutePath,
approval: ApprovalRequestRecord,
) -> Result<ApprovalRequestRecord, Response> {
let tools = state
.catalog
.list_tools(&path.workspace_slug, &path.agent_slug)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())?;
let Some(tool) = tools.into_iter().find(|tool| {
tool.operation.id == approval.approval.operation_id
&& tool.operation.version == approval.approval.operation_version
}) else {
return finish_unavailable_approval(state, &approval).await;
};
let operation = runtime_operation(&tool);
let request_preview = build_request_preview(
&state.runtime,
&operation,
&approval.approval.request_payload,
);
let started_at = Instant::now();
let runtime_request_context =
RuntimeRequestContext::from_request_id(approval.approval.id.as_str().to_owned())
.with_response_cache_scope(
tool.workspace_id.as_str().to_owned(),
tool.agent_id.as_str().to_owned(),
)
.with_metering_context(
tool.workspace_id.clone(),
Some(tool.agent_id.clone()),
InvocationSource::AgentToolCall,
)
.with_approval_granted();
let resolved_auth =
resolve_operation_auth(state, &tool.workspace_id, &operation.execution_config).await;
let result = match resolved_auth {
Ok(resolved_auth) => {
state
.runtime
.execute_request(
RuntimeExecutionRequest::new(&operation, &approval.approval.request_payload)
.with_optional_auth(resolved_auth.as_ref())
.with_context(&runtime_request_context),
)
.await
}
Err(error) => Err(error),
};
let (status, response_payload, invocation_status, invocation_level, message, error_kind) =
match result {
Ok(output) => (
ApprovalRequestStatus::Completed,
output,
InvocationStatus::Ok,
InvocationLevel::Info,
"approved tool call completed",
None,
),
Err(error) => (
ApprovalRequestStatus::Failed,
json!({
"error": {
"code": runtime_error_code(&error),
"message": error.to_string(),
}
}),
InvocationStatus::Error,
InvocationLevel::Error,
"approved tool call failed",
Some(runtime_error_code(&error)),
),
};
if let Err(error) = persist_invocation(
state,
&tool,
InvocationRecord {
request_id: Some(approval.approval.id.as_str()),
tool_name: &tool.tool_name,
status: invocation_status,
level: invocation_level,
message,
status_code: None,
error_kind,
duration: started_at.elapsed(),
request_preview,
response_preview: response_payload.clone(),
},
)
.await
{
warn!(error = %error, "approved invocation log write failed");
}
state
.registry
.finish_approval_request(FinishApprovalRequest {
workspace_id: &approval.approval.workspace_id,
agent_id: &approval.approval.agent_id,
approval_id: &approval.approval.id,
status,
response_payload: Some(response_payload),
decision_note: None,
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())?
.ok_or_else(|| StatusCode::CONFLICT.into_response())
}
async fn finish_unavailable_approval(
state: &Arc<AppState>,
approval: &ApprovalRequestRecord,
) -> Result<ApprovalRequestRecord, Response> {
state
.registry
.finish_approval_request(FinishApprovalRequest {
workspace_id: &approval.approval.workspace_id,
agent_id: &approval.approval.agent_id,
approval_id: &approval.approval.id,
status: ApprovalRequestStatus::Failed,
response_payload: Some(json!({
"error": {
"code": "approved_operation_unavailable",
"message": "the approved operation version is no longer published"
}
})),
decision_note: None,
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())?
.ok_or_else(|| StatusCode::CONFLICT.into_response())
}