cache: define response cache boundaries

This commit is contained in:
a.tolmachev
2026-05-03 22:22:16 +00:00
parent d3b7d246da
commit 851d70ad7a
9 changed files with 234 additions and 19 deletions
+73 -3
View File
@@ -240,9 +240,9 @@ mod tests {
use crank_core::{
AsyncJobHandle, AsyncJobId, DescriptorId, ExecutionConfig, ExecutionMode,
GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod, JobStatus, MembershipRole,
OperationId, OperationSecurityLevel, Protocol, RestTarget, SecretKind, SoapBindingStyle,
SoapOperationMetadata, SoapTarget, SoapVersion, StreamSession, StreamStatus, Target,
ToolDescription, TransportBehavior, WebsocketTarget, WorkspaceId,
OperationId, OperationSecurityLevel, Protocol, ResponseCachePolicy, RestTarget, SecretKind,
SoapBindingStyle, SoapOperationMetadata, SoapTarget, SoapVersion, StreamSession,
StreamStatus, Target, ToolDescription, TransportBehavior, WebsocketTarget, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_registry::{CreateAsyncJobRequest, CreateStreamSessionRequest, PostgresRegistry};
@@ -1362,6 +1362,71 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_response_cache_for_non_get_rest_operation_create() {
let registry = test_registry().await;
let storage_root = test_storage_root("community_response_cache_post_reject");
let upstream_base_url = spawn_upstream_server().await;
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let mut payload = test_operation_payload(&upstream_base_url, "crm_cache_post");
payload.execution_config.response_cache = Some(ResponseCachePolicy { ttl_ms: 5_000 });
let response = client
.post(format!("{base_url}/operations"))
.json(&payload)
.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"]["context"]["field"],
"execution_config.response_cache"
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_response_cache_for_operation_with_auth_profile() {
let registry = test_registry().await;
let storage_root = test_storage_root("community_response_cache_auth_reject");
let upstream_base_url = spawn_upstream_server().await;
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let mut payload = test_operation_payload(&upstream_base_url, "crm_cache_auth");
payload.target = Target::Rest(RestTarget {
base_url: upstream_base_url,
method: HttpMethod::Get,
path_template: "/crm/leads".to_owned(),
static_headers: BTreeMap::new(),
});
payload.execution_config.response_cache = Some(ResponseCachePolicy { ttl_ms: 5_000 });
payload.execution_config.auth_profile_ref = Some("auth_profile_01".into());
let response = client
.post(format!("{base_url}/operations"))
.json(&payload)
.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"]["context"]["field"],
"execution_config.auth_profile_ref"
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn manages_workspace_access_lifecycle() {
@@ -3455,6 +3520,7 @@ mod tests {
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
@@ -3512,6 +3578,7 @@ mod tests {
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
@@ -3568,6 +3635,7 @@ mod tests {
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
@@ -3725,6 +3793,7 @@ mod tests {
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
@@ -3780,6 +3849,7 @@ mod tests {
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
+55 -9
View File
@@ -1,10 +1,7 @@
use std::collections::BTreeMap;
use std::path::PathBuf;
use base64::{
Engine as _,
engine::general_purpose::URL_SAFE_NO_PAD,
};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use crank_adapter_soap::{SoapServiceSummary, inspect_wsdl};
use crank_core::{
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
@@ -15,9 +12,9 @@ use crank_core::{
IssueOneTimeAgentTokenRequest, IssuedAgentTokenResponse, JobStatus, MachineAccessMode,
MembershipRole, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey,
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, ProductEdition, Protocol,
SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus, StreamSession, StreamStatus,
Target, TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace, WorkspaceId,
WorkspaceStatus,
ResponseCachePolicy, SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus,
StreamSession, StreamStatus, Target, TransportBehavior, UsagePeriod, UserId, UserSessionId,
Workspace, WorkspaceId, WorkspaceStatus,
};
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
@@ -3890,6 +3887,7 @@ 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_response_cache_policy(&payload.target, &payload.execution_config)?;
payload.input_mapping.validate_paths()?;
payload.output_mapping.validate_paths()?;
Ok(())
@@ -3898,6 +3896,7 @@ 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_response_cache_policy(&operation.target, &operation.execution_config)?;
operation.input_mapping.validate_paths()?;
operation.output_mapping.validate_paths()?;
Ok(())
@@ -4067,8 +4066,12 @@ impl AdminService {
)
.await?;
self.seed_demo_invocation_logs(workspace_id, &AgentId::new(revops_agent.id), &rest_operation.id)
.await?;
self.seed_demo_invocation_logs(
workspace_id,
&AgentId::new(revops_agent.id),
&rest_operation.id,
)
.await?;
Ok(())
}
@@ -4539,6 +4542,7 @@ fn demo_rest_operation_payload() -> OperationPayload {
execution_config: crank_core::ExecutionConfig {
timeout_ms: 10_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
@@ -4601,6 +4605,7 @@ fn demo_archived_operation_payload() -> OperationPayload {
execution_config: crank_core::ExecutionConfig {
timeout_ms: 6_000,
retry_policy: None,
response_cache: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
@@ -5039,6 +5044,47 @@ fn agent_mcp_endpoint(workspace_slug: &str, agent_slug: &str) -> String {
format!("/mcp/v1/{workspace_slug}/{agent_slug}")
}
fn validate_response_cache_policy(
target: &Target,
execution_config: &crank_core::ExecutionConfig,
) -> Result<(), ApiError> {
let Some(ResponseCachePolicy { ttl_ms }) = execution_config.response_cache.as_ref() else {
return Ok(());
};
if *ttl_ms == 0 {
return Err(ApiError::validation_with_context(
"response cache ttl must be greater than zero".to_owned(),
json!({
"field": "execution_config.response_cache.ttl_ms",
}),
));
}
match target {
Target::Rest(rest_target) if rest_target.method == crank_core::HttpMethod::Get => {}
_ => {
return Err(ApiError::validation_with_context(
"response cache is supported only for REST GET operations".to_owned(),
json!({
"field": "execution_config.response_cache",
}),
));
}
}
if execution_config.auth_profile_ref.is_some() {
return Err(ApiError::validation_with_context(
"response cache is not supported for operations with auth_profile_ref".to_owned(),
json!({
"field": "execution_config.auth_profile_ref",
}),
));
}
Ok(())
}
fn enrich_operation_summary(
summary: OperationSummary,
usage_summary: OperationUsageSummaryView,