Execute approved tool calls
This commit is contained in:
@@ -18,8 +18,8 @@ use crank_core::{
|
||||
PlatformApiKeyScope, SecretId,
|
||||
};
|
||||
use crank_registry::{
|
||||
CreateApprovalRequest, CreateInvocationLogRequest, DecideApprovalRequest, PostgresRegistry,
|
||||
PublishedAgentTool,
|
||||
ApprovalRequestRecord, CreateApprovalRequest, CreateInvocationLogRequest,
|
||||
DecideApprovalRequest, FinishApprovalRequest, PostgresRegistry, PublishedAgentTool,
|
||||
};
|
||||
use crank_runtime::{
|
||||
RequestRateLimiter, ResolvedAuth, RuntimeError, RuntimeExecutionRequest, RuntimeExecutor,
|
||||
@@ -155,6 +155,10 @@ pub fn build_app(
|
||||
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}/approve",
|
||||
post(approve_request),
|
||||
)
|
||||
.route(
|
||||
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}",
|
||||
get(get_approval_request),
|
||||
)
|
||||
.route(
|
||||
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}/deny",
|
||||
post(deny_request),
|
||||
@@ -213,6 +217,42 @@ async fn approve_request(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_approval_request(
|
||||
Path(path): Path<ApprovalRoutePath>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
let agent_path = AgentRoutePath {
|
||||
workspace_slug: path.workspace_slug,
|
||||
agent_slug: path.agent_slug,
|
||||
};
|
||||
let key = match require_approval_access(
|
||||
&state,
|
||||
&agent_path,
|
||||
&headers,
|
||||
PlatformApiKeyScope::ReadPending,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(key) => key,
|
||||
Err(status) => return status.into_response(),
|
||||
};
|
||||
let Some(agent_id) = key.api_key.agent_id.as_ref() else {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
};
|
||||
let approval_id = ApprovalRequestId::new(path.approval_id);
|
||||
|
||||
match state
|
||||
.registry
|
||||
.get_approval_request_for_agent(&key.api_key.workspace_id, agent_id, &approval_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(record)) => Json(json!(record)).into_response(),
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn deny_request(
|
||||
Path(path): Path<ApprovalRoutePath>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
@@ -279,12 +319,115 @@ async fn decide_approval_request(
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Some(record)) if status == ApprovalRequestStatus::Approved => {
|
||||
match execute_approved_request(&state, &agent_path, record).await {
|
||||
Ok(record) => Json(json!(record)).into_response(),
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
Ok(Some(record)) => Json(json!(record)).into_response(),
|
||||
Ok(None) => StatusCode::CONFLICT.into_response(),
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
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 Err(StatusCode::NOT_FOUND.into_response());
|
||||
};
|
||||
|
||||
let operation = runtime_operation(&tool);
|
||||
let request_preview = build_request_preview(
|
||||
&state.runtime,
|
||||
&operation,
|
||||
&approval.approval.request_payload,
|
||||
);
|
||||
let started_at = Instant::now();
|
||||
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()),
|
||||
)
|
||||
.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)),
|
||||
),
|
||||
};
|
||||
|
||||
let _ = 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;
|
||||
|
||||
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 mcp_get(
|
||||
Path(path): Path<AgentRoutePath>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
||||
Reference in New Issue
Block a user