product: make community rest only

This commit is contained in:
a.tolmachev
2026-05-03 20:52:15 +00:00
parent 89761ec1e5
commit 201fe16aae
12 changed files with 77 additions and 427 deletions
+39 -6
View File
@@ -1031,10 +1031,7 @@ mod tests {
.await;
assert_eq!(response["edition"], "community");
assert_eq!(
response["supported_protocols"],
json!(["rest", "graphql", "grpc"])
);
assert_eq!(response["supported_protocols"], json!(["rest"]));
assert_eq!(response["supported_security_levels"], json!(["standard"]));
assert_eq!(
response["machine_access_modes"],
@@ -1143,7 +1140,7 @@ mod tests {
)
.await;
assert_eq!(response["items"].as_array().unwrap().len(), 3);
assert_eq!(response["items"].as_array().unwrap().len(), 1);
assert_eq!(
response["items"]
.as_array()
@@ -1151,7 +1148,7 @@ mod tests {
.iter()
.map(|item| item["protocol"].as_str().unwrap())
.collect::<Vec<_>>(),
vec!["rest", "graphql", "grpc"]
vec!["rest"]
);
for item in response["items"].as_array().unwrap() {
assert_eq!(item["supports_execution_modes"], json!(["unary"]));
@@ -1162,6 +1159,42 @@ 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() {
+11 -403
View File
@@ -3,9 +3,8 @@ use std::path::PathBuf;
use base64::{
Engine as _,
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
engine::general_purpose::URL_SAFE_NO_PAD,
};
use crank_adapter_grpc::test_support as grpc_test_support;
use crank_adapter_soap::{SoapServiceSummary, inspect_wsdl};
use crank_core::{
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
@@ -1574,7 +1573,7 @@ impl AdminService {
pub async fn get_capabilities(&self) -> EditionCapabilities {
EditionCapabilities {
edition: ProductEdition::Community,
supported_protocols: vec![Protocol::Rest, Protocol::Graphql, Protocol::Grpc],
supported_protocols: vec![Protocol::Rest],
supported_security_levels: vec![OperationSecurityLevel::Standard],
machine_access_modes: vec![MachineAccessMode::StaticAgentKey],
limits: EditionLimits {
@@ -3909,7 +3908,7 @@ impl AdminService {
protocol: Protocol,
security_level: OperationSecurityLevel,
) -> Result<(), ApiError> {
let supported_protocols = [Protocol::Rest, Protocol::Graphql, Protocol::Grpc];
let supported_protocols = [Protocol::Rest];
if !supported_protocols.contains(&protocol) {
return Err(ApiError::validation_with_context(
format!(
@@ -4033,32 +4032,6 @@ impl AdminService {
)
.await?;
let graphql_operation = self
.ensure_demo_operation(workspace_id, demo_graphql_operation_payload())
.await?;
self.ensure_operation_published(workspace_id, &graphql_operation)
.await?;
self.ensure_demo_json_samples(
workspace_id,
&graphql_operation.id,
&demo_graphql_input_sample(),
&demo_graphql_output_sample(),
)
.await?;
let grpc_operation = self
.ensure_demo_operation(workspace_id, demo_grpc_operation_payload())
.await?;
self.ensure_demo_json_samples(
workspace_id,
&grpc_operation.id,
&demo_grpc_input_sample(),
&demo_grpc_output_sample(),
)
.await?;
self.ensure_demo_grpc_artifacts(workspace_id, &grpc_operation.id)
.await?;
let archived_operation = self
.ensure_demo_operation(workspace_id, demo_archived_operation_payload())
.await?;
@@ -4071,49 +4044,17 @@ impl AdminService {
self.ensure_demo_agent_bindings(
workspace_id,
&AgentId::new(revops_agent.id.clone()),
vec![
AgentBindingPayload {
operation_id: rest_operation.id.as_str().to_owned(),
operation_version: rest_operation.current_draft_version,
tool_name: "create_crm_lead".to_owned(),
tool_title: "Create CRM Lead".to_owned(),
tool_description_override: Some(
"Create a new CRM lead in the revenue workspace.".to_owned(),
),
enabled: true,
},
AgentBindingPayload {
operation_id: graphql_operation.id.as_str().to_owned(),
operation_version: graphql_operation.current_draft_version,
tool_name: "get_invoice_status".to_owned(),
tool_title: "Get Invoice Status".to_owned(),
tool_description_override: Some(
"Fetch invoice state from billing GraphQL.".to_owned(),
),
enabled: true,
},
],
true,
)
.await?;
let support_agent = self
.ensure_demo_agent(workspace_id, demo_support_agent_payload())
.await?;
self.ensure_demo_agent_bindings(
workspace_id,
&AgentId::new(support_agent.id.clone()),
vec![AgentBindingPayload {
operation_id: grpc_operation.id.as_str().to_owned(),
operation_version: grpc_operation.current_draft_version,
tool_name: "lookup_support_ticket".to_owned(),
tool_title: "Lookup Support Ticket".to_owned(),
operation_id: rest_operation.id.as_str().to_owned(),
operation_version: rest_operation.current_draft_version,
tool_name: "create_crm_lead".to_owned(),
tool_title: "Create CRM Lead".to_owned(),
tool_description_override: Some(
"Draft support lookup flow backed by unary gRPC.".to_owned(),
"Create a new CRM lead in the revenue workspace.".to_owned(),
),
enabled: true,
}],
false,
true,
)
.await?;
@@ -4125,23 +4066,9 @@ impl AdminService {
false,
)
.await?;
self.ensure_demo_platform_api_key(
workspace_id,
&AgentId::new(support_agent.id.clone()),
"Readonly Export Key",
vec![PlatformApiKeyScope::Read],
true,
)
.await?;
self.seed_demo_invocation_logs(
workspace_id,
&AgentId::new(revops_agent.id),
&rest_operation.id,
&graphql_operation.id,
&grpc_operation.id,
)
.await?;
self.seed_demo_invocation_logs(workspace_id, &AgentId::new(revops_agent.id), &rest_operation.id)
.await?;
Ok(())
}
@@ -4353,49 +4280,6 @@ impl AdminService {
Ok(())
}
async fn ensure_demo_grpc_artifacts(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
) -> Result<(), ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
let descriptors = self
.registry
.list_descriptor_metadata(operation_id, summary.current_draft_version)
.await?;
let has_proto = descriptors
.iter()
.any(|item| item.descriptor_kind == crank_registry::DescriptorKind::ProtoUpload);
let has_descriptor = descriptors
.iter()
.any(|item| item.descriptor_kind == crank_registry::DescriptorKind::DescriptorSet);
if !has_proto {
self.upload_proto_file(
workspace_id,
operation_id,
Some("echo.proto"),
DEMO_GRPC_PROTO.as_bytes(),
)
.await?;
}
if !has_descriptor {
let descriptor_set = STANDARD
.decode(grpc_test_support::descriptor_set_b64())
.map_err(|error| ApiError::internal(error.to_string()))?;
self.upload_descriptor_set(
workspace_id,
operation_id,
Some("echo_descriptor.bin"),
&descriptor_set,
)
.await?;
}
Ok(())
}
async fn ensure_demo_agent(
&self,
workspace_id: &WorkspaceId,
@@ -4437,8 +4321,6 @@ impl AdminService {
workspace_id: &WorkspaceId,
revops_agent_id: &AgentId,
rest_operation_id: &OperationId,
graphql_operation_id: &OperationId,
grpc_operation_id: &OperationId,
) -> Result<(), ApiError> {
if !self
.registry
@@ -4467,25 +4349,6 @@ impl AdminService {
.current_draft_version,
)
.await?;
let graphql_operation = self
.get_operation_version(
workspace_id,
graphql_operation_id,
self.get_operation(workspace_id, graphql_operation_id)
.await?
.current_draft_version,
)
.await?;
let grpc_operation = self
.get_operation_version(
workspace_id,
grpc_operation_id,
self.get_operation(workspace_id, grpc_operation_id)
.await?
.current_draft_version,
)
.await?;
self.record_invocation(InvocationRecordRequest {
workspace_id,
agent_id: Some(revops_agent_id),
@@ -4509,78 +4372,6 @@ impl AdminService {
response_preview: demo_rest_response_sample(),
})
.await?;
self.record_invocation(InvocationRecordRequest {
workspace_id,
agent_id: Some(revops_agent_id),
operation: &graphql_operation.snapshot,
request_id: None,
source: InvocationSource::AgentToolCall,
level: InvocationLevel::Info,
status: InvocationStatus::Ok,
message: "invoice status resolved".to_owned(),
status_code: Some(200),
error_kind: None,
duration_ms: 126,
request_preview: json!({
"path": {},
"query": {},
"headers": { "x-demo-source": "crank-seed" },
"variables": demo_graphql_variables_sample(),
"grpc": null,
"body": null
}),
response_preview: demo_graphql_response_sample(),
})
.await?;
self.record_invocation(InvocationRecordRequest {
workspace_id,
agent_id: Some(revops_agent_id),
operation: &graphql_operation.snapshot,
request_id: None,
source: InvocationSource::AgentToolCall,
level: InvocationLevel::Error,
status: InvocationStatus::Error,
message: "billing service returned operation error".to_owned(),
status_code: Some(502),
error_kind: Some("graphql_error".to_owned()),
duration_ms: 412,
request_preview: json!({
"path": {},
"query": {},
"headers": { "x-demo-source": "crank-seed" },
"variables": { "invoiceId": "inv_4099" },
"grpc": null,
"body": null
}),
response_preview: json!({
"errors": [{ "message": "invoice not available" }]
}),
})
.await?;
self.record_invocation(InvocationRecordRequest {
workspace_id,
agent_id: None,
operation: &grpc_operation.snapshot,
request_id: None,
source: InvocationSource::AdminTestRun,
level: InvocationLevel::Info,
status: InvocationStatus::Ok,
message: "descriptor-backed unary echo tested".to_owned(),
status_code: Some(200),
error_kind: None,
duration_ms: 98,
request_preview: json!({
"path": {},
"query": {},
"headers": {},
"variables": null,
"grpc": demo_grpc_request_sample(),
"body": null
}),
response_preview: demo_grpc_response_sample(),
})
.await?;
Ok(())
}
@@ -4616,21 +4407,6 @@ impl AdminService {
}
const DEMO_USER_PASSWORD: &str = "CrankDemoPass123!";
const DEMO_GRPC_PROTO: &str = r#"syntax = "proto3";
package echo;
service EchoService {
rpc UnaryEcho (EchoRequest) returns (EchoResponse);
}
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
"#;
fn build_request_preview(
mapping: &MappingSet,
@@ -4728,21 +4504,6 @@ fn demo_revops_agent_payload() -> AgentPayload {
}
}
fn demo_support_agent_payload() -> AgentPayload {
AgentPayload {
slug: "support-triage".to_owned(),
display_name: "Support Triage".to_owned(),
description: "Draft support agent focused on unary gRPC workflows.".to_owned(),
instructions: json!({
"system": "Use support tools carefully and ask for confirmation before mutating state."
}),
tool_selection_policy: json!({
"max_tools": 4,
"prefer_tag": ["support"]
}),
}
}
fn demo_rest_operation_payload() -> OperationPayload {
let input = demo_rest_input_sample();
let request = demo_rest_request_sample();
@@ -4797,115 +4558,6 @@ fn demo_rest_operation_payload() -> OperationPayload {
}
}
fn demo_graphql_operation_payload() -> OperationPayload {
let input = demo_graphql_input_sample();
let variables = demo_graphql_variables_sample();
let data = demo_graphql_data_sample();
let output = demo_graphql_output_sample();
OperationPayload {
name: "billing_get_invoice".to_owned(),
display_name: "Get Invoice Status".to_owned(),
category: "finance".to_owned(),
protocol: Protocol::Graphql,
security_level: OperationSecurityLevel::Standard,
target: Target::Graphql(crank_core::GraphqlTarget {
endpoint: "https://billing.demo.internal/graphql".to_owned(),
operation_type: crank_core::GraphqlOperationType::Query,
operation_name: "InvoiceById".to_owned(),
query_template: "query InvoiceById($invoiceId: ID!) { invoice(id: $invoiceId) { id status total currency customerName } }".to_owned(),
response_path: "$.response.body.data.invoice".to_owned(),
}),
input_schema: Schema::from_json_sample(&input),
output_schema: Schema::from_json_sample(&output),
input_mapping: infer_mapping_from_samples(
&input,
JsonPathRoot::Mcp,
&variables,
JsonPathRoot::RequestVariables,
),
output_mapping: infer_mapping_from_samples(
&data,
JsonPathRoot::ResponseData,
&output,
JsonPathRoot::Output,
),
execution_config: crank_core::ExecutionConfig {
timeout_ms: 8_000,
retry_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
streaming: None,
},
tool_description: crank_core::ToolDescription {
title: "Get Invoice Status".to_owned(),
description: "Fetch invoice state and amount from billing.".to_owned(),
tags: vec!["finance".to_owned(), "billing".to_owned()],
examples: vec![crank_core::ToolExample {
input: json!({ "invoiceId": "inv_1001" }),
}],
},
}
}
fn demo_grpc_operation_payload() -> OperationPayload {
let input = demo_grpc_input_sample();
let request = demo_grpc_request_sample();
let response = demo_grpc_response_sample();
let output = demo_grpc_output_sample();
OperationPayload {
name: "support_lookup_ticket".to_owned(),
display_name: "Lookup Support Ticket".to_owned(),
category: "support".to_owned(),
protocol: Protocol::Grpc,
security_level: OperationSecurityLevel::Standard,
target: Target::Grpc(crank_core::GrpcTarget {
server_addr: "http://grpc.demo.internal".to_owned(),
package: "echo".to_owned(),
service: "EchoService".to_owned(),
method: "UnaryEcho".to_owned(),
descriptor_ref: crank_core::DescriptorId::new("desc_echo_demo"),
descriptor_set_b64: grpc_test_support::descriptor_set_b64(),
}),
input_schema: Schema::from_json_sample(&input),
output_schema: Schema::from_json_sample(&output),
input_mapping: infer_mapping_from_samples(
&input,
JsonPathRoot::Mcp,
&request,
JsonPathRoot::RequestGrpc,
),
output_mapping: infer_mapping_from_samples(
&response,
JsonPathRoot::ResponseGrpc,
&output,
JsonPathRoot::Output,
),
execution_config: crank_core::ExecutionConfig {
timeout_ms: 5_000,
retry_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: Some(crank_core::ProtocolOptions {
grpc: Some(crank_core::GrpcProtocolOptions { use_tls: false }),
websocket: None,
soap: None,
}),
streaming: None,
},
tool_description: crank_core::ToolDescription {
title: "Lookup Support Ticket".to_owned(),
description: "Draft unary gRPC support lookup using a descriptor set.".to_owned(),
tags: vec!["support".to_owned(), "grpc".to_owned()],
examples: vec![crank_core::ToolExample {
input: json!({ "message": "ticket: T-1001" }),
}],
},
}
}
fn demo_archived_operation_payload() -> OperationPayload {
let input = json!({
"contactId": "contact_123",
@@ -4989,50 +4641,6 @@ fn demo_rest_output_sample() -> Value {
demo_rest_response_sample()
}
fn demo_graphql_input_sample() -> Value {
json!({ "invoiceId": "inv_1001" })
}
fn demo_graphql_variables_sample() -> Value {
json!({ "invoiceId": "inv_1001" })
}
fn demo_graphql_data_sample() -> Value {
json!({
"id": "inv_1001",
"status": "paid",
"total": 4200,
"currency": "USD",
"customerName": "Cyberdyne"
})
}
fn demo_graphql_response_sample() -> Value {
json!({
"invoice": demo_graphql_data_sample()
})
}
fn demo_graphql_output_sample() -> Value {
demo_graphql_data_sample()
}
fn demo_grpc_input_sample() -> Value {
json!({ "message": "ticket: T-1001" })
}
fn demo_grpc_request_sample() -> Value {
demo_grpc_input_sample()
}
fn demo_grpc_response_sample() -> Value {
json!({ "message": "ticket: T-1001 is currently open" })
}
fn demo_grpc_output_sample() -> Value {
demo_grpc_response_sample()
}
fn default_enabled() -> bool {
true
}