docs(arch): ADR-066 — move from_jsonschema to alknet-http as HTTP-backed single-endpoint adapter
from_jsonschema was in alknet-call as a schema-only placeholder with a NOT_FOUND handler — broken (an op in the registry needs a real handler) and in the wrong crate (alknet-call has no HTTP client; a useful from_jsonschema needs reqwest like from_openapi). ADR-066 moves it to alknet-http as a real reqwest-backed single-endpoint adapter for non-standard/non-OpenAPI REST endpoints, functionally similar to from_openapi but one endpoint at a time. FromJsonSchema provenance stays in alknet-call (now a handler-bearing leaf). - New ADR-066 (supersedes ADR-017 §5 from_jsonschema clause + ADR-022 FromJsonSchema row; both amended with strikethrough + pointer) - Updated specs: call/client-and-adapters, call/README, call/operation-registry, http/http-adapters (new from_jsonschema section), http/overview, http/README, arch README + overview - New task: tasks/http/adapters/from-jsonschema.md (depends on from-openapi; includes alknet-call cleanup of the broken placeholder) - Old task tasks/call/client/from-jsonschema.md marked superseded taskgraph validate: 116 tasks valid
This commit is contained in:
1 parent
06ab5db459
commit
97456bc608
13 files changed
+739
-95
No files matched your search
@@ -45,6 +45,21 @@ use `from_stream` with `tokio::io::sink`/`empty`). See
|
||||
[`docs/research/transport-generalization/findings.md`](../research/transport-generalization/findings.md)
|
||||
for the full trace.
|
||||
|
||||
**`from_jsonschema` relocation (ADR-066).** The `from_jsonschema`
|
||||
adapter was originally placed in `alknet-call` (ADR-017 §5) as a
|
||||
schema-only adapter with a `NOT_FOUND`-returning placeholder handler —
|
||||
broken, because an op in the registry needs a real handler.
|
||||
[ADR-066](decisions/066-from-jsonschema-as-http-adapter.md) moves it to
|
||||
`alknet-http` as a real reqwest-backed single-endpoint adapter
|
||||
(functionally similar to `from_openapi`, but one endpoint at a time),
|
||||
for non-standard / non-OpenAPI / basic REST endpoints that don't have a
|
||||
full OpenAPI document. The `FromJsonSchema` provenance variant stays in
|
||||
`alknet-call` (now a handler-bearing leaf, not a "no handler" entry).
|
||||
The "schema-only, no handler" concept is removed — schema validation
|
||||
without a handler is served by consuming `OperationSpec` directly. The
|
||||
adapter location map is now consistent: all HTTP-backed adapters
|
||||
(`from_openapi`, `from_mcp`, `from_jsonschema`) live in `alknet-http`.
|
||||
|
||||
## Architecture Documents
|
||||
|
||||
| Document | Status | Description |
|
||||
@@ -59,12 +74,12 @@ for the full trace.
|
||||
| [crates/call/README.md](crates/call/README.md) | draft | alknet-call crate index |
|
||||
| [crates/call/call-protocol.md](crates/call/call-protocol.md) | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-064), stream model, PendingRequestMap, bidirectional calls, streaming subscribe example |
|
||||
| [crates/call/operation-registry.md](crates/call/operation-registry.md) | draft | OperationSpec, Handler, OperationRegistry, AccessControl, capability injection, service discovery (hand-rolled, no irpc) |
|
||||
| [crates/call/client-and-adapters.md](crates/call/client-and-adapters.md) | draft | CallClient (outbound connection opener), from_call / from_jsonschema, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern |
|
||||
| [crates/call/client-and-adapters.md](crates/call/client-and-adapters.md) | draft | CallClient (outbound connection opener), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
|
||||
| [crates/http/README.md](crates/http/README.md) | draft | alknet-http crate index |
|
||||
| [crates/http/overview.md](crates/http/overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map |
|
||||
| [crates/http/http-server.md](crates/http/http-server.md) | draft | HttpAdapter for h2/http1.1 + WebSocket upgrade route, axum over QUIC, Bearer auth, stealth, /healthz |
|
||||
| [crates/http/websocket.md](crates/http/websocket.md) | draft | WebSocket browser bidirectional path — native `EventEnvelope` call-protocol session (not the gateway shape); framing, dispatch, bidirectionality, connection-local overlay, browsers-are-not-peers, deferred `from_wss` |
|
||||
| [crates/http/http-adapters.md](crates/http/http-adapters.md) | draft | from_openapi (reqwest; JSON + YAML input per ADR-051) and to_openapi (projection); no-env-vars injection point |
|
||||
| [crates/http/http-adapters.md](crates/http/http-adapters.md) | draft | from_openapi (reqwest; JSON + YAML input per ADR-051), from_jsonschema (single-endpoint reqwest forwarding handler per ADR-066), and to_openapi (projection); no-env-vars injection point |
|
||||
| [crates/http/http-mcp.md](crates/http/http-mcp.md) | draft | from_mcp / to_mcp (feature-gated), streamable-HTTP-only, stdio exclusion |
|
||||
| [crates/http/webtransport.md](crates/http/webtransport.md) | deferred | h3/WebTransport handler — deferred per ADR-044; browser bidirectional path uses WebSocket (see http-server.md). Spec kept intact for revival. |
|
||||
| [crates/tty/README.md](crates/tty/README.md) | draft | alknet-tty crate index |
|
||||
@@ -103,12 +118,12 @@ for the full trace.
|
||||
| [014](decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Accepted |
|
||||
| [015](decisions/015-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | Accepted |
|
||||
| [016](decisions/016-abort-cascade-for-nested-calls.md) | Abort Cascade for Nested Calls | Accepted |
|
||||
| [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | Accepted |
|
||||
| [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | Accepted (`from_jsonschema` clause superseded by ADR-066) |
|
||||
| [018](decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Accepted |
|
||||
| [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | Accepted |
|
||||
| [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | Accepted |
|
||||
| [021](decisions/021-key-rotation-via-version-indexed-paths.md) | Key Rotation via Version-Indexed Paths | Accepted |
|
||||
| [022](decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Accepted |
|
||||
| [022](decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Accepted (`FromJsonSchema` row superseded by ADR-066) |
|
||||
| [023](decisions/023-operation-error-schemas.md) | Operation Error Schemas | Accepted |
|
||||
| [024](decisions/024-operation-registry-layering.md) | Operation Registry Layering | Accepted |
|
||||
| [025](decisions/025-vault-local-only-dispatch.md) | Vault Local-Only Dispatch | Accepted |
|
||||
@@ -152,6 +167,7 @@ for the full trace.
|
||||
| [063](decisions/063-exit-code-on-terminal-call-responded.md) | Exit Code on a Terminal `call.responded` for Non-Interactive Exec | Accepted |
|
||||
| [064](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | irpc Was Never Integrated — Hand-Rolled EventEnvelope Framing | Accepted (supersedes ADR-005) |
|
||||
| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` — Generic Single-Stream Connections | Accepted |
|
||||
| [066](decisions/066-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter in alknet-http | Accepted (supersedes the `from_jsonschema` clause of ADR-017 §5 and the `FromJsonSchema` provenance row of ADR-022) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
|
||||
|----------|--------|-------------|
|
||||
| [call-protocol.md](call-protocol.md) | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-064), stream model, PendingRequestMap, bidirectional calls |
|
||||
| [operation-registry.md](operation-registry.md) | draft | OperationSpec, Handler, OperationRegistry, AccessControl, service discovery, hand-rolled framing (no irpc — ADR-064) |
|
||||
| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (outbound connection opener), from_call / from_jsonschema, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern |
|
||||
| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (outbound connection opener), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
|
||||
|
||||
## Applicable ADRs
|
||||
|
||||
@@ -36,7 +36,8 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
|
||||
| [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Call protocol carries no secret material; capabilities injected at assembly layer |
|
||||
| [015](../../decisions/015-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env |
|
||||
| [016](../../decisions/016-abort-cascade-for-nested-calls.md) | Abort Cascade for Nested Calls | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in |
|
||||
| [017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction |
|
||||
| [017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction. ~~`from_jsonschema` clause superseded by ADR-066~~ |
|
||||
| [066](../../decisions/066-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter in alknet-http | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf |
|
||||
| [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Registration bundle carries provenance, composition authority, scoped env, capabilities |
|
||||
| [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity |
|
||||
| [024](../../decisions/024-operation-registry-layering.md) | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) |
|
||||
@@ -79,8 +80,8 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
|
||||
7. **No secret material on the wire**: The call protocol carries no private keys, API keys, mnemonics, or decrypted credentials. Handlers receive outbound credentials through `OperationContext.capabilities`, injected at the assembly layer. See ADR-014.
|
||||
8. **Abort cascades to descendants**: `call.aborted` for a parent request cascades to all non-terminal descendants. Default `abort-dependents`; `continue-running` opt-in. See ADR-016.
|
||||
9. **Internal calls switch authority context, not skip ACL**: The `internal` flag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-015, ADR-022.
|
||||
10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-022.
|
||||
10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`, `FromJsonSchema`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-022. (`FromJsonSchema` is now a real HTTP-forwarding leaf per ADR-066, not a schema-only placeholder.)
|
||||
11. **Connection direction is independent of call direction**: Who opens the QUIC connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` opens them; both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md).
|
||||
12. **Peer authorization via `AccessControl`**: A remote peer's call is authorized by `AccessControl::check(peer_identity)` against the op's `AccessControl` — the same mechanism that gates every other call. No `remote_safe` flag, no `trusted_peer` bypass. An op with `AccessControl::default()` is callable by any peer; an op with `required_scopes` is callable only by peers whose `Identity.scopes` satisfy them; an op with `Visibility::Internal` is never callable from the wire. See ADR-029.
|
||||
13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call`/`from_jsonschema` are in `alknet-call` (QUIC / pure parse); `from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. See [client-and-adapters.md](client-and-adapters.md).
|
||||
13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call` is in `alknet-call` (QUIC); `from_jsonschema`/`from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. (`from_jsonschema` was originally in `alknet-call` as a schema-only placeholder; ADR-066 moved it to `alknet-http` as a real HTTP-backed adapter.) See [client-and-adapters.md](client-and-adapters.md).
|
||||
14. **No handler reads outbound credentials from any source other than `OperationContext.capabilities`** (no-env-vars invariant): the credential injection path is vault → assembly layer → `Capabilities` → `HandlerRegistration.capabilities` → `OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. See ADR-014, [client-and-adapters.md](client-and-adapters.md).
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-02
|
||||
last_updated: 2026-07-09
|
||||
---
|
||||
|
||||
# alknet-call — Client and Adapters
|
||||
@@ -9,14 +9,16 @@ The outbound half of the call protocol: opening connections, importing remote
|
||||
operations, and the adapter contract that ties import-style adapters together.
|
||||
This document covers what ADR-017 specced but the server-side implementation
|
||||
(`call-protocol.md`, `operation-registry.md`) did not include — the `CallClient`
|
||||
that *opens* a connection, the `from_call`/`from_jsonschema` adapters, and the
|
||||
`OperationAdapter` trait. The server-side `CallAdapter` and `CallConnection`
|
||||
that *opens* a connection, the `from_call` adapter, and the
|
||||
`OperationAdapter` trait. (`from_jsonschema` was originally specced here
|
||||
too, but ADR-066 moved it to `alknet-http` — see §"from_jsonschema" below.)
|
||||
The server-side `CallAdapter` and `CallConnection`
|
||||
dispatch loop are covered in `call-protocol.md`; this document covers the
|
||||
client-side connection-establishment half and the adapter surface.
|
||||
|
||||
## What
|
||||
|
||||
This document specifies four components, all in `alknet-call`:
|
||||
This document specifies three components, all in `alknet-call`:
|
||||
|
||||
1. **`CallClient`** — opens an outbound `alknet/call` QUIC connection and
|
||||
produces a `CallConnection`. The dispatch loop is shared with the
|
||||
@@ -27,12 +29,19 @@ This document specifies four components, all in `alknet-call`:
|
||||
via `services/list` + `services/schema` (already implemented in
|
||||
`registry/discovery.rs`) and registers them in the connection's Layer 2
|
||||
overlay as `FromCall`-provenance leaves with forwarding handlers.
|
||||
3. **`from_jsonschema`** — schema-only registration: produces
|
||||
`HandlerRegistration` bundles with no handler, for validation, discovery,
|
||||
and composition-graph construction without a runtime.
|
||||
4. **`OperationAdapter` trait** — the async trait that `from_call`,
|
||||
3. **`OperationAdapter` trait** — the async trait that `from_call`,
|
||||
`from_openapi`, `from_mcp`, and `from_jsonschema` all implement.
|
||||
|
||||
> **`from_jsonschema` moved.** ADR-066 moved `from_jsonschema` from
|
||||
> `alknet-call` to `alknet-http` and gave it a real reqwest-backed
|
||||
> forwarding handler (it was a broken schema-only placeholder before).
|
||||
> It is now an HTTP-backed single-endpoint adapter for non-standard /
|
||||
> non-OpenAPI / basic REST endpoints, functionally similar to
|
||||
> `from_openapi` but one endpoint at a time. See
|
||||
> [`crates/http/http-adapters.md`](../http/http-adapters.md) §"from_jsonschema".
|
||||
> The `FromJsonSchema` provenance variant stays in `alknet-call`
|
||||
> (`OperationProvenance`); only the adapter implementation moved.
|
||||
|
||||
It also records two cross-cutting architectural mechanisms that the adapter
|
||||
surface rests on:
|
||||
|
||||
@@ -58,8 +67,8 @@ trait is the enabling gap for `alknet-http`'s `from_openapi`/`from_mcp`.
|
||||
|
||||
ADR-017 specced this surface. This document is the spec that operationally
|
||||
fills the gap ADR-017 left to implementation: the `CallClient` API, the
|
||||
`from_call`/`from_jsonschema` flows, the trait signature, the adapter
|
||||
location, the credential invariant, and the bilateral pattern. The gap
|
||||
`from_call` flow, the trait signature, the adapter location, the credential
|
||||
invariant, and the bilateral pattern. The gap
|
||||
analysis (`docs/research/alknet-call-completion/gap-analysis.md`) identified
|
||||
four decisions (DC-1..4) needed before implementation. DC-1 was initially
|
||||
resolved by ADR-028 (`remote_safe`/`trusted_peer`), but a subsequent research
|
||||
@@ -377,29 +386,31 @@ want to disclose the originator. See [ADR-032](../../decisions/032-forwarded-for
|
||||
|
||||
### from_jsonschema
|
||||
|
||||
Schema-only registration: produces `HandlerRegistration` bundles with no
|
||||
handler (`FromJsonSchema` provenance). Used for validation, discovery, and
|
||||
composition-graph construction without a runtime — type-checking a composition
|
||||
plan without executing it, building a UI of available operations without
|
||||
standing up the transports, etc.
|
||||
`from_jsonschema` was originally specified here (ADR-017 §5) as a
|
||||
schema-only adapter in `alknet-call` — a placeholder handler returning
|
||||
`NOT_FOUND`. That was broken: an op in the registry needs a real handler,
|
||||
and the "schema-only, no handler" concept conflated schema validation
|
||||
(a planning activity that doesn't need a registry entry) with operation
|
||||
registration (which always needs a handler).
|
||||
|
||||
```rust
|
||||
pub fn from_jsonschema(
|
||||
spec: OperationSpec,
|
||||
schema: serde_json::Value,
|
||||
) -> HandlerRegistration;
|
||||
```
|
||||
[ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) moved
|
||||
`from_jsonschema` to `alknet-http` as an HTTP-backed single-endpoint
|
||||
adapter: the caller supplies an `OperationSpec` + `HttpServiceConfig` +
|
||||
path template + method, and the adapter builds one
|
||||
`HandlerRegistration` with a real reqwest forwarding handler and
|
||||
`FromJsonSchema` provenance. It is functionally similar to `from_openapi`
|
||||
but one endpoint at a time, for non-standard / non-OpenAPI / basic REST
|
||||
endpoints that don't have a full OpenAPI document. See
|
||||
[`crates/http/http-adapters.md`](../http/http-adapters.md) §"from_jsonschema".
|
||||
|
||||
Distinct from `from_call` (gap analysis DC-5, confirmed not a decision):
|
||||
The schema-validation-without-a-handler use case (the original stated
|
||||
purpose) is served by consuming `OperationSpec` directly — the spec
|
||||
already carries the input/output JSON Schemas. No adapter, no registry
|
||||
entry, no handler is needed for that.
|
||||
|
||||
| | `from_jsonschema` | `from_call` |
|
||||
|---|---|---|
|
||||
| Schema source | Provided directly (caller fetches, passes in) | Discovered over wire (`services/list` + `services/schema`) |
|
||||
| Handler at call time | None (schema-only, `FromJsonSchema` provenance) | Forwards over QUIC (`FromCall` provenance, leaf) |
|
||||
| Use case | Type validation, discovery, composition graph construction | Actually invoking remote operations |
|
||||
|
||||
Keeping them separate preserves the "schema-only, no execution" use case
|
||||
(type checking, safe composition planning without runtime).
|
||||
The `FromJsonSchema` provenance variant stays in `alknet-call`
|
||||
(`OperationProvenance` in `registry/registration.rs`); only the adapter
|
||||
implementation moved.
|
||||
|
||||
### OperationAdapter trait
|
||||
|
||||
@@ -433,8 +444,9 @@ door, recorded here.
|
||||
|
||||
Implementations:
|
||||
- `FromCall` — QUIC-backed (in `alknet-call`).
|
||||
- `FromJsonSchema` — pure parse, no transport (in `alknet-call`).
|
||||
- `FromOpenAPI` — HTTP-backed (in `alknet-http`).
|
||||
- `FromJsonSchema` — HTTP-backed, single-endpoint (in `alknet-http` per
|
||||
ADR-066; was a broken schema-only placeholder in `alknet-call`).
|
||||
- `FromMCP` — MCP streamable-HTTP-backed (in `alknet-http`, feature-gated).
|
||||
|
||||
The `to_*` adapters (`to_openapi`, `to_mcp`) are outbound projections, not
|
||||
@@ -451,12 +463,12 @@ dependencies live.**
|
||||
alknet-call (lean — no HTTP client, no HTTP server)
|
||||
├── OperationAdapter trait (the contract — async, per ADR-017 §5)
|
||||
├── from_call (QUIC — discovers remote ops via call protocol)
|
||||
├── from_jsonschema (pure parse — caller fetches the doc, passes it in)
|
||||
└── CallClient (outbound connection opener — the #1 gap)
|
||||
|
||||
alknet-http (owns HTTP server + HTTP client — separate crate, separate Phase 0)
|
||||
├── ProtocolHandler for h2/http1.1/h3 (axum server — inbound HTTP)
|
||||
├── from_openapi (parse OpenAPI doc + reqwest forwarding handler)
|
||||
├── from_jsonschema (single-endpoint reqwest forwarding handler — ADR-066)
|
||||
├── to_openapi (generate OpenAPI doc from local registry)
|
||||
├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest)
|
||||
└── to_mcp (feature-gated) (expose local ops as MCP tools over streamable HTTP — axum)
|
||||
@@ -591,12 +603,10 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
already-implemented `services/list` + `services/schema` discovery API.
|
||||
|
||||
3. **`OperationAdapter` trait** (enabling) — the async trait. Small,
|
||||
standalone, unblocks `alknet-http` Phase 1.
|
||||
standalone, unblocks `alknet-http` Phase 1 (including `from_jsonschema`
|
||||
per ADR-066).
|
||||
|
||||
4. **`from_jsonschema`** (medium, standalone) — schema-only registration, no
|
||||
handler. Small.
|
||||
|
||||
5. **DC-1 resolution** (peer-graph routing model, ADR-029) — the
|
||||
4. **DC-1 resolution** (peer-graph routing model, ADR-029) — the
|
||||
peer-keyed overlay + `AccessControl`-based peer authorization model that
|
||||
replaces ADR-028's `remote_safe`/`trusted_peer`. This is a structural
|
||||
change to `CompositeOperationEnv` (→ `PeerCompositeEnv`), the dispatch
|
||||
@@ -617,10 +627,12 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
|
||||
## Constraints
|
||||
|
||||
- **No HTTP in alknet-call.** `from_openapi`/`from_mcp`/`to_openapi`/`to_mcp`
|
||||
live in `alknet-http`. The `OperationAdapter` trait and the QUIC-backed
|
||||
adapters (`from_call`, `from_jsonschema`) live in `alknet-call`. See
|
||||
Adapter Location Map.
|
||||
- **No HTTP in alknet-call.** `from_openapi`/`from_mcp`/`from_jsonschema`/
|
||||
`to_openapi`/`to_mcp` live in `alknet-http`. The `OperationAdapter`
|
||||
trait and the QUIC-backed adapter (`from_call`) live in `alknet-call`.
|
||||
`from_jsonschema` was originally (mis)placed in `alknet-call` as a
|
||||
schema-only placeholder; ADR-066 moved it to `alknet-http` as a real
|
||||
HTTP-backed adapter. See Adapter Location Map.
|
||||
- **No secret material on the wire.** `CallCredentials` carries vault-derived
|
||||
material for the *outbound* connection (TLS identity, auth token); the
|
||||
call protocol's wire format carries no private keys, API keys, or decrypted
|
||||
@@ -669,7 +681,8 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction; trait is async; adapters produce `HandlerRegistration` bundles |
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction; trait is async; adapters produce `HandlerRegistration` bundles. ~~`from_jsonschema` clause superseded by ADR-066~~ |
|
||||
| `from_jsonschema` as HTTP-backed single-endpoint adapter in alknet-http | [ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf |
|
||||
| Peer-graph routing model (DC-1, supersedes ADR-028) | [ADR-029](../../decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via existing `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` |
|
||||
| PeerEntry and Identity.id decoupling | [ADR-030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | `PeerId` source changes from UUID to `Identity.id` (= `PeerEntry.peer_id`, stable across key rotation); `Identity.id` decoupled from crypto material on the fingerprint path |
|
||||
| Forwarded-for identity | [ADR-032](../../decisions/032-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; the `from_call` handler populates it; metadata only, never used by `AccessControl::check` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-05
|
||||
last_updated: 2026-07-09
|
||||
---
|
||||
|
||||
# Operation Registry
|
||||
@@ -74,7 +74,7 @@ Operation names use slash-based paths without a leading slash, aligned with URL
|
||||
|
||||
The `namespace` field is derived from the name: for `fs/readFile` it's `fs`, for `agent/chat` it's `agent`. It's a convenience accessor for ACL matching and service grouping.
|
||||
|
||||
Visibility (ADR-015) controls whether an operation is callable from the wire. `External` operations are wire-facing — they appear in `services/list` and accept `call.requested` from clients. `Internal` operations are composition-only — they return `NOT_FOUND` (not `FORBIDDEN`) when called from the wire, and do not appear in `services/list`. The assembly layer declares visibility at registration. All import adapters (`from_openapi`, `from_mcp`, `from_jsonschema`, `from_call`) register operations as `Internal` by default (they're composition material, not directly callable); the handler that composes them is `External`.
|
||||
Visibility (ADR-015) controls whether an operation is callable from the wire. `External` operations are wire-facing — they appear in `services/list` and accept `call.requested` from clients. `Internal` operations are composition-only — they return `NOT_FOUND` (not `FORBIDDEN`) when called from the wire, and do not appear in `services/list`. The assembly layer declares visibility at registration. All import adapters (`from_openapi`, `from_mcp`, `from_jsonschema`, `from_call`) register operations as `Internal` by default (they're composition material, not directly callable); the handler that composes them is `External`. (`from_jsonschema` is now a real HTTP-backed adapter in `alknet-http` per ADR-066, not the schema-only placeholder it was.)
|
||||
|
||||
### AccessControl
|
||||
|
||||
@@ -383,7 +383,7 @@ pub enum OperationProvenance {
|
||||
FromOpenAPI, // HTTP forwarding stub (from_openapi), leaf
|
||||
FromMCP, // MCP forwarding stub (from_mcp), leaf
|
||||
FromCall, // QUIC forwarding stub (from_call), leaf locally
|
||||
FromJsonSchema, // JSON Schema definition, no handler — schema only
|
||||
FromJsonSchema, // HTTP forwarding stub (from_jsonschema, single endpoint), leaf
|
||||
Session, // Agent-written, sandboxed, can compose within sandbox
|
||||
}
|
||||
```
|
||||
@@ -394,9 +394,19 @@ pub enum OperationProvenance {
|
||||
| `FromOpenAPI` | No (leaf) | No | Internal |
|
||||
| `FromMCP` | No (leaf) | No | Internal |
|
||||
| `FromCall` | No (leaf in local registry) | No | Internal |
|
||||
| `FromJsonSchema` | N/A (no handler) | No | N/A |
|
||||
| `FromJsonSchema` | No (leaf) | No | Internal |
|
||||
| `Session` | Yes (within sandbox) | Yes — scopes set at sandbox creation | Internal always |
|
||||
|
||||
> **ADR-066 update.** `FromJsonSchema` was originally a schema-only
|
||||
> provenance with no handler (the old row read "N/A (no handler) /
|
||||
> N/A"). ADR-066 moved `from_jsonschema` to `alknet-http` as a real
|
||||
> HTTP-backed single-endpoint adapter with a reqwest forwarding
|
||||
> handler. `FromJsonSchema` is now a leaf, same trust model as
|
||||
> `FromOpenAPI` (HTTP endpoint trusted; handler is a forwarding stub).
|
||||
> The "schema-only, no handler" concept is removed — schema validation
|
||||
> without a handler is served by consuming `OperationSpec` directly,
|
||||
> not by registering a placeholder op.
|
||||
|
||||
#### CompositionAuthority
|
||||
|
||||
The declared authority (label + scopes + resources) the handler operates
|
||||
@@ -906,7 +916,8 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe
|
||||
| Handler registration, provenance, and composition authority | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle |
|
||||
| Operation registry layering | [ADR-024](../../decisions/024-operation-registry-layering.md) | Curated (static, immutable) + session and connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) |
|
||||
| Operation error schemas | [ADR-023](../../decisions/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity for `from_openapi`/`to_openapi` |
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `from_call`/`from_jsonschema`/`OperationAdapter` produce `HandlerRegistration` bundles; adapter-registered ops are `Internal` leaves. Surface specced in [client-and-adapters.md](client-and-adapters.md) |
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `from_call`/`OperationAdapter` produce `HandlerRegistration` bundles; adapter-registered ops are `Internal` leaves. Surface specced in [client-and-adapters.md](client-and-adapters.md). ~~`from_jsonschema` clause superseded by ADR-066~~ |
|
||||
| `from_jsonschema` as HTTP-backed single-endpoint adapter | [ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf (now handler-bearing, not "no handler") |
|
||||
| Peer-graph routing model (supersedes ADR-028) | [ADR-029](../../decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` (the field this doc's `HandlerRegistration` previously gained) |
|
||||
| Forwarded-for identity | [ADR-032](../../decisions/032-forwarded-for-identity.md) | `forwarded_for` field on `OperationContext` and `call.requested`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it |
|
||||
| ~~Peer-scoped registry filtering~~ (superseded) | ~~[ADR-028](../../decisions/028-callclient-peer-scoped-registry-filtering.md)~~ | ~~`remote_safe` marking on `HandlerRegistration`~~ — superseded by ADR-029 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-06
|
||||
last_updated: 2026-07-09
|
||||
---
|
||||
|
||||
# alknet-http
|
||||
@@ -19,7 +19,7 @@ protocol), and hosts the HTTP-backed call-protocol adapters
|
||||
| [overview.md](overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map |
|
||||
| [http-server.md](http-server.md) | draft | `HttpAdapter` (`ProtocolHandler` for `h2`/`http/1.1` + WS upgrade route), axum over QUIC, Bearer auth, stealth, `/healthz`; WS hands off to the native session spec |
|
||||
| [websocket.md](websocket.md) | draft | WebSocket browser bidirectional path — native `EventEnvelope` call-protocol session (not the gateway shape, ADR-048); framing, dispatch, bidirectionality, connection-local Layer 2 overlay, browsers-are-not-peers rationale, streaming (native `call.responded`, no SSE), deferred `from_wss` adapter |
|
||||
| [http-adapters.md](http-adapters.md) | draft | `from_openapi` (reqwest client; JSON + YAML input per ADR-051) and `to_openapi` (OpenAPI projection); no-env-vars invariant point |
|
||||
| [http-adapters.md](http-adapters.md) | draft | `from_openapi` (reqwest client; JSON + YAML input per ADR-051), `from_jsonschema` (single-endpoint reqwest forwarding handler per ADR-066), and `to_openapi` (OpenAPI projection); no-env-vars invariant point |
|
||||
| [http-mcp.md](http-mcp.md) | draft | `from_mcp` / `to_mcp` (feature-gated), streamable-HTTP-only, stdio exclusion |
|
||||
| [webtransport.md](webtransport.md) | deferred | `h3`/WebTransport handler — **deferred per ADR-044**; spec kept intact for revival |
|
||||
|
||||
@@ -36,8 +36,8 @@ protocol), and hosts the HTTP-backed call-protocol adapters
|
||||
| [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow | `from_openapi`/`from_mcp` are the credential injection point |
|
||||
| [015](../../decisions/015-privilege-model-and-authority-context.md) | Privilege Model | Adapter-registered ops are `Internal` by default |
|
||||
| [017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `OperationAdapter` trait; `to_*` are projections; published-spec contract |
|
||||
| [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, Composition Authority | `from_openapi`/`from_mcp` produce leaf bundles |
|
||||
| [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | `from_openapi`/`to_openapi` error fidelity; `HTTP_<status>` error codes |
|
||||
| [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, Composition Authority | `from_openapi`/`from_mcp`/`from_jsonschema` produce leaf bundles (`FromJsonSchema` now handler-bearing per ADR-066) |
|
||||
| [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | `from_openapi`/`from_jsonschema`/`to_openapi` error fidelity; `HTTP_<status>` error codes |
|
||||
| [027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign | Browsers require X.509; applies to WebTransport (deferred) and any browser-facing TLS |
|
||||
| [034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and Three Peer Roles | Browsers are not alknet peers (§4 amended by ADR-044 §5 with the addressability rationale) |
|
||||
| [036](../../decisions/036-http-to-call-operation-mapping.md) | HTTP-to-Call Operation Mapping | ~~Direct path mapping~~ — **routing superseded by ADR-047**; non-routing clauses survive (SSE projection, Bearer auth, `/healthz`, stealth, error mapping) |
|
||||
@@ -55,6 +55,7 @@ protocol), and hosts the HTTP-backed call-protocol adapters
|
||||
| [048](../../decisions/048-websocket-native-session-not-gateway.md) | WebSocket Carries the Native Call-Protocol Session, Not the Gateway Shape | WS is the native `EventEnvelope` session; the gateway endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are HTTP-only and do not appear on WS; discovery via `services/list`/`services/schema` as call-protocol ops |
|
||||
| [049](../../decisions/049-streaming-handler-for-subscriptions.md) | Streaming Handler for Subscription Operations | `from_openapi` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE → `BoxStream<ResponseEnvelope>` |
|
||||
| [051](../../decisions/051-yaml-input-for-from-openapi.md) | YAML Input Format for from_openapi | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (defensive default, §2 amended — `yaml_serde` 0.10.x is YAML 1.2, not 1.1); YAML dep is `yaml_serde` (maintained fork of deprecated `serde_yaml`); `to_openapi` output stays JSON (out of scope, §4) |
|
||||
| [066](../../decisions/066-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter in alknet-http | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a handler-bearing leaf; supersedes ADR-017 §5's `from_jsonschema` clause and ADR-022's `FromJsonSchema` row |
|
||||
|
||||
## Relevant Open Questions
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-06
|
||||
last_updated: 2026-07-09
|
||||
---
|
||||
|
||||
# HTTP Adapters — from_openapi and to_openapi
|
||||
# HTTP Adapters — from_openapi, from_jsonschema, and to_openapi
|
||||
|
||||
The OpenAPI-direction adapters: `from_openapi` imports external HTTP APIs
|
||||
as call-protocol operations (reqwest-backed forwarding handlers), and
|
||||
The OpenAPI-direction adapters plus the single-endpoint adapter:
|
||||
`from_openapi` imports external HTTP APIs described by a full OpenAPI
|
||||
document, `from_jsonschema` imports a single non-standard / non-OpenAPI
|
||||
HTTP endpoint described by a caller-supplied `OperationSpec`, and
|
||||
`to_openapi` generates an OpenAPI spec from the local registry's
|
||||
`External` operations. This document covers both, the error fidelity
|
||||
(ADR-023), and the no-env-vars credential injection point.
|
||||
`External` operations. This document covers all three, the error
|
||||
fidelity (ADR-023), and the no-env-vars credential injection point.
|
||||
|
||||
## What
|
||||
|
||||
Two adapters, both in `alknet-http`:
|
||||
Three adapters, all in `alknet-http`:
|
||||
|
||||
1. **`from_openapi`** — parses an OpenAPI document, constructs a
|
||||
`HandlerRegistration` bundle per OpenAPI operation with a forwarding
|
||||
@@ -23,7 +25,15 @@ Two adapters, both in `alknet-http`:
|
||||
`alknet-call`, ADR-017 §5). Provenance is `FromOpenAPI` (leaf,
|
||||
`composition_authority: None`, `scoped_env: None`, `Internal` by
|
||||
default — ADR-015/022).
|
||||
2. **`to_openapi`** — generates an OpenAPI document from the local
|
||||
2. **`from_jsonschema`** — registers a single HTTP endpoint as a
|
||||
call-protocol operation, one at a time, for non-standard /
|
||||
non-OpenAPI / basic REST endpoints that don't have a full OpenAPI
|
||||
document. The caller supplies an `OperationSpec` + `HttpServiceConfig`
|
||||
+ path template + HTTP method; the adapter builds one
|
||||
`HandlerRegistration` with a reqwest forwarding handler (the same
|
||||
handler shape as `from_openapi`) and `FromJsonSchema` provenance.
|
||||
Implements `OperationAdapter`. See ADR-066.
|
||||
3. **`to_openapi`** — generates an OpenAPI document from the local
|
||||
registry's `External` operations. A pure projection: it consumes the
|
||||
registry, it does not produce entries for it (ADR-017 §5 — the `to_*`
|
||||
adapters are outbound projections, not `OperationAdapter`
|
||||
@@ -271,6 +281,80 @@ source other than `OperationContext.capabilities`. See
|
||||
[overview.md](overview.md) and
|
||||
[client-and-adapters.md](../call/client-and-adapters.md).
|
||||
|
||||
### from_jsonschema
|
||||
|
||||
`from_jsonschema` registers a single HTTP endpoint as a call-protocol
|
||||
operation, one at a time. It is functionally similar to `from_openapi`
|
||||
but for one endpoint instead of a full OpenAPI document — for
|
||||
non-standard, non-OpenAPI, or basic REST endpoints that don't have a
|
||||
`paths` object, an `operationId`, or `components`. The caller supplies
|
||||
the schema directly; the adapter builds a reqwest forwarding handler
|
||||
identical in shape to `from_openapi`'s. See
|
||||
[ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md).
|
||||
|
||||
```rust
|
||||
pub struct FromJsonSchema {
|
||||
spec: OperationSpec,
|
||||
config: HttpServiceConfig,
|
||||
path_template: String,
|
||||
method: String,
|
||||
http_client: Arc<SharedHttpClient>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OperationAdapter for FromJsonSchema {
|
||||
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
|
||||
}
|
||||
```
|
||||
|
||||
The adapter:
|
||||
|
||||
1. Takes an `OperationSpec` (name, op type, input/output JSON Schema,
|
||||
`error_schemas`, `access_control`, `visibility`), an
|
||||
`HttpServiceConfig` (base URL, auth scheme, default headers — the
|
||||
same config type `from_openapi` uses), a path template
|
||||
(e.g. `/users/{id}/posts`), and an HTTP method (e.g. `GET`).
|
||||
2. Builds one `HandlerRegistration`:
|
||||
- `spec` = the caller-supplied `OperationSpec` (the caller already
|
||||
has the JSON Schemas; no parsing needed).
|
||||
- `handler` = a reqwest forwarding handler, identical in shape to
|
||||
`from_openapi`'s: builds the HTTP request (path-template
|
||||
substitution, query params, body), injects credentials from
|
||||
`context.capabilities`, sends via the shared HTTP client, parses
|
||||
the response (JSON / text / binary — same content-type branching).
|
||||
For `Subscription` op type, registers a `StreamingHandler`
|
||||
(ADR-049) expecting `text/event-stream`.
|
||||
- `provenance` = `FromJsonSchema` (leaf, `composition_authority: None`,
|
||||
`scoped_env: None` — ADR-022).
|
||||
- `capabilities` = the credentials the forwarding handler needs
|
||||
(same no-env-vars path as `from_openapi`).
|
||||
3. Returns the single bundle. The caller registers it in the
|
||||
`OperationRegistry`.
|
||||
|
||||
#### Relationship to from_openapi
|
||||
|
||||
`from_jsonschema` is functionally similar to `from_openapi` but for one
|
||||
endpoint instead of a full OpenAPI document. The two adapters share the
|
||||
forwarding-handler implementation, the credential injection path, the
|
||||
error-fidelity rule (`HTTP_<status>` prefix, ADR-023), the streaming
|
||||
shape (ADR-049), and the no-env-vars invariant (ADR-014). The difference
|
||||
is purely the input shape: a full document vs. a single endpoint. See
|
||||
[ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md)
|
||||
§"Relationship to `from_openapi`" for the comparison table.
|
||||
|
||||
#### Origin (ADR-066)
|
||||
|
||||
`from_jsonschema` was originally placed in `alknet-call` (ADR-017 §5) as a
|
||||
schema-only adapter with a `NOT_FOUND`-returning placeholder handler —
|
||||
broken, because an op in the registry needs a real handler.
|
||||
[ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) moved
|
||||
it to `alknet-http` and gave it a real reqwest forwarding handler. The
|
||||
`FromJsonSchema` provenance variant stays in `alknet-call`
|
||||
(`OperationProvenance`); only the adapter implementation moved. See
|
||||
[ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) for the
|
||||
full rationale (why the placeholder was broken, why the "schema-only"
|
||||
concept conflated two things, why it was mispaced in `alknet-call`).
|
||||
|
||||
### to_openapi
|
||||
|
||||
```rust
|
||||
@@ -397,6 +481,14 @@ its errors are typed. The agent crate's LLM provider calls go through
|
||||
`from_openapi`-imported operations — that's how the no-env-vars
|
||||
invariant makes aisdk's env-var reads unreachable.
|
||||
|
||||
`from_jsonschema` fills the gap that `from_openapi` can't: endpoints
|
||||
that have no OpenAPI document. A non-standard REST endpoint, a basic
|
||||
internal API, or a third-party service with only a JSON Schema
|
||||
description can be registered as a call-protocol operation one at a
|
||||
time, with the same reqwest forwarding handler and the same
|
||||
no-env-vars credential path. The caller supplies the schema; the
|
||||
adapter supplies the handler. See ADR-066.
|
||||
|
||||
`to_openapi` is how external systems discover the alknet operation
|
||||
surface. A client generator, a human developer, or a `fetch`-based
|
||||
client reads the OpenAPI doc to learn the gateway's shape (5 fixed
|
||||
@@ -415,12 +507,15 @@ once published, the 5-endpoint gateway shape is one-way.
|
||||
- **`from_openapi`/`from_mcp` handlers read credentials from
|
||||
`OperationContext.capabilities`, not `std::env::var`.** This is the
|
||||
no-env-vars invariant (ADR-014). The handler implementations are
|
||||
verified against this invariant.
|
||||
verified against this invariant. `from_jsonschema` shares this
|
||||
invariant — same handler shape, same credential path (ADR-066).
|
||||
- **`from_openapi`-registered ops are `Internal` by default.** They are
|
||||
composition material, not directly callable from the wire (ADR-015).
|
||||
The handler that composes them is `External`.
|
||||
The handler that composes them is `External`. `from_jsonschema`
|
||||
ops are `Internal` by default for the same reason (ADR-066).
|
||||
- **`from_openapi` error codes are prefixed `HTTP_<status>`.** No
|
||||
collision with protocol-level codes (ADR-023, review #002 W20).
|
||||
`from_jsonschema` shares this rule (ADR-066).
|
||||
- **`from_openapi` accepts JSON and YAML; `from_str` detects format
|
||||
JSON-first.** JSON-first is a defensive default (ADR-051 §2 as
|
||||
amended): JSON's stricter grammar is immune to any YAML-specific type
|
||||
@@ -460,7 +555,8 @@ once published, the 5-endpoint gateway shape is one-way.
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| `from_openapi` is an `OperationAdapter` | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Async trait; produces `HandlerRegistration` bundles |
|
||||
| `from_openapi` is an `OperationAdapter` | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Async trait; produces `HandlerRegistration` bundles. ~~`from_jsonschema` clause superseded by ADR-066~~ |
|
||||
| `from_jsonschema` as HTTP-backed single-endpoint adapter in alknet-http | [ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf |
|
||||
| `to_openapi` is a projection, not an adapter | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Consumes the registry, doesn't produce entries |
|
||||
| Adapter-registered ops are `Internal` | [ADR-015](../../decisions/015-privilege-model-and-authority-context.md) | `from_openapi` ops are composition material |
|
||||
| `from_openapi` provenance is a leaf | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | `composition_authority: None`, `scoped_env: None` |
|
||||
@@ -469,7 +565,7 @@ once published, the 5-endpoint gateway shape is one-way.
|
||||
| HTTP path = operation path (~~direct-call surface~~) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) → superseded by [ADR-047](../../decisions/047-remove-direct-call-http-surface.md) | ~~`POST /{service}/{op}` → `call.requested`~~ — removed; the gateway `/call` with `{ operation, input }` is the sole invoke path; `to_openapi` describes the gateway, not a per-operation surface |
|
||||
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered. Supersedes ADR-036's original `to_openapi` "paths mirror `/{service}/{op}`" clause |
|
||||
| `to_openapi` published-spec versioning | [ADR-045](../../decisions/045-to-openapi-gateway-spec-versioning.md) | `info.version` semver tracks the gateway endpoint contract, not the operation set; consumers detect breaking changes via the major version |
|
||||
| Streaming handler for subscriptions | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `from_openapi` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE response → `BoxStream<ResponseEnvelope>`; `Query`/`Mutation` stay `HandlerKind::Once` |
|
||||
| Streaming handler for subscriptions | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `from_openapi` / `from_jsonschema` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE response → `BoxStream<ResponseEnvelope>`; `Query`/`Mutation` stay `HandlerKind::Once` |
|
||||
| YAML input + JSON-first format detection | [ADR-051](../../decisions/051-yaml-input-for-from-openapi.md) | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (defensive default, §2 amended — `yaml_serde` 0.10.x is YAML 1.2, not 1.1; JSON-first locks the contract against a future parser swap); YAML dep is `yaml_serde`; `to_openapi` output stays JSON (out of scope, §4) |
|
||||
|
||||
## Open Questions
|
||||
@@ -492,6 +588,9 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
|
||||
- [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md)
|
||||
— `OperationAdapter` trait, `to_*` are projections
|
||||
- [ADR-066](../../decisions/066-from-jsonschema-as-http-adapter.md) —
|
||||
`from_jsonschema` as HTTP-backed single-endpoint adapter in
|
||||
`alknet-http` (supersedes ADR-017 §5's `from_jsonschema` clause)
|
||||
- [ADR-023](../../decisions/023-operation-error-schemas.md) — error
|
||||
fidelity, `HTTP_<status>` prefix rule
|
||||
- [overview.md](overview.md) — adapter location map, no-env-vars
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-06
|
||||
last_updated: 2026-07-09
|
||||
---
|
||||
|
||||
# alknet-http — Overview
|
||||
@@ -154,7 +154,6 @@ implementations live where their transport dependencies live.
|
||||
alknet-call (lean — no HTTP client, no HTTP server)
|
||||
├── OperationAdapter trait (the contract — async, ADR-017 §5)
|
||||
├── from_call (QUIC — discovers remote ops via call protocol)
|
||||
├── from_jsonschema (pure parse — caller fetches the doc, passes it in)
|
||||
└── CallClient (outbound connection opener)
|
||||
|
||||
alknet-http (owns HTTP server + HTTP client)
|
||||
@@ -162,6 +161,7 @@ alknet-http (owns HTTP server + HTTP client)
|
||||
├── [WS upgrade → native session] (hands the WS message stream to the shared Dispatcher —
|
||||
│ not an adapter; see websocket.md, ADR-048)
|
||||
├── from_openapi (parse OpenAPI doc + reqwest forwarding handler)
|
||||
├── from_jsonschema (single-endpoint reqwest forwarding handler — ADR-066)
|
||||
├── to_openapi (generate OpenAPI doc from local registry)
|
||||
├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest)
|
||||
├── to_mcp (feature-gated) (expose local ops as MCP tools over streamable HTTP — axum)
|
||||
@@ -169,10 +169,12 @@ alknet-http (owns HTTP server + HTTP client)
|
||||
from_call-aligned, same-protocol; see websocket.md §"Future")
|
||||
```
|
||||
|
||||
`alknet-call` never sees the HTTP client. The `from_openapi`/`from_mcp`
|
||||
forwarding handlers are opaque `Arc<dyn Handler>` from the registry's
|
||||
perspective. `alknet-call` stays lean; `alknet-http` owns both HTTP
|
||||
directions.
|
||||
`alknet-call` never sees the HTTP client. The `from_openapi`/
|
||||
`from_mcp`/`from_jsonschema` forwarding handlers are opaque
|
||||
`Arc<dyn Handler>` from the registry's perspective. `alknet-call` stays
|
||||
lean; `alknet-http` owns both HTTP directions. `from_jsonschema` was
|
||||
originally (mis)placed in `alknet-call` as a schema-only placeholder;
|
||||
ADR-066 moved it to `alknet-http` as a real HTTP-backed adapter.
|
||||
|
||||
## Feature Gates
|
||||
|
||||
@@ -208,9 +210,9 @@ Rust, no native code), consistent with the default-features philosophy.
|
||||
|
||||
## The No-Env-Vars Invariant
|
||||
|
||||
The `from_openapi`/`from_mcp` forwarding handlers are the **credential
|
||||
injection point** for the no-env-vars architecture. The path (from the
|
||||
gap analysis):
|
||||
The `from_openapi`/`from_mcp`/`from_jsonschema` forwarding handlers are
|
||||
the **credential injection point** for the no-env-vars architecture. The
|
||||
path (from the gap analysis):
|
||||
|
||||
```
|
||||
vault → assembly layer → Capabilities → HandlerRegistration.capabilities
|
||||
@@ -227,8 +229,9 @@ through `from_openapi` operations that carry the credential in
|
||||
|
||||
**This is a spec-level invariant**: no handler reads outbound
|
||||
credentials from any source other than `OperationContext.capabilities`.
|
||||
The `from_openapi`/`from_mcp` implementations in `alknet-http` are
|
||||
verified against this invariant. See ADR-014 and
|
||||
The `from_openapi`/`from_mcp`/`from_jsonschema` implementations in
|
||||
`alknet-http` are verified against this invariant (same handler shape —
|
||||
`from_jsonschema` per ADR-066). See ADR-014 and
|
||||
[client-and-adapters.md](../call/client-and-adapters.md).
|
||||
|
||||
## Architecture (component pointers)
|
||||
@@ -245,7 +248,9 @@ verified against this invariant. See ADR-014 and
|
||||
browsers-are-not-peers rationale, streaming (native `call.responded`,
|
||||
no SSE), and the deferred `from_wss` adapter.
|
||||
- **[http-adapters.md](http-adapters.md)** — `from_openapi` (parse
|
||||
OpenAPI, build forwarding handlers with `reqwest`) and `to_openapi`
|
||||
OpenAPI, build forwarding handlers with `reqwest`),
|
||||
`from_jsonschema` (single-endpoint reqwest forwarding handler for
|
||||
non-standard/non-OpenAPI endpoints, ADR-066), and `to_openapi`
|
||||
(generate an OpenAPI doc from the registry's `External` operations).
|
||||
Error fidelity per ADR-023.
|
||||
- **[http-mcp.md](http-mcp.md)** — `from_mcp`/`to_mcp` (feature-gated),
|
||||
|
||||
@@ -182,8 +182,12 @@ Implementations:
|
||||
- `FromMCP` — imports from an MCP server (MCP-backed handlers)
|
||||
- `FromCall` — imports from a remote call protocol endpoint
|
||||
(call-protocol-backed handlers)
|
||||
- `FromJsonSchema` — imports from a JSON Schema definition (schema-only, no
|
||||
handler — used for validation or client generation)
|
||||
- ~~`FromJsonSchema` — imports from a JSON Schema definition (schema-only, no
|
||||
handler — used for validation or client generation)~~ — **superseded by
|
||||
[ADR-066](066-from-jsonschema-as-http-adapter.md)**: `from_jsonschema` is
|
||||
now an HTTP-backed single-endpoint adapter in `alknet-http` (reqwest
|
||||
forwarding handler, not a schema-only placeholder); `FromJsonSchema`
|
||||
provenance stays in `alknet-call` as a handler-bearing leaf.
|
||||
|
||||
The `to_*` adapters are outbound projections, not `OperationAdapter`
|
||||
implementations — they consume the registry, they don't produce entries for it.
|
||||
@@ -191,7 +195,10 @@ implementations — they consume the registry, they don't produce entries for it
|
||||
The specific trait signatures (error types, configuration parameters) are
|
||||
two-way doors for implementation. The one-way doors are the architectural
|
||||
commitments: adapters produce `HandlerRegistration` bundles (ADR-022), the
|
||||
trait is async (required by `from_call`), and adapters live in alknet-call.
|
||||
trait is async (required by `from_call`), and the adapter *trait* lives in
|
||||
`alknet-call` while adapter *implementations* live with their transport
|
||||
(HTTP-backed adapters in `alknet-http` per ADR-066; QUIC-backed `from_call`
|
||||
in `alknet-call`). See `client-and-adapters.md` §"Adapter Location Map."
|
||||
|
||||
### 6. Cross-node call tree and abort cascade
|
||||
|
||||
@@ -405,9 +412,27 @@ OQ-28.
|
||||
### Operational spec
|
||||
|
||||
The gap this ADR left to implementation — the `CallClient` API, the
|
||||
`from_call`/`from_jsonschema` flows, the trait signature, the adapter
|
||||
location map, the no-env-vars invariant, and the exchange-of-operations
|
||||
pattern — is specified in
|
||||
`from_call` flow, the trait signature, the adapter location map, the
|
||||
no-env-vars invariant, and the exchange-of-operations pattern — is
|
||||
specified in
|
||||
[client-and-adapters.md](../crates/call/client-and-adapters.md). That document
|
||||
is the operational complement to this ADR; this ADR remains the architectural
|
||||
authority.
|
||||
authority.
|
||||
|
||||
## Amendments (2026-07-09)
|
||||
|
||||
### `from_jsonschema` clause superseded by ADR-066
|
||||
|
||||
The §5 `FromJsonSchema` implementation listing ("schema-only, no handler")
|
||||
is **superseded by [ADR-066](066-from-jsonschema-as-http-adapter.md)**.
|
||||
`from_jsonschema` is now an HTTP-backed single-endpoint adapter in
|
||||
`alknet-http` (reqwest forwarding handler, same shape as `from_openapi`),
|
||||
not a schema-only placeholder in `alknet-call`. The `FromJsonSchema`
|
||||
provenance variant stays in `alknet-call` (`OperationProvenance`) but is
|
||||
now a handler-bearing leaf, not a "no handler" entry. The "schema-only,
|
||||
no handler" concept is removed — schema validation without a handler is
|
||||
served by consuming `OperationSpec` directly. The §5 "adapters live in
|
||||
alknet-call" one-way-door statement is corrected above to "the adapter
|
||||
trait lives in `alknet-call`; implementations live with their transport."
|
||||
See [ADR-066](066-from-jsonschema-as-http-adapter.md) and
|
||||
[client-and-adapters.md](../crates/call/client-and-adapters.md) §"from_jsonschema".
|
||||
+16
-4
@@ -123,7 +123,9 @@ pub enum OperationProvenance {
|
||||
/// QUIC forwarding stub (from_call). Leaf in the local registry —
|
||||
/// forwards calls to a remote node; cannot compose locally.
|
||||
FromCall,
|
||||
/// JSON Schema definition (from_jsonschema), no handler — schema only.
|
||||
/// HTTP forwarding stub (from_jsonschema, single endpoint), leaf —
|
||||
/// cannot compose. (ADR-066: was "no handler — schema only"; now a
|
||||
/// real reqwest-backed forwarding handler in alknet-http.)
|
||||
FromJsonSchema,
|
||||
/// Agent-written, sandboxed, can compose within sandbox bounds.
|
||||
Session,
|
||||
@@ -136,12 +138,22 @@ pub enum OperationProvenance {
|
||||
| `FromOpenAPI` | No (leaf) | No | Internal | HTTP endpoint trusted; handler is a forwarding stub |
|
||||
| `FromMCP` | No (leaf) | No | Internal | MCP server trusted; handler is a forwarding stub |
|
||||
| `FromCall` | No (leaf in local registry) | No | Internal | Remote node trusted; handler is a forwarding stub |
|
||||
| `FromJsonSchema` | N/A (no handler) | No | N/A | N/A |
|
||||
| `FromJsonSchema` | No (leaf) | No | Internal | HTTP endpoint trusted; handler is a forwarding stub (ADR-066) |
|
||||
| `Session` | Yes (within sandbox) | Yes — scopes set by assembly layer at sandbox creation | Internal always | Untrusted code in sandbox |
|
||||
|
||||
> **ADR-066 amendment (2026-07-09).** The `FromJsonSchema` row
|
||||
> previously read "N/A (no handler) / N/A / N/A" — `from_jsonschema`
|
||||
> was a schema-only placeholder in `alknet-call` with a
|
||||
> `NOT_FOUND`-returning handler.
|
||||
> [ADR-066](066-from-jsonschema-as-http-adapter.md) moved the adapter
|
||||
> to `alknet-http` as a real HTTP-backed single-endpoint adapter with a
|
||||
> reqwest forwarding handler. `FromJsonSchema` is now a leaf, same
|
||||
> trust model as `FromOpenAPI` (HTTP endpoint trusted; handler is a
|
||||
> forwarding stub). The "schema-only, no handler" concept is removed.
|
||||
|
||||
Only `Local` and `Session` ops get composition authority. Leaves
|
||||
(`FromOpenAPI`, `FromMCP`, `FromCall`) don't compose, so they don't get one.
|
||||
The assembly layer does not invent identities for leaves.
|
||||
(`FromOpenAPI`, `FromMCP`, `FromCall`, `FromJsonSchema`) don't compose, so
|
||||
they don't get one. The assembly layer does not invent identities for leaves.
|
||||
|
||||
### 2. Composition authority replaces `handler_identity: Identity`
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# ADR-066: `from_jsonschema` as an HTTP-Backed Single-Endpoint Adapter in alknet-http
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (supersedes the `from_jsonschema` clause of ADR-017 §5 and the
|
||||
`FromJsonSchema` provenance row of ADR-022 — both described a schema-only,
|
||||
no-handler adapter in `alknet-call`)
|
||||
|
||||
## Context
|
||||
|
||||
`from_jsonschema` was originally specified (ADR-017 §5) as a schema-only
|
||||
adapter living in `alknet-call`: it produced `HandlerRegistration` bundles
|
||||
with a `NOT_FOUND`-returning placeholder handler and `FromJsonSchema`
|
||||
provenance. The stated use case was validation, discovery, and
|
||||
composition-graph construction without a runtime — type-checking a
|
||||
composition plan without executing it, building a UI of available
|
||||
operations without standing up the transports.
|
||||
|
||||
This is broken. An operation in the `OperationRegistry` needs a real
|
||||
handler. A placeholder that returns `NOT_FOUND` does not work with how
|
||||
the registry is supposed to function: an `Internal` op registered with
|
||||
a dead handler is a trap, not a feature. The "schema-only, no handler"
|
||||
concept conflated two things — schema *validation* (a compile-time /
|
||||
planning activity that doesn't need a registry entry at all) and
|
||||
operation *registration* (which always needs a handler). Validation
|
||||
against a JSON Schema does not require a `HandlerRegistration`; it
|
||||
requires the schema and a validator. Registering an operation requires
|
||||
a handler. The old `from_jsonschema` tried to do the former by abusing
|
||||
the latter, and produced something that works for neither.
|
||||
|
||||
The misplacement was compounded by a location error: the adapter lived
|
||||
in `alknet-call` (which is supposed to stay lean — no HTTP client), but
|
||||
a `from_jsonschema` that is actually useful for calling non-standard
|
||||
endpoints needs reqwest, exactly like `from_openapi` and `from_mcp`.
|
||||
The adapter location map in ADR-017 / `client-and-adapters.md` already
|
||||
establishes that HTTP-backed adapters live in `alknet-http`; the old
|
||||
`from_jsonschema` violated its own stated principle by living in
|
||||
`alknet-call`.
|
||||
|
||||
A concrete use case now forces the decision: composing a non-standard,
|
||||
non-OpenAPI, basic REST endpoint that does not have a full OpenAPI
|
||||
document. The endpoint has a method, a URL, an input/output JSON Schema,
|
||||
and an auth scheme — but no `paths` object, no `operationId`, no
|
||||
`components`. `from_openapi` requires an OpenAPI document; this endpoint
|
||||
doesn't have one. The gap is: register a single HTTP endpoint as a
|
||||
call-protocol operation, one at a time, with the caller supplying the
|
||||
schema directly.
|
||||
|
||||
## Decision
|
||||
|
||||
`from_jsonschema` becomes an HTTP-backed single-endpoint adapter in
|
||||
`alknet-http`, functionally similar to `from_openapi` but registering
|
||||
one endpoint at a time instead of parsing a full OpenAPI document:
|
||||
|
||||
1. **Move the adapter implementation to `alknet-http`**
|
||||
(`crates/alknet-http/src/adapters/from_jsonschema.rs`). The
|
||||
forwarding handler uses the same reqwest-backed `SharedHttpClient`
|
||||
and the same no-env-vars credential injection as `from_openapi`. The
|
||||
adapter implements `OperationAdapter` (the trait from `alknet-call`,
|
||||
ADR-017 §5 — unchanged).
|
||||
|
||||
2. **Give it a real forwarding handler.** A `from_jsonschema`-imported
|
||||
operation is a leaf with a reqwest forwarding handler, identical in
|
||||
shape to a `from_openapi`-imported operation — it builds an HTTP
|
||||
request from the input (path/query/body split per a path template),
|
||||
injects credentials from `context.capabilities`, sends via the shared
|
||||
HTTP client, and parses the response (JSON, text, or binary — same
|
||||
content-type branching as `from_openapi`). For a `Subscription`
|
||||
op type with `text/event-stream` response, it registers a
|
||||
`StreamingHandler` (ADR-049), same as `from_openapi`.
|
||||
|
||||
3. **Single-endpoint registration.** The caller supplies:
|
||||
- An `OperationSpec` (name, op type, input/output JSON Schema,
|
||||
`error_schemas`, `access_control`, `visibility`).
|
||||
- An `HttpServiceConfig` (base URL, auth scheme, default headers —
|
||||
the same config type `from_openapi` uses).
|
||||
- A path template + HTTP method (the one endpoint).
|
||||
|
||||
The adapter builds one `HandlerRegistration` with `FromJsonSchema`
|
||||
provenance and a real forwarding handler. The caller registers it in
|
||||
the `OperationRegistry`. This is the "one endpoint at a time" shape:
|
||||
no `paths` object to iterate, no `operationId` to normalize.
|
||||
|
||||
4. **`FromJsonSchema` provenance stays in `alknet-call`** (in the
|
||||
`OperationProvenance` enum, `registration.rs`). The provenance type
|
||||
lives where the registry types live; only the adapter implementation
|
||||
moves. `FromJsonSchema` is now a leaf provenance — it has a handler
|
||||
(a reqwest forwarding handler), same trust model as `FromOpenAPI`
|
||||
(HTTP endpoint trusted; handler is a forwarding stub).
|
||||
|
||||
5. **Remove the "schema-only, no handler" concept.** The placeholder
|
||||
handler and the "schema-only ops are `Internal`, so dispatch should
|
||||
never reach them" rationale are removed. An op registered with
|
||||
`FromJsonSchema` provenance is a real, callable, HTTP-forwarding
|
||||
operation — `Internal` by default (adapter-registered ops are
|
||||
composition material, ADR-015), but it actually forwards if invoked.
|
||||
|
||||
The schema-validation-without-a-handler use case (type-checking a
|
||||
composition plan, building a UI) does not require a
|
||||
`HandlerRegistration` at all. That use case is served by consuming
|
||||
the `OperationSpec` directly (the spec already carries the input/
|
||||
output JSON Schemas); no adapter, no registry entry, no handler is
|
||||
needed. If a future use case requires registering a schema-only op
|
||||
for discovery purposes, that is a separate feature and would warrant
|
||||
its own ADR — it is not what `from_jsonschema` is.
|
||||
|
||||
### Relationship to `from_openapi`
|
||||
|
||||
| | `from_openapi` | `from_jsonschema` |
|
||||
|---|---|---|
|
||||
| Input | A full OpenAPI 3.x document (JSON or YAML) | A single endpoint: `OperationSpec` + `HttpServiceConfig` + path template + method |
|
||||
| Granularity | One `HandlerRegistration` per `(path, method)` in the doc | One `HandlerRegistration` per call |
|
||||
| Schema source | Parsed from the OpenAPI doc (parameters, request body, responses) | Supplied directly by the caller |
|
||||
| Handler | reqwest forwarding handler (shared HTTP client) | Same reqwest forwarding handler |
|
||||
| Provenance | `FromOpenAPI` | `FromJsonSchema` |
|
||||
| Location | `alknet-http` | `alknet-http` |
|
||||
| Use case | Standard OpenAPI APIs (GitHub, OpenAI, Anthropic) | Non-standard, non-OpenAPI, or basic REST endpoints without a full spec |
|
||||
|
||||
The two adapters share the forwarding-handler implementation, the
|
||||
credential injection path, the error-fidelity rule (`HTTP_<status>`
|
||||
prefix, ADR-023), and the no-env-vars invariant (ADR-014). The
|
||||
difference is purely the input shape: a full document vs. a single
|
||||
endpoint.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**:
|
||||
- `from_jsonschema` actually works — it has a real handler, not a
|
||||
placeholder. A concrete use case (non-standard REST endpoints) is
|
||||
served.
|
||||
- The adapter location is consistent: all HTTP-backed adapters
|
||||
(`from_openapi`, `from_mcp`, `from_jsonschema`) live in `alknet-http`,
|
||||
where reqwest is. `alknet-call` stays lean.
|
||||
- The "schema-only, no handler" trap is removed. An op in the registry
|
||||
is always callable.
|
||||
- `FromJsonSchema` provenance becomes a real leaf, consistent with
|
||||
`FromOpenAPI`/`FromMCP`/`FromCall`.
|
||||
|
||||
**Negative**:
|
||||
- The schema-validation-without-a-handler use case (the original stated
|
||||
purpose) is no longer served by `from_jsonschema`. That use case is
|
||||
served by consuming `OperationSpec` directly, but any code that relied
|
||||
on the placeholder handler returning `NOT_FOUND` breaks. The only
|
||||
existing consumer is the call crate's own tests; no downstream consumer
|
||||
depended on this — the placeholder was a trap, not a contract.
|
||||
- `alknet-call` loses a public export (`from_jsonschema`, `FromJsonSchema`
|
||||
the adapter struct). The `FromJsonSchema` provenance variant stays;
|
||||
the adapter struct moves. Downstream consumers that referenced the
|
||||
adapter (none currently) would need to use `alknet-http`'s re-export.
|
||||
|
||||
**Neutral**:
|
||||
- `FromJsonSchema` provenance is now a leaf (handler-bearing), not a
|
||||
"no handler" provenance. The ADR-022 table row updates: it can compose?
|
||||
No. Has composition authority? No. Default visibility? Internal. Trust
|
||||
model? HTTP endpoint trusted; handler is a forwarding stub. This
|
||||
aligns with the other leaves. ADR-017 §5 and ADR-022's provenance
|
||||
table/enum-doc are amended (2026-07-09) to point here — the
|
||||
supersession is recorded in the superseded ADRs, not only in this one.
|
||||
|
||||
## References
|
||||
|
||||
- Supersedes the `from_jsonschema` clause of
|
||||
[ADR-017](017-call-protocol-client-and-adapter-contract.md) §5
|
||||
("`FromJsonSchema` — imports from a JSON Schema definition (schema-only,
|
||||
no handler)") and the operational spec in
|
||||
`docs/architecture/crates/call/client-and-adapters.md` §"from_jsonschema".
|
||||
- Supersedes the `FromJsonSchema` row of
|
||||
[ADR-022](022-handler-registration-provenance-and-composition-authority.md)
|
||||
(the "no handler — schema only" framing).
|
||||
- Aligns with the adapter location principle in
|
||||
[ADR-017](017-call-protocol-client-and-adapter-contract.md) §5 and
|
||||
`client-and-adapters.md` §"Adapter Location Map": HTTP-backed adapters
|
||||
live in `alknet-http`.
|
||||
- Reuses the forwarding handler, credential injection, error fidelity
|
||||
(`HTTP_<status>` prefix, [ADR-023](023-operation-error-schemas.md)),
|
||||
streaming shape ([ADR-049](049-streaming-handler-for-subscriptions.md)),
|
||||
and no-env-vars invariant ([ADR-014](014-secret-material-flow-and-capability-injection.md))
|
||||
established by `from_openapi`.
|
||||
- Reuses `HttpServiceConfig` and `SharedHttpClient` from
|
||||
`from_openapi` (in `alknet-http`).
|
||||
@@ -144,7 +144,7 @@ See [ADR-008](decisions/008-secret-service-integration.md) and [ADR-014](decisio
|
||||
|
||||
alknet-call uses hand-rolled `EventEnvelope` framing (length-prefixed JSON). The wire format, operation registry, and dispatch are all hand-rolled in alknet-call — irpc was never integrated (ADR-064 supersedes ADR-005, which had accepted "irpc as the call protocol foundation" based on the previous architecture but was never implemented as stated). Operations are registered in a hand-rolled registry with JSON Schema discovery. The call protocol supports request/response, streaming subscriptions, and pub/sub.
|
||||
|
||||
The call protocol's adapter contract (from_openapi, from_mcp, from_call, to_openapi, to_mcp) enables bidirectional composition — operations can be imported from external sources and exported to external protocols. These adapter traits are defined in Rust in alknet-call. The existing TypeScript `@alkdev/operations` library informed the design and may be adapted for browser use (see ADR-013).
|
||||
The call protocol's adapter contract (from_openapi, from_jsonschema, from_mcp, from_call, to_openapi, to_mcp) enables bidirectional composition — operations can be imported from external sources and exported to external protocols. The adapter *trait* is defined in `alknet-call`; HTTP-backed adapter implementations (`from_openapi`, `from_jsonschema`, `from_mcp`, `to_openapi`, `to_mcp`) live in `alknet-http` (`from_jsonschema` moved there per ADR-066; the QUIC-backed `from_call` stays in `alknet-call`). The existing TypeScript `@alkdev/operations` library informed the design and may be adapted for browser use (see ADR-013).
|
||||
|
||||
See [ADR-064](decisions/064-irpc-never-integrated-hand-rolled-framing.md) for the full rationale (supersedes [ADR-005](decisions/005-irpc-as-call-protocol-foundation.md)).
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ scope: narrow
|
||||
risk: low
|
||||
impact: isolated
|
||||
level: implementation
|
||||
superseded_by: [http/adapters/from-jsonschema]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
id: http/adapters/from-jsonschema
|
||||
name: Move from_jsonschema to alknet-http as a real HTTP-backed single-endpoint adapter (ADR-066); remove broken placeholder from alknet-call
|
||||
status: pending
|
||||
depends_on: [http/adapters/from-openapi]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
`from_jsonschema` was originally implemented in `alknet-call`
|
||||
(`crates/alknet-call/src/client/from_jsonschema.rs`) as a schema-only
|
||||
adapter with a `NOT_FOUND`-returning placeholder handler. That is broken:
|
||||
an operation in the `OperationRegistry` needs a real handler, and a
|
||||
placeholder that returns `NOT_FOUND` does not work with how the registry
|
||||
is supposed to function. It was also in the wrong crate — `alknet-call`
|
||||
is supposed to stay lean (no HTTP client), but a useful `from_jsonschema`
|
||||
needs reqwest, exactly like `from_openapi`.
|
||||
|
||||
ADR-066 moves `from_jsonschema` to `alknet-http` as a real HTTP-backed
|
||||
single-endpoint adapter, functionally similar to `from_openapi` but
|
||||
registering one endpoint at a time instead of parsing a full OpenAPI
|
||||
document. The use case is non-standard, non-OpenAPI, or basic REST
|
||||
endpoints that don't have a full OpenAPI document — the caller supplies
|
||||
the schema directly.
|
||||
|
||||
This task implements the move: adds the real adapter in `alknet-http`
|
||||
and removes the broken placeholder from `alknet-call`. The
|
||||
`FromJsonSchema` provenance variant stays in `alknet-call`
|
||||
(`OperationProvenance` in `registry/registration.rs`) — only the adapter
|
||||
implementation moves.
|
||||
|
||||
### The adapter (http-adapters.md §"from_jsonschema", ADR-066)
|
||||
|
||||
```rust
|
||||
pub struct FromJsonSchema {
|
||||
spec: OperationSpec,
|
||||
config: HttpServiceConfig,
|
||||
path_template: String,
|
||||
method: String,
|
||||
http_client: Arc<SharedHttpClient>,
|
||||
}
|
||||
|
||||
impl FromJsonSchema {
|
||||
pub fn new(
|
||||
spec: OperationSpec,
|
||||
config: HttpServiceConfig,
|
||||
path_template: String,
|
||||
method: String,
|
||||
http_client: Arc<SharedHttpClient>,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OperationAdapter for FromJsonSchema {
|
||||
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
|
||||
}
|
||||
```
|
||||
|
||||
The caller supplies:
|
||||
- An `OperationSpec` (name, op type, input/output JSON Schema,
|
||||
`error_schemas`, `access_control`, `visibility`) — the caller already
|
||||
has the JSON Schemas; no parsing needed.
|
||||
- An `HttpServiceConfig` (base URL, auth scheme, default headers — the
|
||||
same config type `from_openapi` uses, re-exported from
|
||||
`crate::adapters::from_openapi`).
|
||||
- A path template (e.g. `/users/{id}/posts`) and an HTTP method (e.g.
|
||||
`GET`).
|
||||
|
||||
### The import flow
|
||||
|
||||
The adapter builds one `HandlerRegistration`:
|
||||
- `spec` = the caller-supplied `OperationSpec` (no parsing, no
|
||||
`operationId` normalization — the caller named the op).
|
||||
- `handler` = a reqwest forwarding handler, **identical in shape to
|
||||
`from_openapi`'s**. Reuse `from_openapi`'s `build_request`, `forward`,
|
||||
and `forward_stream` functions (or factor the shared logic if those
|
||||
are not already reusable as-is — they should be, since the handler
|
||||
shape is the same; the only thing that differs is where the path
|
||||
template / method / error status codes come from). See
|
||||
"Implementation note" below.
|
||||
- `provenance` = `FromJsonSchema` (leaf, `composition_authority: None`,
|
||||
`scoped_env: None` — ADR-022).
|
||||
- `capabilities` = the credentials the forwarding handler needs (same
|
||||
no-env-vars path as `from_openapi` — injected at registration, read
|
||||
from `context.capabilities` at call time).
|
||||
|
||||
For a `Subscription` op type, register a `StreamingHandler`
|
||||
(`HandlerKind::Stream`) expecting `text/event-stream`, same as
|
||||
`from_openapi` (ADR-049). For `Query`/`Mutation`, register
|
||||
`HandlerKind::Once`.
|
||||
|
||||
Returns the single bundle. The caller registers it in the
|
||||
`OperationRegistry`.
|
||||
|
||||
### Implementation note — reuse from_openapi's forwarding logic
|
||||
|
||||
`from_openapi` (in `crates/alknet-http/src/adapters/from_openapi.rs`)
|
||||
already implements the full forwarding handler: `build_request` (path
|
||||
template substitution, query params, body, auth header injection from
|
||||
`context.capabilities`), `forward` (the `Once` handler — sends via
|
||||
`SharedHttpClient`, content-type branching JSON/text/binary, error
|
||||
mapping), and `forward_stream` (the `Stream` handler — SSE parsing).
|
||||
|
||||
These functions are currently free functions in `from_openapi.rs`. They
|
||||
take `base_url`, `path_template`, `method`, `auth_scheme`,
|
||||
`default_headers`, `namespace`, `error_status_codes`, `op_type`, `input`,
|
||||
`context` — exactly the parameters a single-endpoint adapter has. The
|
||||
cleanest implementation is one of:
|
||||
|
||||
1. **Call them directly** if they're `pub(crate)` — `from_jsonschema` is
|
||||
in the same crate (`alknet-http`), same module tree
|
||||
(`adapters/`). Make `build_request`, `forward`, `forward_stream` (and
|
||||
the helpers `value_to_path_segment`, `value_to_query`,
|
||||
`parse_sse_frames`) `pub(crate)` and call them from
|
||||
`from_jsonschema.rs`.
|
||||
2. **Factor a small shared module** (e.g. `adapters/forwarding.rs`) if
|
||||
you prefer the dependency to be explicit rather than reaching into
|
||||
`from_openapi`'s module. This is cleaner if you anticipate a third
|
||||
HTTP-backed adapter, but not required for this task.
|
||||
|
||||
Either is fine — pick whichever is less code churn. The handler logic
|
||||
is identical; do not duplicate it. The point of ADR-066 is that
|
||||
`from_jsonschema` shares `from_openapi`'s forwarding implementation.
|
||||
|
||||
### Error fidelity (ADR-023)
|
||||
|
||||
Same rule as `from_openapi`: error codes prefixed `HTTP_<status>` to
|
||||
avoid collision with protocol-level codes. The `error_schemas` come
|
||||
from the caller-supplied `OperationSpec` (the caller declares them);
|
||||
the handler maps non-2xx HTTP responses to the declared
|
||||
`ErrorDefinition` by status code, same as `from_openapi`. If the
|
||||
`OperationSpec`'s `error_schemas` is empty, fall back to
|
||||
`HTTP_<status>` (same fallback `from_openapi` uses).
|
||||
|
||||
### No-env-vars invariant (ADR-014)
|
||||
|
||||
The forwarding handler reads
|
||||
`context.capabilities.get(config.namespace)`, never `std::env::var`.
|
||||
Same invariant as `from_openapi`. The handler implementation is verified
|
||||
against this.
|
||||
|
||||
### Removal of the broken placeholder (alknet-call cleanup)
|
||||
|
||||
Remove the old broken implementation from `alknet-call`:
|
||||
- Delete `crates/alknet-call/src/client/from_jsonschema.rs`.
|
||||
- Remove `mod from_jsonschema;` and the
|
||||
`pub use from_jsonschema::{from_jsonschema, FromJsonSchema};` re-export
|
||||
from `crates/alknet-call/src/client/mod.rs`.
|
||||
- Do **not** remove the `FromJsonSchema` variant from
|
||||
`OperationProvenance` in `registry/registration.rs` — it stays. It is
|
||||
now a handler-bearing leaf (the handler lives in `alknet-http`, but
|
||||
the provenance type lives in `alknet-call` where the registry types
|
||||
live).
|
||||
|
||||
The old task `call/client/from-jsonschema` (the one that built the
|
||||
broken placeholder) is already marked `status: completed`; this task
|
||||
supersedes that work. No downstream consumer depends on the old
|
||||
`from_jsonschema` / `FromJsonSchema` exports from `alknet-call` (the
|
||||
only consumer was the call crate's own tests, which are removed with
|
||||
the file).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### alknet-http (new adapter)
|
||||
|
||||
- [ ] `crates/alknet-http/src/adapters/from_jsonschema.rs` exists with
|
||||
`FromJsonSchema` struct + `new()` constructor
|
||||
- [ ] `FromJsonSchema` holds `spec`, `config`, `path_template`,
|
||||
`method`, `http_client`
|
||||
- [ ] `FromJsonSchema` implements `OperationAdapter` (`import()` returns
|
||||
one `HandlerRegistration`)
|
||||
- [ ] The `HandlerRegistration` has `provenance: FromJsonSchema`,
|
||||
`composition_authority: None`, `scoped_env: None`
|
||||
- [ ] The handler is a real reqwest forwarding handler (not a
|
||||
placeholder) — reuses `from_openapi`'s forwarding logic
|
||||
(`build_request` / `forward` / `forward_stream`)
|
||||
- [ ] `Query`/`Mutation` → `HandlerKind::Once`; `Subscription` →
|
||||
`HandlerKind::Stream` (ADR-049)
|
||||
- [ ] Path-template substitution (`{id}` → input value), query params
|
||||
from non-path fields, `body` field for the request body
|
||||
- [ ] Credential injection from `context.capabilities` (Bearer / ApiKey
|
||||
/ Basic), never `std::env::var` (ADR-014)
|
||||
- [ ] Response parsing: JSON / text / binary (same content-type
|
||||
branching as `from_openapi`)
|
||||
- [ ] SSE streaming for `Subscription` ops (same `parse_sse_frames` as
|
||||
`from_openapi`)
|
||||
- [ ] Error fidelity: non-2xx mapped to declared `ErrorDefinition` by
|
||||
status code, `HTTP_<status>` prefix (ADR-023)
|
||||
- [ ] `HttpServiceConfig` and `HttpAuthScheme` reused from
|
||||
`from_openapi` (not redefined)
|
||||
- [ ] Exported from `crates/alknet-http/src/adapters/mod.rs`
|
||||
(`pub use from_jsonschema::FromJsonSchema;`)
|
||||
- [ ] Unit test: `import()` produces one `HandlerRegistration` with
|
||||
`FromJsonSchema` provenance + `None` authority/env
|
||||
- [ ] Unit test: forwarding handler builds the correct URL (path
|
||||
substitution + query)
|
||||
- [ ] Unit test: forwarding handler injects Bearer token from
|
||||
`context.capabilities`
|
||||
- [ ] Unit test: `Query` op → `HandlerKind::Once`, `Subscription` op →
|
||||
`HandlerKind::Stream`
|
||||
- [ ] Integration test: forwarding handler calls an external endpoint
|
||||
via `SharedHttpClient` and returns the response (use the
|
||||
`spawn_echo_server` pattern from `from_openapi`'s tests)
|
||||
- [ ] Integration test: non-2xx response → declared error code
|
||||
(`HTTP_<status>`)
|
||||
- [ ] Integration test: SSE subscription streams `call.responded` events
|
||||
- [ ] No `std::env::var` reads in the forwarding handler
|
||||
|
||||
### alknet-call (cleanup)
|
||||
|
||||
- [ ] `crates/alknet-call/src/client/from_jsonschema.rs` deleted
|
||||
- [ ] `mod from_jsonschema;` removed from
|
||||
`crates/alknet-call/src/client/mod.rs`
|
||||
- [ ] `pub use from_jsonschema::{from_jsonschema, FromJsonSchema};`
|
||||
removed from `crates/alknet-call/src/client/mod.rs`
|
||||
- [ ] `FromJsonSchema` variant **kept** in `OperationProvenance`
|
||||
(`registry/registration.rs`) — do not remove
|
||||
- [ ] `AdapterError::SchemaParse` doc comment in
|
||||
`crates/alknet-call/src/client/mod.rs` — update the
|
||||
"`from_openapi` / `from_jsonschema` couldn't parse the spec"
|
||||
wording if it now reads oddly (the variant stays; `from_jsonschema`
|
||||
in `alknet-http` can still return `SchemaParse` if the
|
||||
caller-supplied `OperationSpec` is somehow invalid, though that's
|
||||
unlikely since the caller constructs it — use judgment)
|
||||
|
||||
### Build / lint
|
||||
|
||||
- [ ] `cargo build -p alknet-call -p alknet-http` succeeds
|
||||
- [ ] `cargo test -p alknet-call -p alknet-http` succeeds
|
||||
- [ ] `cargo clippy -p alknet-call -p alknet-http --all-targets` succeeds
|
||||
with no warnings
|
||||
- [ ] `cargo fmt --check` succeeds
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md —
|
||||
ADR-066 (the decision this task implements)
|
||||
- docs/architecture/crates/http/http-adapters.md — §"from_jsonschema"
|
||||
(the spec: API, flow, relationship to from_openapi, origin)
|
||||
- docs/architecture/crates/call/client-and-adapters.md — §"from_jsonschema"
|
||||
(the move note + pointer to the http spec)
|
||||
- docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md —
|
||||
ADR-017 §5 (amended — `from_jsonschema` clause superseded by ADR-066)
|
||||
- docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md —
|
||||
ADR-022 (amended — `FromJsonSchema` row now handler-bearing leaf)
|
||||
- docs/architecture/decisions/023-operation-error-schemas.md — ADR-023
|
||||
(`HTTP_<status>` prefix, error fidelity)
|
||||
- docs/architecture/decisions/049-streaming-handler-for-subscriptions.md —
|
||||
ADR-049 (`StreamingHandler` for `Subscription` ops)
|
||||
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md —
|
||||
ADR-014 (no-env-vars invariant)
|
||||
- tasks/http/adapters/from-openapi.md — the `from_openapi` task
|
||||
(completed; the forwarding logic to reuse lives in its implementation)
|
||||
- tasks/http/client/shared-http-client.md — `SharedHttpClient` (the
|
||||
shared reqwest client both adapters use)
|
||||
- tasks/call/client/from-jsonschema.md — the old task that built the
|
||||
broken placeholder (superseded by this task)
|
||||
|
||||
## Notes
|
||||
|
||||
> This is a contained move, not new architecture. ADR-066 already
|
||||
> landed the architecture; the spec docs already reflect the move.
|
||||
> The implementation is: (1) write the real adapter in `alknet-http`
|
||||
> reusing `from_openapi`'s forwarding logic (`build_request` /
|
||||
> `forward` / `forward_stream` — make them `pub(crate)` or factor a
|
||||
> shared `adapters/forwarding.rs`, whichever is less churn), (2) delete
|
||||
> the broken placeholder from `alknet-call`, (3) keep the
|
||||
> `FromJsonSchema` provenance variant in `registration.rs`. The handler
|
||||
> shape is identical to `from_openapi` — do not duplicate the
|
||||
> forwarding logic. The difference between the two adapters is purely
|
||||
> the input shape: `from_openapi` parses a full OpenAPI doc and
|
||||
> iterates `(path, method)` pairs; `from_jsonschema` takes one
|
||||
> `(path_template, method)` from the caller and produces one bundle.
|
||||
> Risk is low because the forwarding logic is already tested in
|
||||
> `from_openapi`; the new code is the thin `FromJsonSchema` adapter
|
||||
> struct + `import()` + wiring.
|
||||
Reference in new issue
Block a user