- 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
197 lines
8.3 KiB
Markdown
197 lines
8.3 KiB
Markdown
# 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 001–071, 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). |