cache: add graphql query response caching
This commit is contained in:
@@ -38,6 +38,10 @@ Implementation slices:
|
|||||||
- ключи response cache изолируются минимум по `workspace + agent + operation + request fingerprint`;
|
- ключи response cache изолируются минимум по `workspace + agent + operation + request fingerprint`;
|
||||||
- ключи coordination state изолируются минимум по `workspace + agent + cache scope`;
|
- ключи coordination state изолируются минимум по `workspace + agent + cache scope`;
|
||||||
- при отсутствии внешнего cache все эти контуры продолжают работать через in-memory fallback.
|
- при отсутствии внешнего cache все эти контуры продолжают работать через in-memory fallback.
|
||||||
|
7. Довести response cache не только до Community baseline, но и до первых коммерчески ценных read-only hot paths:
|
||||||
|
- `REST GET`
|
||||||
|
- `GraphQL query`
|
||||||
|
при сохранении той же изоляции и opt-in policy.
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- без `Valkey/Redis` Community и self-hosted deployment остаются рабочими;
|
- без `Valkey/Redis` Community и self-hosted deployment остаются рабочими;
|
||||||
@@ -46,6 +50,10 @@ DoD:
|
|||||||
- `Enterprise` может подключать cache layer на своей инфраструктуре, включая кластерный вариант;
|
- `Enterprise` может подключать cache layer на своей инфраструктуре, включая кластерный вариант;
|
||||||
- runtime/docs/manifests не делают cache обязательным для базового запуска.
|
- runtime/docs/manifests не делают cache обязательным для базового запуска.
|
||||||
- cache boundaries и key namespaces документированы так, чтобы разные агенты и рабочие области не пересекались друг с другом.
|
- cache boundaries и key namespaces документированы так, чтобы разные агенты и рабочие области не пересекались друг с другом.
|
||||||
|
- response cache покрывает первый коммерчески значимый protocol set без перехода к небезопасному "cache everything":
|
||||||
|
- `REST GET`
|
||||||
|
- `GraphQL query`
|
||||||
|
- дальнейшее расширение на `gRPC unary` возможно только для явно read-only / cacheable операций.
|
||||||
|
|
||||||
Verification:
|
Verification:
|
||||||
- targeted runtime/admin/mcp tests for cache contracts and fallback mode;
|
- targeted runtime/admin/mcp tests for cache contracts and fallback mode;
|
||||||
@@ -64,8 +72,9 @@ Progress:
|
|||||||
- runtime executor now performs actual response cache reads/writes for eligible `REST GET` operations with keys isolated by `workspace + agent + operation + request fingerprint`
|
- runtime executor now performs actual response cache reads/writes for eligible `REST GET` operations with keys isolated by `workspace + agent + operation + request fingerprint`
|
||||||
- response cache keys now also include `operation version`, so a new published version does not reuse stale cached payloads from the previous one
|
- response cache keys now also include `operation version`, so a new published version does not reuse stale cached payloads from the previous one
|
||||||
- mcp-server published tool catalog now uses shared coordination snapshots across instances, while preserving the same `workspace + agent` isolation model
|
- mcp-server published tool catalog now uses shared coordination snapshots across instances, while preserving the same `workspace + agent` isolation model
|
||||||
|
- response cache runtime/admin contracts now also cover `GraphQL query` as the first commercial protocol extension beyond the `REST GET` baseline
|
||||||
- pending:
|
- pending:
|
||||||
- preserve the same isolation model while extending response cache beyond the first `REST GET` hot path
|
- preserve the same isolation model while extending response cache to the next monetizable read-only path: explicit `gRPC unary` read operations
|
||||||
- wire replay guard into the first real token or nonce flow once commercial machine-token issuance stops being a `Community` stub
|
- wire replay guard into the first real token or nonce flow once commercial machine-token issuance stops being a `Community` stub
|
||||||
- wire coordination state into additional runtime or transport paths beyond the shared published catalog snapshot
|
- wire coordination state into additional runtime or transport paths beyond the shared published catalog snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -5063,9 +5063,12 @@ fn validate_response_cache_policy(
|
|||||||
|
|
||||||
match target {
|
match target {
|
||||||
Target::Rest(rest_target) if rest_target.method == crank_core::HttpMethod::Get => {}
|
Target::Rest(rest_target) if rest_target.method == crank_core::HttpMethod::Get => {}
|
||||||
|
Target::Graphql(graphql_target)
|
||||||
|
if graphql_target.operation_type == crank_core::GraphqlOperationType::Query => {}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ApiError::validation_with_context(
|
return Err(ApiError::validation_with_context(
|
||||||
"response cache is supported only for REST GET operations".to_owned(),
|
"response cache is supported only for REST GET and GraphQL query operations"
|
||||||
|
.to_owned(),
|
||||||
json!({
|
json!({
|
||||||
"field": "execution_config.response_cache",
|
"field": "execution_config.response_cache",
|
||||||
}),
|
}),
|
||||||
@@ -5085,6 +5088,82 @@ fn validate_response_cache_policy(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crank_core::{
|
||||||
|
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, ResponseCachePolicy,
|
||||||
|
RestTarget, Target,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::validate_response_cache_policy;
|
||||||
|
|
||||||
|
fn cacheable_execution_config() -> ExecutionConfig {
|
||||||
|
ExecutionConfig {
|
||||||
|
timeout_ms: 1_000,
|
||||||
|
retry_policy: None,
|
||||||
|
response_cache: Some(ResponseCachePolicy { ttl_ms: 5_000 }),
|
||||||
|
auth_profile_ref: None,
|
||||||
|
headers: BTreeMap::new(),
|
||||||
|
protocol_options: None,
|
||||||
|
streaming: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_response_cache_for_graphql_query() {
|
||||||
|
let target = Target::Graphql(GraphqlTarget {
|
||||||
|
endpoint: "http://example.invalid/graphql".to_owned(),
|
||||||
|
operation_type: 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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = validate_response_cache_policy(&target, &cacheable_execution_config());
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_response_cache_for_graphql_mutation() {
|
||||||
|
let target = Target::Graphql(GraphqlTarget {
|
||||||
|
endpoint: "http://example.invalid/graphql".to_owned(),
|
||||||
|
operation_type: GraphqlOperationType::Mutation,
|
||||||
|
operation_name: "CreateLead".to_owned(),
|
||||||
|
query_template:
|
||||||
|
"mutation CreateLead($email: String!) { createLead(email: $email) { id } }"
|
||||||
|
.to_owned(),
|
||||||
|
response_path: "$.response.body.data.createLead".to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let error =
|
||||||
|
validate_response_cache_policy(&target, &cacheable_execution_config()).unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, crate::error::ApiError::Validation { .. }));
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"response cache is supported only for REST GET and GraphQL query operations"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_response_cache_for_rest_get() {
|
||||||
|
let target = Target::Rest(RestTarget {
|
||||||
|
base_url: "http://example.invalid".to_owned(),
|
||||||
|
method: HttpMethod::Get,
|
||||||
|
path_template: "/catalog".to_owned(),
|
||||||
|
static_headers: BTreeMap::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = validate_response_cache_policy(&target, &cacheable_execution_config());
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn enrich_operation_summary(
|
fn enrich_operation_summary(
|
||||||
summary: OperationSummary,
|
summary: OperationSummary,
|
||||||
usage_summary: OperationUsageSummaryView,
|
usage_summary: OperationUsageSummaryView,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ pub struct CachedResponse {
|
|||||||
pub status: u16,
|
pub status: u16,
|
||||||
pub headers: Vec<CachedHeader>,
|
pub headers: Vec<CachedHeader>,
|
||||||
pub body: Vec<u8>,
|
pub body: Vec<u8>,
|
||||||
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -679,6 +679,7 @@ mod tests {
|
|||||||
value: "application/json".to_owned(),
|
value: "application/json".to_owned(),
|
||||||
}],
|
}],
|
||||||
body: br#"{"ok":true}"#.to_vec(),
|
body: br#"{"ok":true}"#.to_vec(),
|
||||||
|
data: br#"{"ok":true}"#.to_vec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
store
|
store
|
||||||
@@ -825,6 +826,7 @@ mod tests {
|
|||||||
status: 200,
|
status: 200,
|
||||||
headers: vec![],
|
headers: vec![],
|
||||||
body: b"ok".to_vec(),
|
body: b"ok".to_vec(),
|
||||||
|
data: br#"null"#.to_vec(),
|
||||||
},
|
},
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
)
|
)
|
||||||
@@ -868,6 +870,7 @@ mod tests {
|
|||||||
value: "hit".to_owned(),
|
value: "hit".to_owned(),
|
||||||
}],
|
}],
|
||||||
body: br#"{"queued":true}"#.to_vec(),
|
body: br#"{"queued":true}"#.to_vec(),
|
||||||
|
data: br#"null"#.to_vec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoded = serde_json::to_vec(&response).unwrap();
|
let encoded = serde_json::to_vec(&response).unwrap();
|
||||||
|
|||||||
@@ -757,10 +757,12 @@ fn log_runtime_event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn response_cache_ttl(operation: &RuntimeOperation) -> Option<Duration> {
|
fn response_cache_ttl(operation: &RuntimeOperation) -> Option<Duration> {
|
||||||
if !matches!(
|
let is_cacheable_protocol = match &operation.target {
|
||||||
&operation.target,
|
Target::Rest(target) => target.method == HttpMethod::Get,
|
||||||
Target::Rest(target) if target.method == HttpMethod::Get
|
Target::Graphql(target) => target.operation_type == crank_core::GraphqlOperationType::Query,
|
||||||
) {
|
_ => false,
|
||||||
|
};
|
||||||
|
if !is_cacheable_protocol {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,6 +786,8 @@ fn response_cache_key(
|
|||||||
"query_params": prepared_request.query_params,
|
"query_params": prepared_request.query_params,
|
||||||
"headers": prepared_request.headers,
|
"headers": prepared_request.headers,
|
||||||
"body": prepared_request.body,
|
"body": prepared_request.body,
|
||||||
|
"variables": prepared_request.variables,
|
||||||
|
"grpc": prepared_request.grpc,
|
||||||
});
|
});
|
||||||
let fingerprint_bytes = serde_json::to_vec(&fingerprint).ok()?;
|
let fingerprint_bytes = serde_json::to_vec(&fingerprint).ok()?;
|
||||||
let fingerprint_hash = URL_SAFE_NO_PAD.encode(Sha256::digest(fingerprint_bytes));
|
let fingerprint_hash = URL_SAFE_NO_PAD.encode(Sha256::digest(fingerprint_bytes));
|
||||||
@@ -811,6 +815,7 @@ fn cached_response_from_adapter(adapter_response: &AdapterResponse) -> Option<Ca
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
body,
|
body,
|
||||||
|
data: serde_json::to_vec(&adapter_response.data).ok()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,6 +824,8 @@ fn adapter_response_from_cached(
|
|||||||
) -> Result<AdapterResponse, String> {
|
) -> Result<AdapterResponse, String> {
|
||||||
let body: Value =
|
let body: Value =
|
||||||
serde_json::from_slice(&cached_response.body).map_err(|error| error.to_string())?;
|
serde_json::from_slice(&cached_response.body).map_err(|error| error.to_string())?;
|
||||||
|
let data: Value =
|
||||||
|
serde_json::from_slice(&cached_response.data).map_err(|error| error.to_string())?;
|
||||||
Ok(AdapterResponse {
|
Ok(AdapterResponse {
|
||||||
status_code: cached_response.status,
|
status_code: cached_response.status,
|
||||||
headers: cached_response
|
headers: cached_response
|
||||||
@@ -827,7 +834,7 @@ fn adapter_response_from_cached(
|
|||||||
.map(|header| (header.name, header.value))
|
.map(|header| (header.name, header.value))
|
||||||
.collect(),
|
.collect(),
|
||||||
body,
|
body,
|
||||||
data: Value::Null,
|
data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1089,6 +1096,40 @@ mod tests {
|
|||||||
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn caches_graphql_query_responses_within_agent_scope() {
|
||||||
|
let request_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let endpoint = spawn_cached_graphql_server(Arc::clone(&request_count)).await;
|
||||||
|
let executor = RuntimeExecutor::new()
|
||||||
|
.with_response_cache_store(Arc::new(InMemoryResponseCacheStore::default()));
|
||||||
|
let operation = test_cached_graphql_query_operation(&endpoint);
|
||||||
|
let first_context = RuntimeRequestContext::from_request_id("req_cache_graphql_1")
|
||||||
|
.with_response_cache_scope("ws_cache", "agent_sales");
|
||||||
|
let second_context = RuntimeRequestContext::from_request_id("req_cache_graphql_2")
|
||||||
|
.with_response_cache_scope("ws_cache", "agent_sales");
|
||||||
|
|
||||||
|
let first_output = executor
|
||||||
|
.execute_with_context(
|
||||||
|
&operation,
|
||||||
|
&json!({ "email": "user@example.com" }),
|
||||||
|
Some(&first_context),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let second_output = executor
|
||||||
|
.execute_with_context(
|
||||||
|
&operation,
|
||||||
|
&json!({ "email": "user@example.com" }),
|
||||||
|
Some(&second_context),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(first_output, json!({ "id": "lead_123" }));
|
||||||
|
assert_eq!(second_output, json!({ "id": "lead_123" }));
|
||||||
|
assert_eq!(request_count.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn emits_runtime_tracing_with_request_context() {
|
async fn emits_runtime_tracing_with_request_context() {
|
||||||
let base_url = spawn_runtime_server().await;
|
let base_url = spawn_runtime_server().await;
|
||||||
@@ -1607,6 +1648,41 @@ mod tests {
|
|||||||
format!("http://{}", address)
|
format!("http://{}", address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn spawn_cached_graphql_server(request_count: Arc<AtomicUsize>) -> String {
|
||||||
|
let app = Router::new().route(
|
||||||
|
"/",
|
||||||
|
post(move |Json(payload): Json<Value>| {
|
||||||
|
let request_count = Arc::clone(&request_count);
|
||||||
|
async move {
|
||||||
|
let count = request_count.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
let email = payload
|
||||||
|
.get("variables")
|
||||||
|
.and_then(|variables| variables.get("email"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Json(json!({
|
||||||
|
"data": {
|
||||||
|
"lookupLead": {
|
||||||
|
"id": "lead_123",
|
||||||
|
"email": email,
|
||||||
|
"request_count": count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn_graphql_context_server() -> String {
|
async fn spawn_graphql_context_server() -> String {
|
||||||
let app = Router::new().route("/", post(graphql_context_handler));
|
let app = Router::new().route("/", post(graphql_context_handler));
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
@@ -2090,6 +2166,84 @@ mod tests {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_cached_graphql_query_operation(endpoint: &str) -> RuntimeOperation {
|
||||||
|
RuntimeOperation::from(Operation {
|
||||||
|
id: OperationId::new("op_graphql_cached_runtime"),
|
||||||
|
name: "crm_lookup_lead_graphql".to_owned(),
|
||||||
|
display_name: "Lookup Lead GraphQL".to_owned(),
|
||||||
|
category: "sales".to_owned(),
|
||||||
|
protocol: Protocol::Graphql,
|
||||||
|
security_level: OperationSecurityLevel::Standard,
|
||||||
|
status: OperationStatus::Published,
|
||||||
|
version: 1,
|
||||||
|
target: Target::Graphql(GraphqlTarget {
|
||||||
|
endpoint: endpoint.to_owned(),
|
||||||
|
operation_type: GraphqlOperationType::Query,
|
||||||
|
operation_name: "LookupLead".to_owned(),
|
||||||
|
query_template:
|
||||||
|
"query LookupLead($email: String!) { lookupLead(email: $email) { id email request_count } }"
|
||||||
|
.to_owned(),
|
||||||
|
response_path: "$.response.body.data.lookupLead".to_owned(),
|
||||||
|
}),
|
||||||
|
input_schema: object_schema("email", SchemaKind::String),
|
||||||
|
output_schema: object_schema("id", SchemaKind::String),
|
||||||
|
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: Some(crank_core::ResponseCachePolicy { ttl_ms: 5_000 }),
|
||||||
|
auth_profile_ref: None,
|
||||||
|
headers: BTreeMap::new(),
|
||||||
|
protocol_options: None,
|
||||||
|
streaming: None,
|
||||||
|
},
|
||||||
|
tool_description: ToolDescription {
|
||||||
|
title: "Lookup Lead GraphQL".to_owned(),
|
||||||
|
description: "Reads a CRM lead through GraphQL".to_owned(),
|
||||||
|
tags: vec!["crm".to_owned(), "graphql".to_owned()],
|
||||||
|
examples: vec![ToolExample {
|
||||||
|
input: json!({ "email": "user@example.com" }),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
samples: Some(Samples::default()),
|
||||||
|
generated_draft: Some(GeneratedDraft {
|
||||||
|
status: GeneratedDraftStatus::Available,
|
||||||
|
source_types: vec!["input_json".to_owned()],
|
||||||
|
generated_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||||
|
input_schema_generated: true,
|
||||||
|
output_schema_generated: true,
|
||||||
|
input_mapping_generated: true,
|
||||||
|
output_mapping_generated: true,
|
||||||
|
warnings: Vec::new(),
|
||||||
|
}),
|
||||||
|
config_export: None,
|
||||||
|
created_at: timestamp("2026-03-25T20:00:00Z"),
|
||||||
|
updated_at: timestamp("2026-03-25T20:00:00Z"),
|
||||||
|
published_at: Some(timestamp("2026-03-25T20:00:00Z")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn test_grpc_operation(server_addr: &str) -> RuntimeOperation {
|
fn test_grpc_operation(server_addr: &str) -> RuntimeOperation {
|
||||||
RuntimeOperation::from(Operation {
|
RuntimeOperation::from(Operation {
|
||||||
id: OperationId::new("op_grpc_runtime"),
|
id: OperationId::new("op_grpc_runtime"),
|
||||||
|
|||||||
+2
-1
@@ -141,9 +141,10 @@
|
|||||||
- `execution_config.response_cache` допускается только для:
|
- `execution_config.response_cache` допускается только для:
|
||||||
- `REST`
|
- `REST`
|
||||||
- `GET`
|
- `GET`
|
||||||
|
- или `GraphQL query`
|
||||||
- операций без `auth_profile_ref`
|
- операций без `auth_profile_ref`
|
||||||
- `execution_config.response_cache` не означает глобальный shared cache на все вызовы системы:
|
- `execution_config.response_cache` не означает глобальный shared cache на все вызовы системы:
|
||||||
- response cache должен быть изолирован минимум по `workspace + agent + operation + request fingerprint`
|
- response cache должен быть изолирован минимум по `workspace + agent + operation + operation version + request fingerprint`
|
||||||
- попытка создать, обновить или импортировать операцию с неподдерживаемым `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
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
### Цель
|
### Цель
|
||||||
|
|
||||||
Подготовить optional cache/coordination layer так, чтобы платформа могла использовать `Valkey/Redis`,
|
Подготовить optional cache/coordination layer так, чтобы платформа могла использовать `Valkey/Redis`,
|
||||||
но не зависела от него для базового запуска.
|
но не зависела от него для базового запуска, и при этом закрыть первые коммерчески ценные cache hot paths.
|
||||||
|
|
||||||
### DoD
|
### DoD
|
||||||
|
|
||||||
@@ -198,6 +198,10 @@
|
|||||||
- response cache по умолчанию изолировался по `workspace + agent + operation + operation version + request fingerprint`;
|
- response cache по умолчанию изолировался по `workspace + agent + operation + operation version + request fingerprint`;
|
||||||
- внешний cache backend можно было безопасно шарить между несколькими рабочими областями и агентами.
|
- внешний cache backend можно было безопасно шарить между несколькими рабочими областями и агентами.
|
||||||
- shared coordination cache уже применяется не только для rate limiting, но и для multi-instance snapshots published MCP catalogs.
|
- shared coordination cache уже применяется не только для rate limiting, но и для multi-instance snapshots published MCP catalogs.
|
||||||
|
- response cache уже покрывает:
|
||||||
|
- `REST GET` как Community baseline;
|
||||||
|
- `GraphQL query` как первый коммерчески ценный read-only protocol path.
|
||||||
|
- дальнейшее расширение response cache идет только по явно обоснованным read-only сценариям, начиная с `gRPC unary`, а не как общий cache для всех протоколов подряд.
|
||||||
|
|
||||||
## 14. Этап 11. Live staging and demo readiness
|
## 14. Этап 11. Live staging and demo readiness
|
||||||
|
|
||||||
|
|||||||
@@ -237,9 +237,9 @@ Crank должен корректно работать, если MCP client де
|
|||||||
- `POST` и `GET` transport semantics соответствуют MCP spec `2025-06-18`;
|
- `POST` и `GET` transport semantics соответствуют MCP spec `2025-06-18`;
|
||||||
- endpoint определяется парой `workspace + agent`;
|
- endpoint определяется парой `workspace + agent`;
|
||||||
- одна published operation = один MCP tool внутри agent;
|
- одна published operation = один MCP tool внутри agent;
|
||||||
- если операция явно включает `execution_config.response_cache`, первый поддержанный runtime path ограничен:
|
- если операция явно включает `execution_config.response_cache`, текущий поддержанный runtime path ограничен:
|
||||||
- `REST`
|
- `REST GET`
|
||||||
- `GET`
|
- `GraphQL query`
|
||||||
- без `auth_profile_ref`
|
- без `auth_profile_ref`
|
||||||
- cache keys изолируются минимум по `workspace + agent + operation + operation version + request fingerprint`
|
- cache keys изолируются минимум по `workspace + agent + operation + operation version + request fingerprint`
|
||||||
- streaming operations публикуются как bounded tools или tool families;
|
- streaming operations публикуются как bounded tools или tool families;
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ Demo/deployment:
|
|||||||
|
|
||||||
Второй контур хранит только кэшируемые ответы операций.
|
Второй контур хранит только кэшируемые ответы операций.
|
||||||
|
|
||||||
|
Текущий безопасный runtime scope для response cache:
|
||||||
|
|
||||||
|
- `REST GET`;
|
||||||
|
- `GraphQL query`.
|
||||||
|
|
||||||
|
Это не означает автоматическое кэширование всех protocol families. Следующим кандидатом на расширение
|
||||||
|
может быть только явно read-only `gRPC unary`.
|
||||||
|
|
||||||
Эти контуры не должны смешивать ключи друг с другом.
|
Эти контуры не должны смешивать ключи друг с другом.
|
||||||
|
|
||||||
### Cache key isolation
|
### Cache key isolation
|
||||||
|
|||||||
Reference in New Issue
Block a user