Polish community UI copy and cleanup
Deploy / deploy (push) Successful in 1m36s
CI / Rust Checks (push) Successful in 4m50s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Failing after 5m5s

This commit is contained in:
github-ops
2026-06-19 21:15:02 +00:00
parent 66dd0deee5
commit d072d142ca
57 changed files with 710 additions and 6773 deletions
+2 -227
View File
@@ -196,9 +196,8 @@ mod tests {
StreamSession, StreamStatus, TransportBehavior,
};
use crank_core::{
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, MembershipRole,
OperationSecurityLevel, Protocol, ResponseCachePolicy, RestTarget, SecretKind, Target,
ToolDescription, WebsocketTarget, WorkspaceId,
ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol,
ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId,
};
use crank_core::{IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome};
use crank_mapping::{MappingRule, MappingSet};
@@ -1242,78 +1241,6 @@ mod tests {
);
}
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_graphql_protocol_for_community_operation_create() {
let registry = test_registry().await;
let storage_root = test_storage_root("community_graphql_reject_create");
let upstream_base_url = spawn_graphql_server().await;
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let response = client
.post(format!("{base_url}/operations"))
.json(&test_graphql_operation_payload(
&upstream_base_url,
"crm_graphql_not_in_community",
))
.send()
.await
.unwrap();
let status = response.status();
let body = response.json::<Value>().await.unwrap();
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["code"], "validation_error");
assert_eq!(
body["error"]["message"],
"protocol graphql is not supported in Community"
);
assert_eq!(
body["error"]["context"],
json!({
"protocol": "graphql",
"edition": "community",
})
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_unsupported_protocol_for_community_operation_create() {
let registry = test_registry().await;
let storage_root = test_storage_root("community_protocol_reject");
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let response = client
.post(format!("{base_url}/operations"))
.json(&test_websocket_operation_payload(
"wss://example.com/stream",
"telemetry_ws",
))
.send()
.await
.unwrap();
let status = response.status();
let body = response.json::<Value>().await.unwrap();
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["code"], "validation_error");
assert_eq!(
body["error"]["message"],
"protocol websocket is not supported in Community"
);
assert_eq!(
body["error"]["context"],
json!({
"protocol": "websocket",
"edition": "community",
})
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_premium_security_level_for_community_operation_create() {
@@ -3306,18 +3233,6 @@ mod tests {
format!("http://{}", address)
}
async fn spawn_graphql_server() -> String {
let app = Router::new().route("/", post(graphql_handler));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
format!("http://{}", address)
}
#[cfg(any())]
async fn spawn_soap_server() -> String {
let app = Router::new().route("/", post(soap_handler));
@@ -3501,24 +3416,6 @@ mod tests {
}))
}
async fn graphql_handler(Json(payload): Json<Value>) -> Json<Value> {
let email = payload
.get("variables")
.and_then(|variables| variables.get("email"))
.and_then(Value::as_str)
.unwrap_or_default();
Json(json!({
"data": {
"createLead": {
"id": "lead_123",
"status": "created",
"email": email
}
}
}))
}
#[cfg(any())]
async fn soap_handler(body: String) -> (axum::http::StatusCode, String) {
assert!(body.contains("<email>user@example.com</email>"));
@@ -3647,8 +3544,6 @@ mod tests {
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: ToolDescription {
title: "Create Lead".to_owned(),
@@ -3660,65 +3555,6 @@ mod tests {
}
}
fn test_graphql_operation_payload(endpoint: &str, name: &str) -> OperationPayload {
OperationPayload {
name: name.to_owned(),
display_name: "Create Lead GraphQL".to_owned(),
category: "sales".to_owned(),
protocol: Protocol::Graphql,
security_level: OperationSecurityLevel::Standard,
target: Target::Graphql(GraphqlTarget {
endpoint: endpoint.to_owned(),
operation_type: GraphqlOperationType::Mutation,
operation_name: "CreateLead".to_owned(),
query_template:
"mutation CreateLead($email: String!) { createLead(email: $email) { id status email } }"
.to_owned(),
response_path: "$.response.body.data.createLead".to_owned(),
}),
input_schema: object_schema("email"),
output_schema: object_schema("id"),
input_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.mcp.email".to_owned(),
target: "$.request.variables.email".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
output_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.response.data.id".to_owned(),
target: "$.output.id".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: ToolDescription {
title: "Create Lead GraphQL".to_owned(),
description: "Creates a CRM lead through GraphQL".to_owned(),
tags: vec!["crm".to_owned(), "graphql".to_owned()],
examples: Vec::new(),
},
wizard_state: None,
}
}
#[cfg(any())]
fn test_grpc_operation_payload(server_addr: &str, name: &str) -> OperationPayload {
OperationPayload {
@@ -3766,8 +3602,6 @@ mod tests {
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: ToolDescription {
title: "Unary Echo gRPC".to_owned(),
@@ -3930,8 +3764,6 @@ mod tests {
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: ToolDescription {
title: "Create Lead SOAP".to_owned(),
@@ -3943,63 +3775,6 @@ mod tests {
}
}
fn test_websocket_operation_payload(url: &str, name: &str) -> OperationPayload {
OperationPayload {
name: name.to_owned(),
display_name: "Telemetry WebSocket".to_owned(),
category: "telemetry".to_owned(),
protocol: Protocol::Websocket,
security_level: OperationSecurityLevel::Standard,
target: Target::Websocket(WebsocketTarget {
url: url.to_owned(),
subprotocols: Vec::new(),
subscribe_message_template: Some(json!({ "subscribe": true })),
unsubscribe_message_template: None,
static_headers: BTreeMap::new(),
}),
input_schema: object_schema("channel"),
output_schema: object_schema("event"),
input_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.mcp.channel".to_owned(),
target: "$.request.body.channel".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
output_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.response.body.event".to_owned(),
target: "$.output.event".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: ToolDescription {
title: "Telemetry WebSocket".to_owned(),
description: "Streams telemetry events".to_owned(),
tags: vec!["telemetry".to_owned(), "websocket".to_owned()],
examples: Vec::new(),
},
wizard_state: None,
}
}
#[cfg(any())]
const SOAP_TEST_WSDL: &str = r#"<?xml version="1.0"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
-13
View File
@@ -432,18 +432,12 @@ fn runtime_test_failure_code(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Schema(_) => "runtime_schema_error",
RuntimeError::Mapping(_) => "runtime_mapping_error",
RuntimeError::GraphqlAdapter(_) => "runtime_graphql_error",
RuntimeError::GrpcAdapter(_) => "runtime_grpc_error",
RuntimeError::RestAdapter(_) => "runtime_rest_error",
RuntimeError::ProtocolAdapter(_) => "runtime_adapter_error",
RuntimeError::SoapAdapter(_) => "runtime_soap_error",
RuntimeError::WebsocketAdapter(_) => "runtime_websocket_error",
RuntimeError::UnsupportedProtocol { .. } => "runtime_protocol_error",
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
RuntimeError::InvalidPreparedRequest { .. } => "runtime_request_error",
RuntimeError::MissingStreamingConfig { .. } => "runtime_streaming_config_error",
RuntimeError::UnsupportedExecutionMode { .. } => "runtime_streaming_mode_error",
RuntimeError::InvalidStreamingPayload { .. } => "runtime_streaming_payload_error",
RuntimeError::MissingAuthProfile { .. } => "runtime_auth_profile_error",
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
"runtime_secret_error"
@@ -459,10 +453,6 @@ pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
"field": field,
"reason": reason,
})),
RuntimeError::InvalidStreamingPayload { field, reason } => Some(json!({
"field": field,
"reason": reason,
})),
RuntimeError::InvalidAuthSecretValue { secret_id, reason } => Some(json!({
"secret_id": secret_id,
"reason": reason,
@@ -481,9 +471,6 @@ pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
"secret_id": secret_id,
"version": version,
})),
RuntimeError::MissingStreamingConfig { operation_id } => Some(json!({
"operation_id": operation_id,
})),
RuntimeError::UnsupportedExecutionMode { operation_id, mode } => Some(json!({
"operation_id": operation_id,
"mode": mode,
-3
View File
@@ -88,9 +88,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
info!(
runtime_max_concurrent_unary = runtime_limits.max_concurrent_unary,
runtime_max_concurrent_window = runtime_limits.max_concurrent_window,
runtime_max_concurrent_sessions = runtime_limits.max_concurrent_sessions,
runtime_max_concurrent_jobs = runtime_limits.max_concurrent_jobs,
admin_rate_limit_rps = api_rate_limit.requests_per_second,
admin_rate_limit_burst = api_rate_limit.burst,
cache_backend = %cache_config.backend,
+45 -351
View File
@@ -4,31 +4,29 @@ use std::sync::Arc;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use crank_core::{
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
AsyncJobHandle, AuditSink, AuthConfig, AuthKind, AuthProfile, AuthProfileId, CapabilityProfile,
CommunityCapabilityProfile, ConfigExport, EditionCapabilities, ExecutionMode, ExportMode,
GeneratedDraft, GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel,
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, JobStatus, LoginOutcome,
MachineTokenIssuer, MembershipRole, NoMachineTokenIssuer, NoopAuditSink, OperationId,
OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine, PlatformApiKey,
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine, ProductEdition,
Protocol, ResponseCachePolicy, SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus,
StreamSession, StreamStatus, Target, TransportBehavior, UsagePeriod, UserId, UserSessionId,
WizardState, Workspace, WorkspaceId, WorkspaceStatus,
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuditSink, AuthConfig,
AuthKind, AuthProfile, AuthProfileId, CapabilityProfile, CommunityCapabilityProfile,
ConfigExport, EditionCapabilities, ExecutionMode, ExportMode, GeneratedDraft,
GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel, InvocationLog,
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MachineTokenIssuer,
MembershipRole, NoMachineTokenIssuer, NoopAuditSink, OperationId, OperationSecurityLevel,
OperationStatus, OwnerOnlyPolicyEngine, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol, ResponseCachePolicy, SampleId,
Samples, Secret, SecretId, SecretKind, SecretStatus, Target, UsagePeriod, UserId,
UserSessionId, WizardState, Workspace, WorkspaceId, WorkspaceStatus,
};
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
use crank_registry::{
AgentSummary, AgentVersionRecord, CreateAgentDraftVersionRequest, CreateAgentRequest,
CreateAsyncJobRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
CreateSecretRequest, CreateStreamSessionRequest, CreateVersionRequest, CreateWorkspaceRequest,
InvocationLogRecord, ListInvocationLogsQuery, OperationAgentRef, OperationSampleMetadata,
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation,
RotateSecretRequest, SampleKind, SaveAgentBindingsRequest, SaveAuthProfileRequest,
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, UpdateAsyncJobStatusRequest,
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
WorkspaceUpstream, WorkspaceUpstreamId,
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
CreateVersionRequest, CreateWorkspaceRequest, InvocationLogRecord, ListInvocationLogsQuery,
OperationAgentRef, OperationSampleMetadata, OperationSummary, OperationUsageSummary,
OperationVersionRecord, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest,
PublishRequest, RegistryError, RegistryOperation, RotateSecretRequest, SampleKind,
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveSampleMetadataRequest,
SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
UsageOperationBreakdown, UsageQuery, UsageSummary, UsageTimelinePoint,
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
};
use crank_runtime::{
PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, RuntimeOperation,
@@ -154,120 +152,6 @@ pub struct TestRunResult {
pub request_preview: Value,
pub response_preview: Value,
pub errors: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window: Option<WindowTestRunView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_session: Option<StreamSessionStartView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub async_job: Option<AsyncJobStartView>,
}
#[derive(Clone, Debug, Serialize)]
pub struct WindowTestRunView {
pub window_complete: bool,
pub truncated: bool,
pub has_more: bool,
pub cursor: Option<Value>,
}
#[derive(Clone, Debug, Serialize)]
pub struct StreamSessionStartView {
pub session_id: String,
pub status: StreamStatus,
#[serde(with = "time::serde::rfc3339")]
pub expires_at: OffsetDateTime,
pub poll_after_ms: u64,
pub preview: Value,
}
#[derive(Clone, Debug, Serialize)]
pub struct AsyncJobStartView {
pub job_id: String,
pub status: JobStatus,
pub progress: Value,
}
#[derive(Debug, Serialize, Deserialize)]
struct StoredSessionState {
input: Value,
summary: Value,
items: Vec<Value>,
next_index: usize,
batch_size: usize,
}
enum TestRunOutcome {
Unary {
output: Value,
},
Window {
output: crank_runtime::WindowExecutionResult,
},
Session {
output: StreamSessionStartView,
},
AsyncJob {
output: AsyncJobStartView,
},
}
impl TestRunOutcome {
fn into_result_views(
self,
) -> (
&'static str,
Value,
Option<WindowTestRunView>,
Option<StreamSessionStartView>,
Option<AsyncJobStartView>,
) {
match self {
Self::Unary { output } => ("admin test run completed", output, None, None, None),
Self::Window { output } => (
"admin window test run completed",
json!({
"summary": output.summary,
"items": output.items,
"cursor": output.cursor,
"window_complete": output.window_complete,
"truncated": output.truncated,
"has_more": output.has_more,
}),
Some(WindowTestRunView {
window_complete: output.window_complete,
truncated: output.truncated,
has_more: output.has_more,
cursor: output.cursor,
}),
None,
None,
),
Self::Session { output } => (
"admin session test run started",
json!({
"session_id": output.session_id,
"status": output.status,
"expires_at": format_timestamp(output.expires_at),
"poll_after_ms": output.poll_after_ms,
"preview": output.preview,
}),
None,
Some(output),
None,
),
Self::AsyncJob { output } => (
"admin async job test run started",
json!({
"job_id": output.job_id,
"status": output.status,
"progress": output.progress,
}),
None,
None,
Some(output),
),
}
}
}
#[derive(Clone, Debug, Deserialize)]
@@ -437,12 +321,12 @@ pub struct UsageOverviewResponse {
pub struct ProtocolCapabilityView {
pub protocol: Protocol,
pub supports_execution_modes: Vec<ExecutionMode>,
pub supports_transport_behaviors: Vec<TransportBehavior>,
pub supports_transport_behaviors: Vec<String>,
pub supports_auth_kinds: Vec<String>,
pub supports_upload_artifacts: Vec<String>,
pub supports_cursor_path: bool,
pub supports_done_path: bool,
pub supports_aggregation_mode: Vec<AggregationMode>,
pub supports_aggregation_mode: Vec<String>,
}
#[derive(Clone, Debug, Deserialize)]
@@ -1801,12 +1685,7 @@ impl AdminService {
.get_operation_version(workspace_id, operation_id, payload.version)
.await?;
let runtime = RuntimeOperation::from(record.snapshot.clone());
let mode = runtime
.execution_config
.streaming
.as_ref()
.map(|streaming| streaming.mode)
.unwrap_or(ExecutionMode::Unary);
let mode = ExecutionMode::Unary;
let request_preview =
match build_request_preview(&record.snapshot.input_mapping, &payload.input) {
Ok(preview) => preview,
@@ -1835,9 +1714,6 @@ impl AdminService {
errors: vec![crate::error::runtime_test_failure(&RuntimeError::Mapping(
error,
))],
window: None,
stream_session: None,
async_job: None,
});
}
};
@@ -1847,9 +1723,8 @@ impl AdminService {
.await;
let started_at = std::time::Instant::now();
match match resolved_auth {
Ok(resolved_auth) => match mode {
ExecutionMode::Unary => self
.runtime
Ok(resolved_auth) => {
self.runtime
.execute_with_auth_and_context(
&runtime,
&payload.input,
@@ -1857,46 +1732,12 @@ impl AdminService {
Some(&runtime_request_context),
)
.await
.map(|output| TestRunOutcome::Unary { output }),
ExecutionMode::Window => self
.runtime
.execute_window_with_auth_and_context(
&runtime,
&payload.input,
resolved_auth.as_ref(),
Some(&runtime_request_context),
)
.await
.map(|output| TestRunOutcome::Window { output }),
ExecutionMode::Session => self
.start_stream_session_test(
workspace_id,
&record.snapshot,
&runtime,
&payload.input,
resolved_auth.as_ref(),
&runtime_request_context,
)
.await
.map(|output| TestRunOutcome::Session { output }),
ExecutionMode::AsyncJob => self
.start_async_job_test(
workspace_id,
&record.snapshot,
&runtime,
&payload.input,
&runtime_request_context,
)
.await
.map(|output| TestRunOutcome::AsyncJob { output }),
},
}
Err(error) => Err(error),
} {
Ok(outcome) => {
Ok(response_preview) => {
let duration_ms =
u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
let (message, response_preview, window, stream_session, async_job) =
outcome.into_result_views();
self.record_invocation(InvocationRecordRequest {
workspace_id,
agent_id: None,
@@ -1905,7 +1746,7 @@ impl AdminService {
source: InvocationSource::AdminTestRun,
level: InvocationLevel::Info,
status: InvocationStatus::Ok,
message: message.to_owned(),
message: "admin test run completed".to_owned(),
status_code: None,
error_kind: None,
duration_ms,
@@ -1919,9 +1760,6 @@ impl AdminService {
request_preview,
response_preview,
errors: Vec::new(),
window,
stream_session,
async_job,
})
}
Err(error) => {
@@ -1949,9 +1787,6 @@ impl AdminService {
request_preview,
response_preview: Value::Null,
errors: vec![crate::error::runtime_test_failure(&error)],
window: None,
stream_session: None,
async_job: None,
})
}
}
@@ -1983,6 +1818,7 @@ impl AdminService {
.map(Some)
}
#[cfg(any())]
async fn start_stream_session_test(
&self,
workspace_id: &WorkspaceId,
@@ -2060,6 +1896,7 @@ impl AdminService {
})
}
#[cfg(any())]
async fn start_async_job_test(
&self,
workspace_id: &WorkspaceId,
@@ -3230,7 +3067,6 @@ impl AdminService {
fn validate_operation_payload(&self, payload: &OperationPayload) -> Result<(), ApiError> {
self.validate_operation_capabilities(payload.protocol, payload.security_level)?;
validate_protocol_target(payload.protocol, &payload.target)?;
validate_streaming_policy(&payload.execution_config)?;
validate_response_cache_policy(&payload.target, &payload.execution_config)?;
payload.input_mapping.validate_paths()?;
payload.output_mapping.validate_paths()?;
@@ -3240,7 +3076,6 @@ impl AdminService {
fn validate_registry_operation(&self, operation: &RegistryOperation) -> Result<(), ApiError> {
self.validate_operation_capabilities(operation.protocol, operation.security_level)?;
validate_protocol_target(operation.protocol, &operation.target)?;
validate_streaming_policy(&operation.execution_config)?;
validate_response_cache_policy(&operation.target, &operation.execution_config)?;
operation.input_mapping.validate_paths()?;
operation.output_mapping.validate_paths()?;
@@ -3640,21 +3475,12 @@ fn build_request_preview(
"path": prepared.path_params,
"query": prepared.query_params,
"headers": prepared.headers,
"grpc": prepared.grpc.unwrap_or(Value::Null),
"variables": prepared.variables.unwrap_or(Value::Null),
"body": prepared.body.unwrap_or(Value::Null)
}))
}
fn validate_protocol_target(protocol: Protocol, target: &Target) -> Result<(), ApiError> {
let is_match = matches!(
(protocol, target),
(Protocol::Rest, Target::Rest(_))
| (Protocol::Graphql, Target::Graphql(_))
| (Protocol::Grpc, Target::Grpc(_))
| (Protocol::Websocket, Target::Websocket(_))
| (Protocol::Soap, Target::Soap(_))
);
let is_match = matches!((protocol, target), (Protocol::Rest, Target::Rest(_)));
if is_match {
return Ok(());
@@ -3825,8 +3651,6 @@ fn demo_rest_operation_payload() -> OperationPayload {
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: crank_core::ToolDescription {
title: "Create CRM Lead".to_owned(),
@@ -3893,8 +3717,6 @@ fn demo_archived_operation_payload() -> OperationPayload {
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: crank_core::ToolDescription {
title: "Archive Marketing Contact".to_owned(),
@@ -3977,85 +3799,16 @@ fn map_identity_error(error: IdentityError) -> ApiError {
}
}
async fn resolve_runtime_auth_for_task(
registry: &PostgresRegistry,
secret_crypto: &SecretCrypto,
workspace_id: &WorkspaceId,
execution_config: &crank_core::ExecutionConfig,
) -> Result<Option<ResolvedAuth>, RuntimeError> {
let Some(auth_profile_id) = execution_config.auth_profile_ref.as_ref() else {
return Ok(None);
};
let auth_profile = registry
.get_auth_profile(workspace_id, auth_profile_id)
.await
.map_err(|error| RuntimeError::SecretCrypto {
operation: "load auth profile",
details: error.to_string(),
})?
.ok_or_else(|| RuntimeError::MissingAuthProfile {
auth_profile_id: auth_profile_id.as_str().to_owned(),
})?;
let mut secrets = BTreeMap::new();
let used_at = OffsetDateTime::now_utc();
for secret_id in auth_profile.config.secret_ids() {
let secret = registry
.get_secret(workspace_id, secret_id)
.await
.map_err(|error| RuntimeError::SecretCrypto {
operation: "load secret",
details: error.to_string(),
})?
.ok_or_else(|| RuntimeError::MissingSecret {
secret_id: secret_id.as_str().to_owned(),
})?;
let version = registry
.get_current_secret_version(workspace_id, secret_id)
.await
.map_err(|error| RuntimeError::SecretCrypto {
operation: "load current secret version",
details: error.to_string(),
})?
.ok_or_else(|| RuntimeError::MissingSecretVersion {
secret_id: secret_id.as_str().to_owned(),
version: secret.secret.current_version,
})?;
let plaintext = secret_crypto.decrypt(
&version.secret_version.key_version,
&version.secret_version.ciphertext,
)?;
registry
.touch_secret(workspace_id, secret_id, &used_at)
.await
.map_err(|error| RuntimeError::SecretCrypto {
operation: "touch secret",
details: error.to_string(),
})?;
secrets.insert(secret_id.clone(), plaintext);
}
ResolvedAuth::from_profile(&auth_profile, &secrets).map(Some)
}
fn runtime_error_code(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Schema(_) => "schema_error",
RuntimeError::Mapping(_) => "mapping_error",
RuntimeError::InvalidPreparedRequest { .. } => "invalid_request",
RuntimeError::GraphqlAdapter(_) => "graphql_error",
RuntimeError::GrpcAdapter(_) => "grpc_error",
RuntimeError::RestAdapter(_) => "rest_error",
RuntimeError::ProtocolAdapter(_) => "adapter_error",
RuntimeError::SoapAdapter(_) => "soap_error",
RuntimeError::WebsocketAdapter(_) => "websocket_error",
RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol",
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
RuntimeError::MissingStreamingConfig { .. } => "streaming_config_error",
RuntimeError::UnsupportedExecutionMode { .. } => "streaming_mode_error",
RuntimeError::InvalidStreamingPayload { .. } => "streaming_payload_error",
RuntimeError::MissingAuthProfile { .. } => "auth_profile_not_found",
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
"secret_not_found"
@@ -4065,40 +3818,16 @@ fn runtime_error_code(error: &RuntimeError) -> &'static str {
}
}
fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> ProtocolCapabilityView {
let community_build = matches!(edition, ProductEdition::Community);
let supports_execution_modes = if community_build {
vec![ExecutionMode::Unary]
} else {
[
ExecutionMode::Unary,
ExecutionMode::Window,
ExecutionMode::Session,
ExecutionMode::AsyncJob,
]
.into_iter()
.filter(|mode| protocol.supports_execution_mode(*mode))
.collect()
};
let supports_transport_behaviors = if community_build {
vec![TransportBehavior::RequestResponse]
} else {
[
TransportBehavior::RequestResponse,
TransportBehavior::ServerStream,
TransportBehavior::StatefulSession,
TransportBehavior::DeferredResult,
]
.into_iter()
.filter(|behavior| protocol.supports_transport_behavior(*behavior))
.collect()
};
fn protocol_capability_view(
protocol: Protocol,
_edition: ProductEdition,
) -> ProtocolCapabilityView {
let supports_upload_artifacts = Vec::new();
ProtocolCapabilityView {
protocol,
supports_execution_modes,
supports_transport_behaviors,
supports_execution_modes: vec![ExecutionMode::Unary],
supports_transport_behaviors: vec!["request_response".to_owned()],
supports_auth_kinds: vec![
"none".to_owned(),
"bearer".to_owned(),
@@ -4107,15 +3836,9 @@ fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> Prot
"api_key_query".to_owned(),
],
supports_upload_artifacts,
supports_cursor_path: !matches!(protocol, Protocol::Graphql),
supports_done_path: !matches!(protocol, Protocol::Graphql),
supports_aggregation_mode: vec![
AggregationMode::RawItems,
AggregationMode::SummaryOnly,
AggregationMode::SummaryPlusSamples,
AggregationMode::Stats,
AggregationMode::LatestState,
],
supports_cursor_path: false,
supports_done_path: false,
supports_aggregation_mode: Vec::new(),
}
}
@@ -4246,22 +3969,6 @@ fn agent_mcp_endpoint(workspace_slug: &str, agent_slug: &str) -> String {
format!("/mcp/v1/{workspace_slug}/{agent_slug}")
}
fn validate_streaming_policy(
execution_config: &crank_core::ExecutionConfig,
) -> Result<(), ApiError> {
if execution_config.streaming.is_some() {
return Err(ApiError::validation_with_context(
"streaming execution is not supported in Community".to_owned(),
json!({
"field": "execution_config.streaming",
"edition": "community",
}),
));
}
Ok(())
}
fn validate_response_cache_policy(
target: &Target,
execution_config: &crank_core::ExecutionConfig,
@@ -4279,15 +3986,6 @@ fn validate_response_cache_policy(
));
}
if execution_config.streaming.is_some() {
return Err(ApiError::validation_with_context(
"response cache is supported only for unary operations".to_owned(),
json!({
"field": "execution_config.response_cache",
}),
));
}
match target {
Target::Rest(rest_target) if rest_target.method == crank_core::HttpMethod::Get => {}
_ => {
@@ -4330,8 +4028,6 @@ mod tests {
response_cache: Some(ResponseCachePolicy { ttl_ms: 5_000 }),
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
}
}
@@ -4350,14 +4046,12 @@ mod tests {
}
#[test]
fn rejects_response_cache_for_non_rest_target() {
let target = Target::Graphql(crank_core::GraphqlTarget {
endpoint: "http://example.invalid/graphql".to_owned(),
operation_type: crank_core::GraphqlOperationType::Query,
operation_name: "LookupLead".to_owned(),
query_template:
"query LookupLead($email: String!) { lookupLead(email: $email) { id } }".to_owned(),
response_path: "$.response.body.data.lookupLead".to_owned(),
fn rejects_response_cache_for_non_get_rest_operation() {
let target = Target::Rest(RestTarget {
base_url: "http://example.invalid".to_owned(),
method: HttpMethod::Post,
path_template: "/catalog".to_owned(),
static_headers: BTreeMap::new(),
});
let error =