- bump alkcall 0.8.1 -> 0.9 (gateway feature unchanged). No code change forced: none of the 0.9.0 breaking surfaces are constructed or pattern-matched in this crate — no ManagerError match arms (the new InvalidAdopt variant changes nothing here), no RelayError matching (the removed PubTypedOpen variant is unreachable from this crate), no Establishment construction or Debug dependence (the WS ferry passes Option<OpenEstablisher> through unchanged, per the 0.8.0 audit), and the op/register forward-through refactor does not touch the from_wss import filter or the adapter-registered leaf bundles - ride-through fixes: tests/ws_overlay_ops.rs now expects register_imported's new Result<(), String> return (8 sites assert registration success instead of discarding the value — the bool return they consumed was ignored anyway); fuzz/shared follows the main crate to alkcall 0.9 so the harness replay resolves one alkcall version (the stale 0.8.1 spec pinned two copies against alkhttp's requirement and broke the shared-crate build) - minor bump 0.6.0 -> 0.7.0 per the dep-wave convention (0.3.0 rode alkcall 0.6.0, 0.4.0 rode 0.7.0, 0.5.0 rode 0.8.0): this crate's public API carries alkcall types through re-exports and the ProtocolHandler impl, so a semver-breaking upstream is semver-relevant here Verification: cargo test 441+70 pass (default), 520+70 pass (all-features), clippy --all-features --all-targets -D warnings clean, fmt --check clean, doc --no-deps clean, fuzz corpus replay 21/21
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 axumRouterdriven by hyper's HTTP/1.1 and HTTP/2 connection handling over aBiStream, 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(ato_openapiprojection of the local registry), and/mcp(the MCP tool gateway, featuremcp).- WebSocket upgrade on
/alk/channelscarrying the channels protocol: 8-byte chunk multiplexing with channel 0 pre-negotiated asalk/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-builtOperationSpec.from_mcp— import a remote MCP server's tools as operations over streamable HTTP (featuremcp).from_wss— import a remote alk node's operations over a WSS channels connection (featurewss).SharedHttpClient— the hot-reloadable reqwest stack allfrom_*forwarding rides: same-host-only redirects, idempotent-only retries with a wall-clock budget,Retry-Aftersupport, 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.5.0", 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; thefrom_*adapters are the injection point, and no handler readsstd::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-Afterceiling, 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
- Architecture docs — the authoritative spec: ADRs 001–071, component documents, and open questions.
- API docs — full crate documentation on docs.rs.
- Changelog
License
MIT OR Apache-2.0 — see LICENSE-MIT and LICENSE-APACHE.