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:
glm-5.2 committed 2026-07-09 11:42:56 +00:00
1 parent 06ab5db459
commit 97456bc608
13 files changed
+739 -95

No files matched your search

+20 -4
View File
@@ -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
+5 -4
View File
@@ -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 |
+5 -4
View File
@@ -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
+111 -12
View File
@@ -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
+17 -12
View File
@@ -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".
@@ -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`).
+1 -1
View File
@@ -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)).
+1
View File
@@ -7,6 +7,7 @@ scope: narrow
risk: low
impact: isolated
level: implementation
superseded_by: [http/adapters/from-jsonschema]
---
## Description
+280
View File
@@ -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.