Files
alkhttp/README.md
glm-5.3-flash 911f186629 chore: release alkhttp 0.5.0
- version bump 0.4.1 -> 0.5.0 (minor per the dep-wave convention:
  0.3.0 rode alkcall 0.6.0, 0.4.0 rode alkcall 0.7.0)
- README usage snippet 0.4.1 -> 0.5.0
- changelog: cut [0.5.0] with the alkcall 0.8.0 consumption under
  Changed — the additive-only audit (reply fields, relay/hub-leg,
  flavor-form discovery) and the one ride-through discovery change
  (explicit channel_open_alpn for non-standard open-op names)

Verification: cargo test/clippy/fmt/doc green in the preceding deps
commit; no source change in this commit
2026-09-18 13:48:26 +00:00

197 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](https://crates.io/crates/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:
```rust
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`:
```text
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
```rust
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:
```toml
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; 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
- [Architecture docs](docs/architecture/README.md) — the authoritative
spec: ADRs 001071, component documents, and open questions.
- [API docs](https://docs.rs/alkhttp) — full crate documentation on
docs.rs.
- [Changelog](CHANGELOG.md)
## License
MIT OR Apache-2.0 — see [LICENSE-MIT](LICENSE-MIT) and
[LICENSE-APACHE](LICENSE-APACHE).