Execute approved tool calls
This commit is contained in:
@@ -610,7 +610,33 @@ async fn approval_key_lists_and_decides_pending_requests() {
|
|||||||
let approved_body = approved.json::<Value>().await.unwrap();
|
let approved_body = approved.json::<Value>().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
approved_body["approval"]["status"],
|
approved_body["approval"]["status"],
|
||||||
Value::String("approved".to_owned())
|
Value::String("completed".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
approved_body["approval"]["response_payload"],
|
||||||
|
json!({ "id": "lead_123" })
|
||||||
|
);
|
||||||
|
|
||||||
|
let status_url = format!(
|
||||||
|
"{}/approvals/{}",
|
||||||
|
agent_mcp_url(&base_url, "sales-human-approval"),
|
||||||
|
approval.id
|
||||||
|
);
|
||||||
|
let current = client
|
||||||
|
.get(&status_url)
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {approval_key}"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(current.status(), reqwest::StatusCode::OK);
|
||||||
|
let current_body = current.json::<Value>().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
current_body["approval"]["status"],
|
||||||
|
Value::String("completed".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
current_body["approval"]["response_payload"],
|
||||||
|
json!({ "id": "lead_123" })
|
||||||
);
|
);
|
||||||
|
|
||||||
let pending_after = client
|
let pending_after = client
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ use crank_core::{
|
|||||||
PlatformApiKeyScope, SecretId,
|
PlatformApiKeyScope, SecretId,
|
||||||
};
|
};
|
||||||
use crank_registry::{
|
use crank_registry::{
|
||||||
CreateApprovalRequest, CreateInvocationLogRequest, DecideApprovalRequest, PostgresRegistry,
|
ApprovalRequestRecord, CreateApprovalRequest, CreateInvocationLogRequest,
|
||||||
PublishedAgentTool,
|
DecideApprovalRequest, FinishApprovalRequest, PostgresRegistry, PublishedAgentTool,
|
||||||
};
|
};
|
||||||
use crank_runtime::{
|
use crank_runtime::{
|
||||||
RequestRateLimiter, ResolvedAuth, RuntimeError, RuntimeExecutionRequest, RuntimeExecutor,
|
RequestRateLimiter, ResolvedAuth, RuntimeError, RuntimeExecutionRequest, RuntimeExecutor,
|
||||||
@@ -155,6 +155,10 @@ pub fn build_app(
|
|||||||
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}/approve",
|
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}/approve",
|
||||||
post(approve_request),
|
post(approve_request),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}",
|
||||||
|
get(get_approval_request),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}/deny",
|
"/v1/{workspace_slug}/{agent_slug}/approvals/{approval_id}/deny",
|
||||||
post(deny_request),
|
post(deny_request),
|
||||||
@@ -213,6 +217,42 @@ async fn approve_request(
|
|||||||
.await
|
.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(
|
async fn deny_request(
|
||||||
Path(path): Path<ApprovalRoutePath>,
|
Path(path): Path<ApprovalRoutePath>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -279,12 +319,115 @@ async fn decide_approval_request(
|
|||||||
})
|
})
|
||||||
.await
|
.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(Some(record)) => Json(json!(record)).into_response(),
|
||||||
Ok(None) => StatusCode::CONFLICT.into_response(),
|
Ok(None) => StatusCode::CONFLICT.into_response(),
|
||||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.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(
|
async fn mcp_get(
|
||||||
Path(path): Path<AgentRoutePath>,
|
Path(path): Path<AgentRoutePath>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ pub enum ApprovalRequestStatus {
|
|||||||
Approved,
|
Approved,
|
||||||
Denied,
|
Denied,
|
||||||
Expired,
|
Expired,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ pub mod requests {
|
|||||||
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
||||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
||||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
||||||
FinishImportJobRequest, ListInvocationLogsQuery, PublishAgentRequest, PublishRequest,
|
FinishApprovalRequest, FinishImportJobRequest, ListInvocationLogsQuery,
|
||||||
RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
PublishAgentRequest, PublishRequest, RotateSecretRequest, SaveAgentBindingsRequest,
|
||||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||||
UpdateWorkspaceRequest, UsageQuery,
|
SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest, UsageQuery,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +45,11 @@ pub use model::{
|
|||||||
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
||||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||||
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
||||||
FinishImportJobRequest, ImportJob, ImportJobId, ImportJobKind, ImportJobStatus,
|
FinishApprovalRequest, FinishImportJobRequest, ImportJob, ImportJobId, ImportJobKind,
|
||||||
InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery, MembershipRecord,
|
ImportJobStatus, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
|
||||||
OperationAgentRef, OperationSampleMetadata, OperationSummary, OperationUsageSummary,
|
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
|
||||||
OperationVersionRecord, Page, PlatformApiKeyRecord, PublishAgentRequest, PublishRequest,
|
OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord, PublishAgentRequest,
|
||||||
PublishedAgentTool, RegistryOperation, RotateSecretRequest, SampleKind,
|
PublishRequest, PublishedAgentTool, RegistryOperation, RotateSecretRequest, SampleKind,
|
||||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||||
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
|
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
|
||||||
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
||||||
|
|||||||
@@ -536,6 +536,16 @@ pub struct DecideApprovalRequest<'a> {
|
|||||||
pub decision_note: Option<&'a str>,
|
pub decision_note: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FinishApprovalRequest<'a> {
|
||||||
|
pub workspace_id: &'a WorkspaceId,
|
||||||
|
pub agent_id: &'a AgentId,
|
||||||
|
pub approval_id: &'a ApprovalRequestId,
|
||||||
|
pub status: ApprovalRequestStatus,
|
||||||
|
pub response_payload: Option<Value>,
|
||||||
|
pub decision_note: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct CreateSecretRequest<'a> {
|
pub struct CreateSecretRequest<'a> {
|
||||||
pub secret: &'a Secret,
|
pub secret: &'a Secret,
|
||||||
|
|||||||
@@ -187,6 +187,49 @@ impl PostgresRegistry {
|
|||||||
|
|
||||||
row.map(map_approval_request_row).transpose()
|
row.map(map_approval_request_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn finish_approval_request(
|
||||||
|
&self,
|
||||||
|
request: FinishApprovalRequest<'_>,
|
||||||
|
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
|
||||||
|
let row = sqlx::query(
|
||||||
|
"update approval_requests
|
||||||
|
set status = $1,
|
||||||
|
response_payload_json = $2,
|
||||||
|
decision_note = coalesce($3, decision_note)
|
||||||
|
where workspace_id = $4
|
||||||
|
and agent_id = $5
|
||||||
|
and id = $6
|
||||||
|
and status = 'approved'
|
||||||
|
returning
|
||||||
|
id,
|
||||||
|
workspace_id,
|
||||||
|
agent_id,
|
||||||
|
operation_id,
|
||||||
|
operation_version,
|
||||||
|
status,
|
||||||
|
risk_level,
|
||||||
|
confirmation_title,
|
||||||
|
confirmation_body,
|
||||||
|
request_payload_json,
|
||||||
|
response_payload_json,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
|
decided_at,
|
||||||
|
decided_by_key_id,
|
||||||
|
decision_note",
|
||||||
|
)
|
||||||
|
.bind(serialize_enum_text(&request.status, "approval_status")?)
|
||||||
|
.bind(request.response_payload.as_ref().map(Json))
|
||||||
|
.bind(request.decision_note)
|
||||||
|
.bind(request.workspace_id.as_str())
|
||||||
|
.bind(request.agent_id.as_str())
|
||||||
|
.bind(request.approval_id.as_str())
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
row.map(map_approval_request_row).transpose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_approval_request_row(row: PgRow) -> Result<ApprovalRequestRecord, RegistryError> {
|
fn map_approval_request_row(row: PgRow) -> Result<ApprovalRequestRecord, RegistryError> {
|
||||||
|
|||||||
@@ -35,16 +35,17 @@ use crate::{
|
|||||||
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
||||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
||||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
||||||
DescriptorMetadata, FinishImportJobRequest, ImportJob, ImportJobId, InvitationRecord,
|
DescriptorMetadata, FinishApprovalRequest, FinishImportJobRequest, ImportJob, ImportJobId,
|
||||||
InvocationLogRecord, ListInvocationLogsQuery, MembershipRecord, OperationAgentRef,
|
InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery, MembershipRecord,
|
||||||
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
|
OperationAgentRef, OperationSampleMetadata, OperationSummary, OperationUsageSummary,
|
||||||
PlatformApiKeyRecord, PublishAgentRequest, PublishRequest, PublishedAgentTool,
|
OperationVersionRecord, PlatformApiKeyRecord, PublishAgentRequest, PublishRequest,
|
||||||
RegistryOperation, RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
PublishedAgentTool, RegistryOperation, RotateSecretRequest, SaveAgentBindingsRequest,
|
||||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||||
SecretRecord, SecretVersionRecord, SessionRecord, UpdateWorkspaceRequest,
|
SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord, SessionRecord,
|
||||||
UsageAgentBreakdown, UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary,
|
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageOperationBreakdown, UsageQuery,
|
||||||
UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream,
|
UsageRollupRecord, UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord,
|
||||||
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
WorkspaceRecord, WorkspaceUpstream, YamlImportJob, YamlImportJobCompletion,
|
||||||
|
YamlImportJobId, YamlImportJobStatus,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ use crank_registry::{
|
|||||||
CreateAgentRequest, CreateApprovalRequest, CreateInvocationLogRequest,
|
CreateAgentRequest, CreateApprovalRequest, CreateInvocationLogRequest,
|
||||||
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||||
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
||||||
OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest,
|
FinishApprovalRequest, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
||||||
PublishRequest, RegistryError, RegistryOperation, SampleKind, SaveAuthProfileRequest,
|
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
||||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, UpdateWorkspaceRequest,
|
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||||
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId,
|
||||||
|
YamlImportJobStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn test_workspace_id() -> WorkspaceId {
|
fn test_workspace_id() -> WorkspaceId {
|
||||||
@@ -555,6 +556,25 @@ async fn manages_approval_request_lifecycle() {
|
|||||||
Some(json!({"approve": "yes"}))
|
Some(json!({"approve": "yes"}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let completed = registry
|
||||||
|
.finish_approval_request(FinishApprovalRequest {
|
||||||
|
workspace_id: &workspace_id,
|
||||||
|
agent_id: &agent.id,
|
||||||
|
approval_id: &approval.id,
|
||||||
|
status: ApprovalRequestStatus::Completed,
|
||||||
|
response_payload: Some(json!({"id": "lead_123"})),
|
||||||
|
decision_note: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(completed.approval.status, ApprovalRequestStatus::Completed);
|
||||||
|
assert_eq!(
|
||||||
|
completed.approval.response_payload,
|
||||||
|
Some(json!({"id": "lead_123"}))
|
||||||
|
);
|
||||||
|
|
||||||
let pending_after_decision = registry
|
let pending_after_decision = registry
|
||||||
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
|
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -181,6 +181,13 @@ curl https://crank.example.com/mcp/v1/default/sales/approvals \
|
|||||||
-H 'Authorization: Bearer <approval_api_key>'
|
-H 'Authorization: Bearer <approval_api_key>'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Статус конкретного запроса:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://crank.example.com/mcp/v1/default/sales/approvals/<approval_id> \
|
||||||
|
-H 'Authorization: Bearer <approval_api_key>'
|
||||||
|
```
|
||||||
|
|
||||||
Подтверждение:
|
Подтверждение:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -190,6 +197,8 @@ curl https://crank.example.com/mcp/v1/default/sales/approvals/<approval_id>/appr
|
|||||||
--data '{ "approve": "yes", "note": "Пользователь подтвердил действие" }'
|
--data '{ "approve": "yes", "note": "Пользователь подтвердил действие" }'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
После подтверждения Crank выполняет исходный REST-запрос с тем payload, который был сохранен при первом `tools/call`. Если запрос прошел успешно, заявка получает статус `completed`, а результат сохраняется в `response_payload`. Если upstream вернул ошибку, заявка получает статус `failed`, а в `response_payload` сохраняется код и текст ошибки.
|
||||||
|
|
||||||
Отклонение:
|
Отклонение:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -201,6 +210,8 @@ curl https://crank.example.com/mcp/v1/default/sales/approvals/<approval_id>/deny
|
|||||||
|
|
||||||
Ключ MCP-клиента не подходит для этих endpoints. Ключ подтверждения, наоборот, не подходит для `initialize`, `tools/list` и `tools/call`.
|
Ключ MCP-клиента не подходит для этих endpoints. Ключ подтверждения, наоборот, не подходит для `initialize`, `tools/list` и `tools/call`.
|
||||||
|
|
||||||
|
Текущая реализация возвращает `approval_required` сразу и не держит исходный `tools/call` открытым до решения пользователя. Поэтому внешний интерфейс может восстановить результат через `GET /approvals/<approval_id>`. Долгое ожидание через Streamable HTTP/SSE запланировано отдельно.
|
||||||
|
|
||||||
## Как формируется каталог инструментов
|
## Как формируется каталог инструментов
|
||||||
|
|
||||||
MCP-клиент видит только опубликованные операции, которые привязаны к опубликованному агенту.
|
MCP-клиент видит только опубликованные операции, которые привязаны к опубликованному агенту.
|
||||||
|
|||||||
Reference in New Issue
Block a user