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
+20 -3
View File
@@ -25,8 +25,7 @@ use super::byte_adapter::split_ws_to_bytes;
/// The channels session for an upgraded socket: adapt → `Connection`
/// (identity attached) → `ChannelsAdapter::handle`. `policy` gates
/// data-channel opens (ADR-041); the default surface uses `NoCap`
/// (the deployment's assembly layer can pass a stricter policy).
/// data-channel opens (ADR-041).
pub async fn run_channels_session(
socket: axum::extract::ws::WebSocket,
registry: Arc<OperationRegistry>,
@@ -115,16 +114,34 @@ impl alkcall::core::auth::IdentityProvider for NoopProvider {
}
}
/// Extension wrapper for the channels-policy injection point (SRV-10):
/// a deployment inserts `ChannelsPolicy(Arc<dyn ChannelLifecyclePolicy>)`
/// into the request extensions (a route layer on the WS route) to gate
/// data-channel opens per identity (ADR-041). Without the extension the
/// upgrade defaults to [`NoCap`] — the crate default for the built-in
/// surface (POC/trusted-peer semantics); assembly layers that build
/// their own upgrade route pass a stricter policy directly to
/// [`run_channels_session`].
#[derive(Clone)]
pub struct ChannelsPolicy(pub Arc<dyn ChannelLifecyclePolicy>);
/// The upgrade handler. Requires the resolved identity in request
/// extensions (stashed by [`ws_bearer_auth`]) — a WS session without
/// an identity cannot run `AccessControl::check`.
///
/// The channel lifecycle policy comes from the
/// [`ChannelsPolicy`] request extension when present, else `NoCap`.
pub async fn ws_upgrade_handler(
axum::extract::State(registry): axum::extract::State<Arc<OperationRegistry>>,
axum::Extension(identity): axum::Extension<Identity>,
policy: Option<axum::Extension<ChannelsPolicy>>,
ws_upgrade: WebSocketUpgrade,
) -> Response {
let policy = policy
.map(|axum::Extension(p)| p.0)
.unwrap_or_else(|| Arc::new(NoCap));
ws_upgrade.on_upgrade(move |socket| async move {
run_channels_session(socket, registry, identity, Arc::new(NoCap)).await
run_channels_session(socket, registry, identity, policy).await
})
}