fix(server): hyper knobs, decoy fidelity, cache + builder fixes (SRV-04..SRV-10)

- SRV-04: TokioTimer on h1+h2 builder, header_read_timeout 10s,
  h1 keep-alive on, h2 keep-alive 30s/10s; concurrency boundary documented
- SRV-05: with_decoy rebuild keeps extra routes (clone, not take)
- SRV-07: method_not_allowed_fallback serves the nginx-shaped 405
- SRV-08: UTF-8 percent-decoding, literal '+', tokio::fs syscalls
- SRV-09: /openapi.json cached at construction, generic 500 body,
  to_openapi returns Result (expect removed)
- SRV-10: ChannelsPolicy extension injection point on the WS upgrade;
  single token resolution via route ordering (WS layer before the
  router-wide auth route_layer)

Verification: cargo test 265 passed; --all-features server::/to_openapi::
green; clippy + fmt clean on touched files (remaining tree noise is a
parallel agent's in-flight from_mcp/from_wss/forward work)
This commit is contained in:
2026-08-29 10:47:44 +00:00
parent 1dc1d5af4f
commit 314472012d
10 changed files with 503 additions and 86 deletions
+30 -21
View File
@@ -29,6 +29,7 @@ use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use alkcall::client::AdapterError;
use alkcall::registry::registration::OperationRegistry;
use alkcall::registry::spec::ErrorDefinition;
@@ -61,10 +62,18 @@ const CODE_TIMEOUT: &str = "TIMEOUT";
const HTTP_PREFIX: &str = "HTTP_";
pub fn to_openapi(registry: &OperationRegistry) -> OpenAPISpec {
/// Project the registry into the fixed 6-endpoint gateway doc (ADR-042).
///
/// Returns [`AdapterError::SchemaParse`] if the generated doc does not
/// re-validate against the structural checks in `OpenAPISpec::from_value`
/// — a would-be invariant violation of `build_doc`, not a caller-facing
/// input error. The HTTP surface serves only a generic `500` for this
/// (`/openapi.json` handler caches the serialized doc; SRV-09): no
/// serde/parse internals reach the wire.
pub fn to_openapi(registry: &OperationRegistry) -> Result<OpenAPISpec, AdapterError> {
let operation_errors = collect_operation_errors(registry);
let raw = build_doc(operation_errors);
OpenAPISpec::from_value(raw).expect("to_openapi always emits a valid OpenAPI document")
OpenAPISpec::from_value(raw)
}
fn build_doc(operation_errors: Vec<ErrorDefinition>) -> Value {
@@ -659,7 +668,7 @@ mod tests {
#[test]
fn empty_registry_produces_six_gateway_paths() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let paths = paths_object(&spec);
assert_eq!(paths.len(), 6);
assert!(paths.contains_key(PATH_SEARCH));
@@ -675,7 +684,7 @@ mod tests {
let mut registry = OperationRegistry::new();
register(&mut registry, external_spec("fs/readFile", vec![]));
register(&mut registry, external_spec("agent/chat", vec![]));
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let paths = paths_object(&spec);
assert_eq!(paths.len(), 6);
assert!(!paths.contains_key("/fs/readFile"));
@@ -685,7 +694,7 @@ mod tests {
#[test]
fn info_version_is_1_1_0_after_publish_addition() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let version = spec
.raw
.get("info")
@@ -702,7 +711,7 @@ mod tests {
#[test]
fn info_title_present() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let title = spec
.raw
.get("info")
@@ -715,7 +724,7 @@ mod tests {
#[test]
fn openapi_field_is_3_0_0() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let openapi = spec.raw.get("openapi").and_then(Value::as_str).unwrap();
assert_eq!(openapi, OPENAPI_VERSION);
}
@@ -723,7 +732,7 @@ mod tests {
#[test]
fn publish_has_post_method_with_ndjson_request_body() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
assert!(path(&spec, PATH_PUBLISH).contains_key("post"));
let request_schema = operation(&spec, PATH_PUBLISH, "post")
.get("requestBody")
@@ -747,7 +756,7 @@ mod tests {
#[test]
fn publish_includes_protocol_error_statuses_including_400() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_PUBLISH, "post");
for status in [
STATUS_BAD_REQUEST,
@@ -768,7 +777,7 @@ mod tests {
#[test]
fn publish_400_response_covers_invalid_input_and_invalid_operation_type() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_PUBLISH, "post");
let schema = responses
.get(&STATUS_BAD_REQUEST.to_string())
@@ -799,7 +808,7 @@ mod tests {
#[test]
fn call_request_body_is_flat_operation_input() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let request_schema = operation(&spec, PATH_CALL, "post")
.get("requestBody")
.and_then(|rb| rb.get("content"))
@@ -832,7 +841,7 @@ mod tests {
#[test]
fn call_includes_all_protocol_level_error_statuses() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
for status in [
STATUS_BAD_REQUEST,
@@ -853,7 +862,7 @@ mod tests {
#[test]
fn call_protocol_error_status_codes_have_protocol_codes() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
let invalid_input_schema = responses
@@ -906,7 +915,7 @@ mod tests {
],
),
);
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
assert!(
responses.contains_key("429"),
@@ -934,7 +943,7 @@ mod tests {
&mut registry,
external_spec("svc/op", vec![error("HTTP_404", Some(404))]),
);
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
let response_404 = responses.get("404").unwrap();
let schema = response_404
@@ -963,7 +972,7 @@ mod tests {
&mut registry,
external_spec("svc/op", vec![error("HTTP_404", Some(404))]),
);
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
let response_404 = responses.get("404").unwrap();
let schema = response_404
@@ -1006,7 +1015,7 @@ mod tests {
&mut registry,
external_spec("svc/op", vec![error("SOME_ERROR", None)]),
);
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
assert!(
responses.len() < 10,
@@ -1025,7 +1034,7 @@ mod tests {
&mut registry,
external_spec("svc/b", vec![error("TOO_MANY_REQUESTS", Some(429))]),
);
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
assert!(responses.contains_key("429"));
let schema = responses
@@ -1065,7 +1074,7 @@ mod tests {
Capabilities::new(),
))
.unwrap();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
assert!(
!responses.contains_key("418"),
@@ -1080,7 +1089,7 @@ mod tests {
&mut registry,
external_spec("svc/op", vec![error("HTTP_500", Some(500))]),
);
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let responses = responses(&spec, PATH_CALL, "post");
let schema = responses
.get("500")
@@ -1098,7 +1107,7 @@ mod tests {
#[test]
fn doc_validates_against_openapiv3_parsing() {
let registry = OperationRegistry::new();
let spec = to_openapi(&registry);
let spec = to_openapi(&registry).unwrap();
let text = serde_json::to_string(&spec.raw).unwrap();
let parsed: openapiv3::OpenAPI =
serde_json::from_str(&text).expect("gateway doc parses as OpenAPI 3.0");