Add approval request HTTP surface

This commit is contained in:
github-ops
2026-06-24 11:39:39 +00:00
parent d83ab541d9
commit 8a1cc1746f
16 changed files with 889 additions and 69 deletions
@@ -5,13 +5,13 @@ use super::common::*;
use std::collections::BTreeMap;
use crank_core::{
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig,
AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft,
GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId,
OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind,
PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples,
SecretId, Target, ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState,
Workspace, WorkspaceId,
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig,
ApprovalRequest, ApprovalRequestId, ApprovalRequestStatus, AuthConfig, AuthKind, AuthProfile,
ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod,
InvocationLog, MembershipRole, OperationApprovalRiskLevel, OperationId, OperationSecurityLevel,
OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind, PlatformApiKeyScope,
PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target,
ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_schema::{Schema, SchemaKind};
@@ -20,13 +20,13 @@ use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crank_registry::{
CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId,
YamlImportJobStatus,
CreateAgentRequest, CreateApprovalRequest, CreateInvocationLogRequest,
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest,
PublishRequest, RegistryError, RegistryOperation, SampleKind, SaveAuthProfileRequest,
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, UpdateWorkspaceRequest,
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
};
fn test_workspace_id() -> WorkspaceId {
@@ -455,3 +455,126 @@ async fn manages_platform_api_key_read_paths() {
database.cleanup().await;
}
#[tokio::test]
async fn manages_approval_request_lifecycle() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace_id = test_workspace_id();
let operation = test_operation("op_approval_01", 1, OperationStatus::Draft);
let agent = test_agent("agent_approval_01", AgentStatus::Draft);
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
let approval_key = PlatformApiKey {
id: PlatformApiKeyId::new("key_approval_01"),
workspace_id: workspace_id.clone(),
agent_id: Some(agent.id.clone()),
key_kind: PlatformApiKeyKind::Approval,
name: "Approval key".to_owned(),
prefix: "crk_appr".to_owned(),
scopes: vec![PlatformApiKeyScope::Approve, PlatformApiKeyScope::Deny],
status: PlatformApiKeyStatus::Active,
created_at: timestamp("2026-03-25T12:00:00Z"),
last_used_at: None,
expires_at: None,
allowed_origins: Vec::new(),
};
let approval = ApprovalRequest {
id: ApprovalRequestId::new("approval_01"),
workspace_id: workspace_id.clone(),
agent_id: agent.id.clone(),
operation_id: operation.id.clone(),
operation_version: 1,
status: ApprovalRequestStatus::Pending,
risk_level: OperationApprovalRiskLevel::Dangerous,
confirmation_title: "Confirm action".to_owned(),
confirmation_body: "Check payload before running.".to_owned(),
request_payload: json!({"amount": 100}),
response_payload: None,
created_at: timestamp("2026-03-25T12:01:00Z"),
expires_at: timestamp("2027-03-25T12:06:00Z"),
decided_at: None,
decided_by_key_id: None,
decision_note: None,
};
registry
.create_operation(&workspace_id, &operation, None)
.await
.unwrap();
registry
.create_agent(CreateAgentRequest {
agent: &agent,
version: &version,
bindings: &[],
})
.await
.unwrap();
registry
.create_platform_api_key(CreatePlatformApiKeyRequest {
api_key: &approval_key,
secret_hash: "approval-secret-hash",
})
.await
.unwrap();
registry
.create_approval_request(CreateApprovalRequest {
approval: &approval,
})
.await
.unwrap();
let pending = registry
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
.await
.unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].approval, approval);
let decided = registry
.decide_approval_request(DecideApprovalRequest {
workspace_id: &workspace_id,
agent_id: &agent.id,
approval_id: &approval.id,
status: ApprovalRequestStatus::Approved,
decided_at: timestamp("2026-03-25T12:02:00Z"),
decided_by_key_id: &approval_key.id,
response_payload: Some(json!({"approve": "yes"})),
decision_note: Some("confirmed in test"),
})
.await
.unwrap()
.unwrap();
assert_eq!(decided.approval.status, ApprovalRequestStatus::Approved);
assert_eq!(
decided.approval.decided_by_key_id,
Some(approval_key.id.clone())
);
assert_eq!(
decided.approval.response_payload,
Some(json!({"approve": "yes"}))
);
let pending_after_decision = registry
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
.await
.unwrap();
assert!(pending_after_decision.is_empty());
let repeated_decision = registry
.decide_approval_request(DecideApprovalRequest {
workspace_id: &workspace_id,
agent_id: &agent.id,
approval_id: &approval.id,
status: ApprovalRequestStatus::Denied,
decided_at: timestamp("2026-03-25T12:03:00Z"),
decided_by_key_id: &approval_key.id,
response_payload: Some(json!({"approve": "no"})),
decision_note: None,
})
.await
.unwrap();
assert!(repeated_decision.is_none());
database.cleanup().await;
}