product: make community rest only
This commit is contained in:
@@ -44,6 +44,7 @@ Progress:
|
|||||||
- CI, README, runtime/deploy smoke docs now point to `deploy/community/*` as the Community delivery source of truth
|
- CI, README, runtime/deploy smoke docs now point to `deploy/community/*` as the Community delivery source of truth
|
||||||
- separate `Community` release checklist now exists and explicitly forbids treating future `Enterprise/Cloud` delivery as just another env on the same public manifest
|
- separate `Community` release checklist now exists and explicitly forbids treating future `Enterprise/Cloud` delivery as just another env on the same public manifest
|
||||||
- explicit repository split map now defines what goes to `crank-community`, `crank-enterprise`, and `crank-cloud`
|
- explicit repository split map now defines what goes to `crank-community`, `crank-enterprise`, and `crank-cloud`
|
||||||
|
- `Community` is now fixed as `REST-only` in product docs, capability model, backend validation, demo seed, and UI protocol expectations
|
||||||
- pending:
|
- pending:
|
||||||
- remaining extension seams and packaging split still need to move from planning into concrete `crank-community` / `crank-enterprise` / `crank-cloud` manifests
|
- remaining extension seams and packaging split still need to move from planning into concrete `crank-community` / `crank-enterprise` / `crank-cloud` manifests
|
||||||
- next management gate is to actually create the `3` target repositories before physical split starts
|
- next management gate is to actually create the `3` target repositories before physical split starts
|
||||||
|
|||||||
@@ -1031,10 +1031,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(response["edition"], "community");
|
assert_eq!(response["edition"], "community");
|
||||||
assert_eq!(
|
assert_eq!(response["supported_protocols"], json!(["rest"]));
|
||||||
response["supported_protocols"],
|
|
||||||
json!(["rest", "graphql", "grpc"])
|
|
||||||
);
|
|
||||||
assert_eq!(response["supported_security_levels"], json!(["standard"]));
|
assert_eq!(response["supported_security_levels"], json!(["standard"]));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response["machine_access_modes"],
|
response["machine_access_modes"],
|
||||||
@@ -1143,7 +1140,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(response["items"].as_array().unwrap().len(), 3);
|
assert_eq!(response["items"].as_array().unwrap().len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response["items"]
|
response["items"]
|
||||||
.as_array()
|
.as_array()
|
||||||
@@ -1151,7 +1148,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|item| item["protocol"].as_str().unwrap())
|
.map(|item| item["protocol"].as_str().unwrap())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
vec!["rest", "graphql", "grpc"]
|
vec!["rest"]
|
||||||
);
|
);
|
||||||
for item in response["items"].as_array().unwrap() {
|
for item in response["items"].as_array().unwrap() {
|
||||||
assert_eq!(item["supports_execution_modes"], json!(["unary"]));
|
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")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn rejects_unsupported_protocol_for_community_operation_create() {
|
async fn rejects_unsupported_protocol_for_community_operation_create() {
|
||||||
|
|||||||
+11
-403
@@ -3,9 +3,8 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use base64::{
|
use base64::{
|
||||||
Engine as _,
|
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_adapter_soap::{SoapServiceSummary, inspect_wsdl};
|
||||||
use crank_core::{
|
use crank_core::{
|
||||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
||||||
@@ -1574,7 +1573,7 @@ impl AdminService {
|
|||||||
pub async fn get_capabilities(&self) -> EditionCapabilities {
|
pub async fn get_capabilities(&self) -> EditionCapabilities {
|
||||||
EditionCapabilities {
|
EditionCapabilities {
|
||||||
edition: ProductEdition::Community,
|
edition: ProductEdition::Community,
|
||||||
supported_protocols: vec![Protocol::Rest, Protocol::Graphql, Protocol::Grpc],
|
supported_protocols: vec![Protocol::Rest],
|
||||||
supported_security_levels: vec![OperationSecurityLevel::Standard],
|
supported_security_levels: vec![OperationSecurityLevel::Standard],
|
||||||
machine_access_modes: vec![MachineAccessMode::StaticAgentKey],
|
machine_access_modes: vec![MachineAccessMode::StaticAgentKey],
|
||||||
limits: EditionLimits {
|
limits: EditionLimits {
|
||||||
@@ -3909,7 +3908,7 @@ impl AdminService {
|
|||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
security_level: OperationSecurityLevel,
|
security_level: OperationSecurityLevel,
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let supported_protocols = [Protocol::Rest, Protocol::Graphql, Protocol::Grpc];
|
let supported_protocols = [Protocol::Rest];
|
||||||
if !supported_protocols.contains(&protocol) {
|
if !supported_protocols.contains(&protocol) {
|
||||||
return Err(ApiError::validation_with_context(
|
return Err(ApiError::validation_with_context(
|
||||||
format!(
|
format!(
|
||||||
@@ -4033,32 +4032,6 @@ impl AdminService {
|
|||||||
)
|
)
|
||||||
.await?;
|
.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
|
let archived_operation = self
|
||||||
.ensure_demo_operation(workspace_id, demo_archived_operation_payload())
|
.ensure_demo_operation(workspace_id, demo_archived_operation_payload())
|
||||||
.await?;
|
.await?;
|
||||||
@@ -4071,49 +4044,17 @@ impl AdminService {
|
|||||||
self.ensure_demo_agent_bindings(
|
self.ensure_demo_agent_bindings(
|
||||||
workspace_id,
|
workspace_id,
|
||||||
&AgentId::new(revops_agent.id.clone()),
|
&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 {
|
vec![AgentBindingPayload {
|
||||||
operation_id: grpc_operation.id.as_str().to_owned(),
|
operation_id: rest_operation.id.as_str().to_owned(),
|
||||||
operation_version: grpc_operation.current_draft_version,
|
operation_version: rest_operation.current_draft_version,
|
||||||
tool_name: "lookup_support_ticket".to_owned(),
|
tool_name: "create_crm_lead".to_owned(),
|
||||||
tool_title: "Lookup Support Ticket".to_owned(),
|
tool_title: "Create CRM Lead".to_owned(),
|
||||||
tool_description_override: Some(
|
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,
|
enabled: true,
|
||||||
}],
|
}],
|
||||||
false,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -4125,23 +4066,9 @@ impl AdminService {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.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(
|
self.seed_demo_invocation_logs(workspace_id, &AgentId::new(revops_agent.id), &rest_operation.id)
|
||||||
workspace_id,
|
.await?;
|
||||||
&AgentId::new(revops_agent.id),
|
|
||||||
&rest_operation.id,
|
|
||||||
&graphql_operation.id,
|
|
||||||
&grpc_operation.id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -4353,49 +4280,6 @@ impl AdminService {
|
|||||||
Ok(())
|
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(
|
async fn ensure_demo_agent(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &WorkspaceId,
|
workspace_id: &WorkspaceId,
|
||||||
@@ -4437,8 +4321,6 @@ impl AdminService {
|
|||||||
workspace_id: &WorkspaceId,
|
workspace_id: &WorkspaceId,
|
||||||
revops_agent_id: &AgentId,
|
revops_agent_id: &AgentId,
|
||||||
rest_operation_id: &OperationId,
|
rest_operation_id: &OperationId,
|
||||||
graphql_operation_id: &OperationId,
|
|
||||||
grpc_operation_id: &OperationId,
|
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
if !self
|
if !self
|
||||||
.registry
|
.registry
|
||||||
@@ -4467,25 +4349,6 @@ impl AdminService {
|
|||||||
.current_draft_version,
|
.current_draft_version,
|
||||||
)
|
)
|
||||||
.await?;
|
.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 {
|
self.record_invocation(InvocationRecordRequest {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
agent_id: Some(revops_agent_id),
|
agent_id: Some(revops_agent_id),
|
||||||
@@ -4509,78 +4372,6 @@ impl AdminService {
|
|||||||
response_preview: demo_rest_response_sample(),
|
response_preview: demo_rest_response_sample(),
|
||||||
})
|
})
|
||||||
.await?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4616,21 +4407,6 @@ impl AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEMO_USER_PASSWORD: &str = "CrankDemoPass123!";
|
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(
|
fn build_request_preview(
|
||||||
mapping: &MappingSet,
|
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 {
|
fn demo_rest_operation_payload() -> OperationPayload {
|
||||||
let input = demo_rest_input_sample();
|
let input = demo_rest_input_sample();
|
||||||
let request = demo_rest_request_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 {
|
fn demo_archived_operation_payload() -> OperationPayload {
|
||||||
let input = json!({
|
let input = json!({
|
||||||
"contactId": "contact_123",
|
"contactId": "contact_123",
|
||||||
@@ -4989,50 +4641,6 @@ fn demo_rest_output_sample() -> Value {
|
|||||||
demo_rest_response_sample()
|
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 {
|
fn default_enabled() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ test('shell and wizard expose stable diagnostics hooks', async ({ page }) => {
|
|||||||
await page.goto('/wizard/');
|
await page.goto('/wizard/');
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-grid"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-grid"]')).toBeVisible();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="wizard-protocol-graphql"]')).toBeHidden();
|
||||||
|
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeHidden();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
|
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
|
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
|
||||||
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
|
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ const { login, localized } = require('./helpers');
|
|||||||
test('operations page shows demo catalog and filter works', async ({ page }) => {
|
test('operations page shows demo catalog and filter works', async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
await expect(page.locator('.page-heading')).toHaveText(localized('Operations', 'Операции'));
|
await expect(page.locator('.page-heading')).toHaveText(localized('Operations', 'Операции'));
|
||||||
await expect(page.locator('tbody tr')).toHaveCount(4);
|
await expect(page.locator('tbody tr')).toHaveCount(2);
|
||||||
await page.getByPlaceholder(localized('Search operations', 'Поиск операций')).fill('support');
|
await page.getByPlaceholder(localized('Search operations', 'Поиск операций')).fill('crm');
|
||||||
await expect(page.locator('tbody tr')).toHaveCount(1);
|
await expect(page.locator('tbody tr')).toHaveCount(1);
|
||||||
await expect(page.locator('tbody tr').first()).toContainText(/support_lookup_ticket/i);
|
await expect(page.locator('tbody tr').first()).toContainText(/crm_create_lead/i);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
|||||||
await page.goto('/wizard/');
|
await page.goto('/wizard/');
|
||||||
await expect(page.locator('[data-step-counter]').first()).toContainText(localized('Step', 'Шаг'));
|
await expect(page.locator('[data-step-counter]').first()).toContainText(localized('Step', 'Шаг'));
|
||||||
await expect(page.locator('#step-panel-1 .step-panel-title')).toContainText(localized('Choose a protocol', 'Выберите протокол'));
|
await expect(page.locator('#step-panel-1 .step-panel-title')).toContainText(localized('Choose a protocol', 'Выберите протокол'));
|
||||||
await page.getByText(/graphql/i).first().click();
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
||||||
await page.locator('#btn-continue').click();
|
await page.locator('#btn-continue').click();
|
||||||
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
||||||
});
|
});
|
||||||
@@ -15,13 +15,13 @@ test('community wizard hides premium protocols and explains edition scope', asyn
|
|||||||
await login(page);
|
await login(page);
|
||||||
await page.goto('/wizard/');
|
await page.goto('/wizard/');
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-graphql"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-graphql"]')).toBeHidden();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeHidden();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
|
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
|
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
|
||||||
await expect(page.locator('#wizard-edition-protocol-note')).toContainText(localized('Commercial editions unlock', 'коммерческих редакциях'));
|
await expect(page.locator('#wizard-edition-protocol-note')).toContainText(localized('Commercial editions unlock', 'коммерческих редакциях'));
|
||||||
|
|
||||||
await page.locator('[data-testid="wizard-protocol-grpc"]').click();
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
||||||
await page.locator('#btn-continue').click();
|
await page.locator('#btn-continue').click();
|
||||||
await page.locator('#btn-continue').click();
|
await page.locator('#btn-continue').click();
|
||||||
await page.locator('#btn-continue').click();
|
await page.locator('#btn-continue').click();
|
||||||
|
|||||||
+1
-5
@@ -68,8 +68,6 @@
|
|||||||
- `GET /protocol-capabilities` возвращает только те протоколы и execution capabilities, которые реально доступны в текущей редакции;
|
- `GET /protocol-capabilities` возвращает только те протоколы и execution capabilities, которые реально доступны в текущей редакции;
|
||||||
- для `Community` это означает только:
|
- для `Community` это означает только:
|
||||||
- `REST`
|
- `REST`
|
||||||
- `GraphQL`
|
|
||||||
- `gRPC`
|
|
||||||
|
|
||||||
### 5.1. Workspaces and members
|
### 5.1. Workspaces and members
|
||||||
|
|
||||||
@@ -139,9 +137,7 @@
|
|||||||
- если поле не передано, используется `standard` для обратной совместимости с уже существующим YAML и draft payload;
|
- если поле не передано, используется `standard` для обратной совместимости с уже существующим YAML и draft payload;
|
||||||
- в `Community` backend принимает только:
|
- в `Community` backend принимает только:
|
||||||
- `REST`
|
- `REST`
|
||||||
- `GraphQL`
|
- `security_level = standard`
|
||||||
- `gRPC`
|
|
||||||
- `security_level = standard`
|
|
||||||
- попытка создать, обновить или импортировать операцию с неподдерживаемым `protocol` или `security_level` должна завершаться `validation_error` еще на стороне `admin-api`, а не только скрываться в UI.
|
- попытка создать, обновить или импортировать операцию с неподдерживаемым `protocol` или `security_level` должна завершаться `validation_error` еще на стороне `admin-api`, а не только скрываться в UI.
|
||||||
|
|
||||||
### 5.4. Samples and descriptors
|
### 5.4. Samples and descriptors
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
- доменная модель Community;
|
- доменная модель Community;
|
||||||
- `admin-api`, `mcp-server` и `ui`, необходимые для Community;
|
- `admin-api`, `mcp-server` и `ui`, необходимые для Community;
|
||||||
- `REST`, `GraphQL` и `gRPC unary` в открытой редакции;
|
- `REST` в открытой редакции;
|
||||||
- секреты, auth profiles, agent publishing, logs и usage;
|
- секреты, auth profiles, agent publishing, logs и usage;
|
||||||
- статический ключ AI-агента;
|
- статический ключ AI-агента;
|
||||||
- контейнерное развертывание Community;
|
- контейнерное развертывание Community;
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
- short-lived token service;
|
- short-lived token service;
|
||||||
- one-time token service;
|
- one-time token service;
|
||||||
|
- `GraphQL` и `gRPC unary`, если они выводятся из Community;
|
||||||
- `SSO`, `2FA`, расширенная `RBAC`, `audit log`;
|
- `SSO`, `2FA`, расширенная `RBAC`, `audit log`;
|
||||||
- `WebSocket`, `SOAP`, `gRPC streaming`, если они не включаются в Community;
|
- `WebSocket`, `SOAP`, `gRPC streaming`, если они не включаются в Community;
|
||||||
- advanced streaming execution modes;
|
- advanced streaming execution modes;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
UI должен честно отражать различия редакций:
|
UI должен честно отражать различия редакций:
|
||||||
|
|
||||||
- Community не должен показывать доступные к настройке `WebSocket`, `SOAP`, `gRPC streaming`, если они не входят в открытую поставку;
|
- Community не должен показывать доступные к настройке `GraphQL`, `gRPC`, `WebSocket`, `SOAP`, если они не входят в открытую поставку;
|
||||||
- `security_level = elevated` и `security_level = strict` не должны выглядеть рабочими в Community;
|
- `security_level = elevated` и `security_level = strict` не должны выглядеть рабочими в Community;
|
||||||
- multi-user и enterprise controls должны скрываться или отображаться как capability-locked.
|
- multi-user и enterprise controls должны скрываться или отображаться как capability-locked.
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ Crank развивается как три редакции:
|
|||||||
- один AI-агент;
|
- один AI-агент;
|
||||||
- протоколы:
|
- протоколы:
|
||||||
- `REST / HTTP`
|
- `REST / HTTP`
|
||||||
- `GraphQL`
|
|
||||||
- `gRPC unary`
|
|
||||||
- создание и публикация операций;
|
- создание и публикация операций;
|
||||||
- журналы вызовов и метрики использования;
|
- журналы вызовов и метрики использования;
|
||||||
- секреты и профили аутентификации для внешних сервисов;
|
- секреты и профили аутентификации для внешних сервисов;
|
||||||
@@ -58,6 +56,8 @@ Crank развивается как три редакции:
|
|||||||
- несколько пользователей;
|
- несколько пользователей;
|
||||||
- `SSO`, `2FA`, развитая `RBAC`, `audit log`;
|
- `SSO`, `2FA`, развитая `RBAC`, `audit log`;
|
||||||
- короткоживущие и одноразовые машинные токены;
|
- короткоживущие и одноразовые машинные токены;
|
||||||
|
- `GraphQL`;
|
||||||
|
- `gRPC unary`;
|
||||||
- `WebSocket`, `SOAP`, `gRPC streaming`;
|
- `WebSocket`, `SOAP`, `gRPC streaming`;
|
||||||
- режимы `window`, `session`, `async_job`;
|
- режимы `window`, `session`, `async_job`;
|
||||||
- биллинг и usage metering для SaaS;
|
- биллинг и usage metering для SaaS;
|
||||||
@@ -79,6 +79,8 @@ Crank развивается как три редакции:
|
|||||||
- несколько рабочих областей;
|
- несколько рабочих областей;
|
||||||
- несколько пользователей;
|
- несколько пользователей;
|
||||||
- `SSO`, `2FA`, расширенная `RBAC`, `audit log`;
|
- `SSO`, `2FA`, расширенная `RBAC`, `audit log`;
|
||||||
|
- `GraphQL`;
|
||||||
|
- `gRPC unary`;
|
||||||
- короткоживущие машинные токены;
|
- короткоживущие машинные токены;
|
||||||
- одноразовые токены для критичных операций;
|
- одноразовые токены для критичных операций;
|
||||||
- `security_level = elevated` и `security_level = strict`;
|
- `security_level = elevated` и `security_level = strict`;
|
||||||
@@ -114,7 +116,8 @@ Crank развивается как три редакции:
|
|||||||
| Workspace count | 1 | many | many |
|
| Workspace count | 1 | many | many |
|
||||||
| User count | 1 | many | many |
|
| User count | 1 | many | many |
|
||||||
| Agent count | 1 | many | many |
|
| Agent count | 1 | many | many |
|
||||||
| REST / GraphQL / gRPC unary | yes | yes | yes |
|
| REST / HTTP | yes | yes | yes |
|
||||||
|
| GraphQL / gRPC unary | no | yes | yes |
|
||||||
| WebSocket / SOAP / gRPC streaming | no | yes | yes |
|
| WebSocket / SOAP / gRPC streaming | no | yes | yes |
|
||||||
| Streaming modes | no | yes | yes |
|
| Streaming modes | no | yes | yes |
|
||||||
| Static agent key | yes | yes | yes |
|
| Static agent key | yes | yes | yes |
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
Что нужно:
|
Что нужно:
|
||||||
|
|
||||||
- определить Community-supported protocol set;
|
- определить Community-supported protocol set;
|
||||||
|
- зафиксировать `REST` как единственный Community protocol surface;
|
||||||
- вынести premium protocols и premium execution modes в private delivery plan;
|
- вынести premium protocols и premium execution modes в private delivery plan;
|
||||||
- синхронизировать UI, docs и release process.
|
- синхронизировать UI, docs и release process.
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ Public repository.
|
|||||||
- Community runtime;
|
- Community runtime;
|
||||||
- Community UI;
|
- Community UI;
|
||||||
- `admin-api`, `mcp-server`, `ui`;
|
- `admin-api`, `mcp-server`, `ui`;
|
||||||
- `crank-core`, `crank-registry`, `crank-runtime`, adapters, schema and mapping crates;
|
- `crank-core`, `crank-registry`, `crank-runtime`, schema and mapping crates;
|
||||||
|
- только `REST`-ориентированный Community protocol surface;
|
||||||
- capability model;
|
- capability model;
|
||||||
- Community auth model:
|
- Community auth model:
|
||||||
- static AI-agent key;
|
- static AI-agent key;
|
||||||
@@ -63,6 +64,7 @@ Private repository.
|
|||||||
В нем должны жить:
|
В нем должны жить:
|
||||||
|
|
||||||
- self-hosted commercial extensions;
|
- self-hosted commercial extensions;
|
||||||
|
- `GraphQL` и `gRPC unary`, если они окончательно выводятся из Community;
|
||||||
- short-lived token service;
|
- short-lived token service;
|
||||||
- one-time token service;
|
- one-time token service;
|
||||||
- enterprise access/governance:
|
- enterprise access/governance:
|
||||||
@@ -99,9 +101,12 @@ Private repository.
|
|||||||
- весь текущий Community runtime и UI
|
- весь текущий Community runtime и UI
|
||||||
- все public extension seams
|
- все public extension seams
|
||||||
- все capability checks
|
- все capability checks
|
||||||
|
- `REST`-only product surface
|
||||||
|
|
||||||
Уходит в `crank-enterprise`:
|
Уходит в `crank-enterprise`:
|
||||||
|
|
||||||
|
- `crank-adapter-graphql`
|
||||||
|
- `crank-adapter-grpc`
|
||||||
- private auth/token/governance services
|
- private auth/token/governance services
|
||||||
- enterprise-only runtime additions
|
- enterprise-only runtime additions
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user