glm-5.3-flash 030c5efa51 feat(websocket): data-channel + op/register wiring (review 006 Unit 2+3)
The WS path wires the alkcall 0.3 per-session mechanisms — the OQ-05
deferred half (review-003 WS-20/21/22/25/26, the decisions WS-24/WS-25
resolved upstream):

- install_channel_zero reworked per the ADR-047 §4 amendment #2 shape:
  fork the deployment's base registry, register the generic channel
  ops (channel/close, channel/control, channel/resources/subscribe —
  WS-21), the deployment's openable ALPNs (ChannelCore::register_openable,
  WS-22), the bootstrap discovery set closed over the fork
  (install_bootstrap_discovery, F-06), and op/register (WS-25; the
  collision set is the fork per review-005 G-03), then dispatch over
  the fork. The session's ChannelsPolicy rides the hook (one policy
  instance across open wrappers and the demux teardown path).
- OpenableAlpn { spec, open_handler } + HttpAdapter::with_ws_openable_alpns,
  threaded RouterState -> SessionState -> hook, with the OpenableAlpns
  request-extension fallback (mirroring ChannelsPolicy/WsTimeouts).
- WsSessions retains the channel-0 Arc<CallConnection> (WS-26) with a
  self-removing guard (ConnectionGuard); live_connections() is the
  deployment-visible surface.
- UP-01: ALREADY_EXISTS maps to 409 Conflict in the gateway error map.
- from_wss import excludes the protocol-session ops (bootstrap set +
  channel lifecycle ops): the fork serves them per session, and proxying
  session-scoped machinery (e.g. channel/close across sessions) would be
  nonsense. Discovery runs first, the filter is the listing minus those
  names.
- adapter_install_channel_zero cfg matches its caller (WS-27); it
  inherits the reworked hook (session ops now served in the from_wss
  test-server producer too).

Gates (Unit 3, tests/ws_upgrade_session.rs; the WS-23 e2e set):
open -> channel_id -> discoverable in services/list -> chunks both
ways -> handler sees bytes; channel/close resolves + ledger decrement;
cap denial (channel:-prefixed); mid-open disconnect teardown; TooLarge
demux resync through the WS path (16 MiB + 1 skip consumed);
op/register announce + overlay-collision + serving-registry-collision
ALREADY_EXISTS through the WS path.

