diff --git a/docs/architecture/README.md b/docs/architecture/README.md index d8a2089..1198768 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 diff --git a/docs/architecture/crates/call/README.md b/docs/architecture/crates/call/README.md index e39acc0..58c2722 100644 --- a/docs/architecture/crates/call/README.md +++ b/docs/architecture/crates/call/README.md @@ -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). \ No newline at end of file diff --git a/docs/architecture/crates/call/client-and-adapters.md b/docs/architecture/crates/call/client-and-adapters.md index d063120..09fb495 100644 --- a/docs/architecture/crates/call/client-and-adapters.md +++ b/docs/architecture/crates/call/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` | diff --git a/docs/architecture/crates/call/operation-registry.md b/docs/architecture/crates/call/operation-registry.md index 1ba56eb..3d1ccad 100644 --- a/docs/architecture/crates/call/operation-registry.md +++ b/docs/architecture/crates/call/operation-registry.md @@ -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 | diff --git a/docs/architecture/crates/http/README.md b/docs/architecture/crates/http/README.md index 2c1ac3d..a4d4dd7 100644 --- a/docs/architecture/crates/http/README.md +++ b/docs/architecture/crates/http/README.md @@ -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_` 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_` 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` | | [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 diff --git a/docs/architecture/crates/http/http-adapters.md b/docs/architecture/crates/http/http-adapters.md index e2fa608..43cadba 100644 --- a/docs/architecture/crates/http/http-adapters.md +++ b/docs/architecture/crates/http/http-adapters.md @@ -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, +} + +#[async_trait] +impl OperationAdapter for FromJsonSchema { + async fn import(&self) -> Result, 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_` 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_`.** 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`; `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`; `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_` prefix rule - [overview.md](overview.md) — adapter location map, no-env-vars diff --git a/docs/architecture/crates/http/overview.md b/docs/architecture/crates/http/overview.md index 4322dec..ef52f35 100644 --- a/docs/architecture/crates/http/overview.md +++ b/docs/architecture/crates/http/overview.md @@ -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` 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` 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), diff --git a/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md b/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md index 13b1ce4..4e860e1 100644 --- a/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md +++ b/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md @@ -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. \ No newline at end of file +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". \ No newline at end of file diff --git a/docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md b/docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md index fc7671c..b7cb72d 100644 --- a/docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md +++ b/docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md @@ -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` diff --git a/docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md b/docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md new file mode 100644 index 0000000..2623bed --- /dev/null +++ b/docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md @@ -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_` +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_` 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`). \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index b390d09..e0cf497 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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)). diff --git a/tasks/call/client/from-jsonschema.md b/tasks/call/client/from-jsonschema.md index f42281d..d9819c4 100644 --- a/tasks/call/client/from-jsonschema.md +++ b/tasks/call/client/from-jsonschema.md @@ -7,6 +7,7 @@ scope: narrow risk: low impact: isolated level: implementation +superseded_by: [http/adapters/from-jsonschema] --- ## Description diff --git a/tasks/http/adapters/from-jsonschema.md b/tasks/http/adapters/from-jsonschema.md new file mode 100644 index 0000000..9528d5f --- /dev/null +++ b/tasks/http/adapters/from-jsonschema.md @@ -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, +} + +impl FromJsonSchema { + pub fn new( + spec: OperationSpec, + config: HttpServiceConfig, + path_template: String, + method: String, + http_client: Arc, + ) -> Self; +} + +#[async_trait] +impl OperationAdapter for FromJsonSchema { + async fn import(&self) -> Result, 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_` 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_` (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_` 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_`) +- [ ] 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_` 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. \ No newline at end of file