Add approval request HTTP surface
This commit is contained in:
@@ -17,14 +17,17 @@ use axum::{
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use crank_core::{
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ExecutionConfig, HttpMethod,
|
||||
Operation, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApprovalRequest,
|
||||
ApprovalRequestId, ApprovalRequestStatus, ExecutionConfig, HttpMethod, Operation,
|
||||
OperationApprovalRiskLevel, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId,
|
||||
PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, Target, ToolDescription,
|
||||
WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::{
|
||||
CreateAgentRequest, CreatePlatformApiKeyRequest, ListInvocationLogsQuery, PostgresRegistry,
|
||||
PublishAgentRequest, PublishRequest, SaveAgentBindingsRequest,
|
||||
CreateAgentRequest, CreateApprovalRequest, CreatePlatformApiKeyRequest,
|
||||
ListInvocationLogsQuery, PostgresRegistry, PublishAgentRequest, PublishRequest,
|
||||
SaveAgentBindingsRequest,
|
||||
};
|
||||
use crank_runtime::{
|
||||
InMemoryCoordinationStateStore, RequestRateLimitConfig, RequestRateLimiter, RuntimeExecutor,
|
||||
@@ -508,6 +511,120 @@ async fn rejects_initialize_with_approval_platform_api_key() {
|
||||
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approval_key_lists_and_decides_pending_requests() {
|
||||
let registry = test_registry().await;
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let operation = test_operation(&upstream_base_url, "crm_human_approval");
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, Some("alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
publish_agent_with_bindings(
|
||||
®istry,
|
||||
"sales-human-approval",
|
||||
vec![binding_for_operation(&operation)],
|
||||
)
|
||||
.await;
|
||||
let approval = ApprovalRequest {
|
||||
id: ApprovalRequestId::new("approval_mcp_01"),
|
||||
workspace_id: test_workspace_id(),
|
||||
agent_id: test_agent_id("sales-human-approval"),
|
||||
operation_id: operation.id.clone(),
|
||||
operation_version: 1,
|
||||
status: ApprovalRequestStatus::Pending,
|
||||
risk_level: OperationApprovalRiskLevel::Dangerous,
|
||||
confirmation_title: "Подтвердите создание лида".to_owned(),
|
||||
confirmation_body: "Проверьте email перед отправкой в CRM.".to_owned(),
|
||||
request_payload: json!({"email": "ada@example.com"}),
|
||||
response_payload: None,
|
||||
created_at: OffsetDateTime::now_utc(),
|
||||
expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(5),
|
||||
decided_at: None,
|
||||
decided_by_key_id: None,
|
||||
decision_note: None,
|
||||
};
|
||||
registry
|
||||
.create_approval_request(CreateApprovalRequest {
|
||||
approval: &approval,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let approval_key =
|
||||
create_approval_platform_api_key(®istry, "sales-human-approval", "approval-http").await;
|
||||
let mcp_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-human-approval",
|
||||
"mcp-human-approval",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
.await;
|
||||
let base_url = spawn_mcp_server(build_test_app(
|
||||
registry.clone(),
|
||||
Duration::from_millis(0),
|
||||
Some("https://crank.example.com".to_owned()),
|
||||
))
|
||||
.await;
|
||||
let client = reqwest::Client::new();
|
||||
let approvals_url = format!(
|
||||
"{}/approvals",
|
||||
agent_mcp_url(&base_url, "sales-human-approval")
|
||||
);
|
||||
|
||||
let rejected = client
|
||||
.get(&approvals_url)
|
||||
.header(header::AUTHORIZATION, format!("Bearer {mcp_key}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rejected.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
|
||||
let pending = client
|
||||
.get(&approvals_url)
|
||||
.header(header::AUTHORIZATION, format!("Bearer {approval_key}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(pending.status(), reqwest::StatusCode::OK);
|
||||
let pending_body = pending.json::<Value>().await.unwrap();
|
||||
assert_eq!(pending_body["items"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(
|
||||
pending_body["items"][0]["approval"]["confirmation_title"],
|
||||
"Подтвердите создание лида"
|
||||
);
|
||||
|
||||
let approve_url = format!(
|
||||
"{}/approvals/{}/approve",
|
||||
agent_mcp_url(&base_url, "sales-human-approval"),
|
||||
approval.id
|
||||
);
|
||||
let approved = client
|
||||
.post(&approve_url)
|
||||
.header(header::AUTHORIZATION, format!("Bearer {approval_key}"))
|
||||
.json(&json!({ "approve": "yes", "note": "confirmed by test" }))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(approved.status(), reqwest::StatusCode::OK);
|
||||
let approved_body = approved.json::<Value>().await.unwrap();
|
||||
assert_eq!(
|
||||
approved_body["approval"]["status"],
|
||||
Value::String("approved".to_owned())
|
||||
);
|
||||
|
||||
let pending_after = client
|
||||
.get(&approvals_url)
|
||||
.header(header::AUTHORIZATION, format!("Bearer {approval_key}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(pending_after["items"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_tool_call_with_read_only_platform_api_key() {
|
||||
let registry = test_registry().await;
|
||||
|
||||
@@ -258,7 +258,11 @@ pub(super) async fn create_approval_platform_api_key(
|
||||
agent_slug,
|
||||
name,
|
||||
PlatformApiKeyKind::Approval,
|
||||
&[PlatformApiKeyScope::Approve, PlatformApiKeyScope::Deny],
|
||||
&[
|
||||
PlatformApiKeyScope::ReadPending,
|
||||
PlatformApiKeyScope::Approve,
|
||||
PlatformApiKeyScope::Deny,
|
||||
],
|
||||
"crk_appr",
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -8,7 +8,7 @@ var search = '';
|
||||
|
||||
var SCOPES_BY_KIND = {
|
||||
mcp_client: ['read', 'write', 'deploy'],
|
||||
approval: ['approve', 'deny'],
|
||||
approval: ['read_pending', 'approve', 'deny'],
|
||||
};
|
||||
var SCOPE_DESC = {
|
||||
read: 'apikeys.scope.read',
|
||||
@@ -16,6 +16,7 @@ var SCOPE_DESC = {
|
||||
deploy: 'apikeys.scope.deploy',
|
||||
approve: 'apikeys.scope.approve',
|
||||
deny: 'apikeys.scope.deny',
|
||||
read_pending: 'apikeys.scope.read_pending',
|
||||
};
|
||||
|
||||
function tKey(key) {
|
||||
|
||||
+4
-2
@@ -126,6 +126,7 @@ var TRANSLATIONS = {
|
||||
'apikeys.scope.deploy': 'Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.',
|
||||
'apikeys.scope.approve': 'Confirm a pending human approval request for this agent.',
|
||||
'apikeys.scope.deny': 'Reject a pending human approval request for this agent.',
|
||||
'apikeys.scope.read_pending': 'Read pending human approval requests for this agent.',
|
||||
'apikeys.modal.title': 'Create agent key',
|
||||
'apikeys.modal.title_mcp': 'Create MCP client key',
|
||||
'apikeys.modal.title_approval': 'Create approval key',
|
||||
@@ -967,8 +968,9 @@ var TRANSLATIONS = {
|
||||
'apikeys.scope.read': 'Инициализация MCP-сессий, ping сервера и получение списка инструментов для выбранного AI-агента.',
|
||||
'apikeys.scope.write': 'Выполнение `tools/call` для опубликованных наборов инструментов агента.',
|
||||
'apikeys.scope.deploy': 'Зарезервировано для deploy-автоматизации. Сейчас также разрешает MCP read/write сценарии.',
|
||||
'apikeys.scope.approve': 'Подтверждение ожидающего human approval запроса для этого агента.',
|
||||
'apikeys.scope.deny': 'Отклонение ожидающего human approval запроса для этого агента.',
|
||||
'apikeys.scope.approve': 'Подтверждение ожидающего запроса для этого агента.',
|
||||
'apikeys.scope.deny': 'Отклонение ожидающего запроса для этого агента.',
|
||||
'apikeys.scope.read_pending': 'Получение списка запросов, ожидающих подтверждения для этого агента.',
|
||||
'apikeys.modal.title': 'Создать ключ агента',
|
||||
'apikeys.modal.title_mcp': 'Создать ключ MCP-клиента',
|
||||
'apikeys.modal.title_approval': 'Создать ключ подтверждения',
|
||||
|
||||
Reference in New Issue
Block a user