call_and_await now filters by request id and tolerates data-channel
chunks (a prior Sub's trailing call.completed may interleave).

Verification: cargo test 454 (default) / 582 (all-features), clippy
both sides -D warnings clean, fmt clean, doc clean.
2026-09-04 15:47:15 +00:00

alkhttp

HTTP interface for the alk stack: serves HTTP/1.1 + HTTP/2 on standard ALPNs (with WebSocket upgrade carrying the channels protocol for browser bidirectional access) and hosts the HTTP-backed call-protocol adapters.

alkhttp wraps alkcall — the pure call + channels protocol crate — and turns its operation registry into an HTTP surface. It is the HTTP server host (a ProtocolHandler for the IANA h2/http/1.1 ALPNs) and the HTTP client host (the import adapters that call out over reqwest) in one crate.

What's inside

Server side (server feature) — the producer-facing HTTP surface:

  • HttpAdapter — an axum Router driven by hyper's HTTP/1.1 and HTTP/2 connection handling over a BiStream, served on standard ALPNs so any HTTP client connects without knowing about the alk stack.
  • Six fixed gateway endpoints — the sole HTTP invoke path (no per-operation REST tree): GET /search, GET /schema?name=…, POST /call, POST /batch, POST /subscribe (SSE), POST /publish (NDJSON).
  • /healthz, /openapi.json (a to_openapi projection of the local registry), and /mcp (the MCP tool gateway, feature mcp).
  • WebSocket upgrade on /alk/channels carrying the channels protocol: 8-byte chunk multiplexing with channel 0 pre-negotiated as alk/call, so a browser runs the native bidirectional call-protocol session.
  • Bearer auth, a stealth decoy surface for unknown paths, custom-route mounting, and assembly hooks for WS session caps and timeouts.

Client side (client feature) — the consumer-facing import adapters, each producing ordinary HandlerRegistration bundles for an alkcall registry:

  • from_openapi — import an external HTTP API described by an OpenAPI document (JSON or YAML), forwarding calls over the shared reqwest client.
  • from_jsonschema — import one non-OpenAPI HTTP endpoint behind a caller-built OperationSpec.
  • from_mcp — import a remote MCP server's tools as operations over streamable HTTP (feature mcp).
  • from_wss — import a remote alk node's operations over a WSS channels connection (feature wss).
  • SharedHttpClient — the hot-reloadable reqwest stack all from_* forwarding rides: same-host-only redirects, idempotent-only retries with a wall-clock budget, Retry-After support, TLS config, and streaming byte caps.

Both sides (openapi feature, implied) — the shared OpenAPI document model plus the reverse projections to_openapi (generate the gateway's OpenAPI document from the registry) and to_mcp (expose local operations as MCP tools).

Quick start

Serve the gateway over HTTP

HttpAdapter implements alkcall's ProtocolHandler; the endpoint / accept loop is the consumer's concern (dial and TLS live downstream). Wire it for the ALPN you serve and drive it with your connection source:

use std::sync::Arc;

use alkcall::core::auth::IdentityProvider;
use alkcall::core::types::Connection;
use alkcall::registry::registration::OperationRegistry;
use alkhttp::server::{DecoyConfig, HttpAdapter};

let registry = Arc::new(OperationRegistry::new());
let provider: Arc<dyn IdentityProvider> = /* your identity provider */;

let adapter = HttpAdapter::h2(provider, registry)
    .with_decoy(DecoyConfig::NotFound);

// adapter.alpn() == b"h2" — register it in your handler registry for
// that ALPN; each accepted Connection is handled by
// ProtocolHandler::handle(&adapter, connection, &auth).

Once wired, HTTP callers get the fixed gateway surface. Discovery is per-caller, filtered by AccessControl:

GET  /search                      → { "request_id": …, "result": "ok",
                                      "output": { "operations": [ … ] } }
GET  /schema?name=fs/readFile     → the operation's input/output schema
POST /call                        ← { "operation": "fs/readFile",
                                      "input": { … } }
POST /batch                       ← [ { "operation": …, "input": … }, … ]
POST /subscribe                   → SSE stream of call-protocol envelopes
POST /publish                     ← NDJSON body of published chunks

/openapi.json serves the same surface as an OpenAPI document, so standard tooling can drive the gateway without knowing the alk stack.

Import an OpenAPI-described API

use std::collections::HashMap;
use std::sync::Arc;

use alkhttp::adapters::{FromOpenAPI, HttpServiceConfig, OpenAPISpec};
use alkhttp::client::{HttpClientConfig, SharedHttpClient};

let doc = r#"{"openapi":"3.0.0","info":{"title":"Widgets","version":"1"},
    "paths":{"/widgets":{"get":{"operationId":"listWidgets",
    "responses":{"200":{"content":{"application/json":{"schema":{}}}}}}}}}"#;

let spec = OpenAPISpec::from_json(doc)?;
let config = HttpServiceConfig {
    namespace: "widgets".to_string(),
    base_url: "https://widgets.example.com".to_string(),
    auth: None, // or Some(HttpAuthScheme::Bearer) — the credential is
                // injected per-call from Capabilities, never from config
    default_headers: HashMap::new(),
};
let http_client = Arc::new(SharedHttpClient::new(HttpClientConfig::default())?);

let adapter = FromOpenAPI::new(spec, config, http_client);
let registrations = alkcall::client::OperationAdapter::import(&adapter).await?;

let mut registry = alkcall::registry::registration::OperationRegistry::new();
for reg in registrations {
    registry.register(reg)?;
}
// "widgets/listWidgets" is now callable — and composable — through the
// call protocol and the gateway.

Imported operations register Visibility::Internal (they are composition material); an External facade op composed over them via ctx.env.invoke is what the wire sees.

WebSocket for browsers

A browser upgrades GET /alk/channels (Bearer token in the Authorization header) to a WebSocket and speaks the channels protocol in binary messages: an 8-byte chunk header multiplexes N channels; channel 0 is pre-negotiated as alk/call and runs the native call-protocol session. Both sides can initiate calls; the browser can also register operations and open channels — it is a full consumer and producer on its own connection-local overlay.

Feature flags

The crate is feature-sided — build one side, or both:

Feature Default Gates
server yes the axum host, gateway routes, WS upgrade, /healthz, /openapi.json, to_openapi
client yes the shared outbound client host, from_openapi, from_jsonschema
openapi (implied) the shared OpenAPI document model (implied by both sides)
mcp no from_mcp (needs client) and to_mcp (needs server)
wss no the from_wss consumer adapter
h2, http1 yes the server's HTTP protocol features (imply server)

A lean single-side build takes default-features = false plus the side it needs:

alkhttp = { version = "0.1", default-features = false, features = ["server"] }

Security posture

  • No secret material on the wire. Request/response payloads and headers carry no keys or tokens beyond the caller's own Bearer credential. Outbound credentials flow vault → assembly layer → Capabilities → handler; the from_* adapters are the injection point, and no handler reads std::env::var.
  • Stealth mode. The HTTP surface serves on standard ALPNs, and unregistered paths answer with a configurable decoy (fake nginx 404, static site, or redirect) instead of advertising the gateway.
  • Tight forwarding defaults. Same-host-only redirects, idempotent-only retries with a wall-clock budget and Retry-After ceiling, 30 s request / 10 s connect / 30 s read timeouts, a 2 MiB gateway body cap (8 MiB on /mcp), and a 1 GiB streamed-bytes cap per subscription.
  • Internal-by-default imports. Adapter-registered operations are invisible to direct wire calls (NOT_FOUND) until composed behind an External facade.

Documentation

License

MIT OR Apache-2.0 — see LICENSE-MIT and LICENSE-APACHE.

S
Description
No description provided
Readme
2.1 MiB
0 Stars 6 Watchers 0 Forks
Languages
Rust 98.6%
Python 1.3%
Shell 0.1%