13 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| http/adapters/from-openapi | Implement from_openapi adapter (parse OpenAPI, reqwest forwarding handlers, no-env-vars injection) | completed |
|
broad | medium | component | implementation |
Description
Implement from_openapi in src/adapters/from_openapi.rs. This is the
OpenAPI-direction adapter: it parses an OpenAPI document, constructs a
HandlerRegistration bundle per OpenAPI operation with a forwarding
handler that calls the external HTTP endpoint via reqwest, and returns
the bundles for registration in the OperationRegistry. The adapter
implements OperationAdapter (the async trait from alknet-call,
ADR-017 §5).
The adapter (http-adapters.md §"from_openapi")
pub struct FromOpenAPI {
spec: OpenAPISpec,
config: HttpServiceConfig,
}
#[async_trait]
impl OperationAdapter for FromOpenAPI {
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
}
Type definitions (http-adapters.md §"Type definitions")
/// A parsed OpenAPI document. The concrete type is a two-way-door
/// implementation detail (openapiv3::OpenApi, a local alknet-http type,
/// or a serde_json::Value-based parse); the one-way constraint is that
/// from_openapi accepts a standard OpenAPI 3.x JSON/YAML doc and to_openapi
/// produces one. Both directions share the same Rust type, but not the
/// same document shape. Coordinate with the to-openapi task on the type.
pub struct OpenAPISpec {
pub info: OpenAPIInfo,
pub paths: BTreeMap<String, PathItem>,
pub components: Option<Components>,
// ... OpenAPI 3.x fields as needed
}
/// Configuration for an HTTP-backed adapter (from_openapi). Carries the
/// base URL, auth scheme (from Capabilities at registration, not env vars),
/// and optional headers. The auth field is the scheme the external API
/// expects; the credential itself is read from
/// OperationContext.capabilities at call time, not stored here.
pub struct HttpServiceConfig {
pub namespace: String,
pub base_url: String,
pub auth: Option<HttpAuthScheme>,
pub default_headers: HashMap<String, String>,
}
pub enum HttpAuthScheme {
Bearer, // Authorization: Bearer <token>
ApiKey { header_name: String }, // e.g., X-API-Key: <key>
Basic, // Authorization: Basic <credentials>
}
The import flow (http-adapters.md §"from_openapi")
The adapter:
-
Parses the OpenAPI document (
OpenAPISpec—paths,components,$refresolution). On parse failure, returnsAdapterError::SchemaParse. The TS prior art (@alkdev/operations/src/from_openapi.ts) shows the parsing patterns:resolveReffor$ref,resolveRefsRecursivefor nested refs,buildInputSchema(parameters + request body → input JSON Schema),buildOutputSchema(200/201 response → output JSON Schema),detectOperationType(SSE response →Subscription, GET →Query, elseMutation). -
For each
(path, method, operation)inspec.paths, constructs aHandlerRegistration:spec.name= theoperationId(or a generated${method}_${path_parts}name ifoperationIdis absent — same normalization as the TSnormalizeOperationId).spec.namespace= theconfig.namespace(the importing deployment's name for the service, not the OpenAPI doc'sinfo.title).spec.op_type=Query/Mutation/Subscription(detected from the method + response content type, same as TS).spec.visibility=Internal(adapter-registered ops are composition material, not directly callable from the wire — ADR-015).spec.input_schema/output_schema= the JSON Schemas built from the OpenAPI parameters/responses.spec.error_schemas= theErrorDefinitions built from the non-2xx OpenAPI responses (ADR-023 §5 — see Error Fidelity below).spec.access_control=AccessControl::default()(the adapter doesn't declare scopes; the composing handler that reaches the imported op gates access).handler= a forwarding handler (see Forwarding Handler below).provenance=FromOpenAPI,composition_authority: None,scoped_env: None(leaf — ADR-022).capabilities= the credentials the forwarding handler needs (the bearer token / API key for the external HTTP endpoint, injected by the assembly layer at registration — see No-Env-Vars below).
-
Returns the bundles. The caller (the assembly layer) registers them in the
OperationRegistry.
Forwarding handler (http-adapters.md §"Forwarding handler")
The forwarding handler is the Arc<dyn Handler> stored in the
HandlerRegistration. At call time, it:
- Reads the call input (
serde_json::Value). - Builds the outbound HTTP request:
- URL path: substitutes path parameters (
{id}→ input value), appends query parameters from input fields not in the path. - Method: the OpenAPI operation's method.
- Headers:
Content-Type: application/json+ the auth header built fromcontext.capabilities(see No-Env-Vars below). - Body: the
bodyfield of the input (forMutation/Subscription).
- URL path: substitutes path parameters (
- Sends the request via the shared HTTP client (
SharedHttpClient— theshared-http-clienttask). - For a
Query/Mutation: parses the response body (JSON, text, or binary — same content-type branching as the TScreateHTTPOperation), wraps it in aResponseEnvelope, returns. - For a
Subscription(text/event-streamresponse): streamscall.respondedevents as the SSE chunks arrive (same SSE parsing as the TSparseSSEFrames), thencall.completedon stream end. - On HTTP error (non-2xx): maps to the declared
ErrorDefinitionby HTTP status code (see Error Fidelity below), returns aCallError.
The handler is opaque to the CallAdapter — it's an Arc<dyn Handler>
the registry dispatches. alknet-call never sees reqwest.
No-Env-Vars credential injection (http-adapters.md §"No-Env-Vars credential injection")
The forwarding handler is the credential injection point for the
no-env-vars architecture. The handler reads
context.capabilities.get("<service>") (e.g., "openai", "vastai",
"github"), extracts the credential, and injects it into the outbound
HTTP request:
- Bearer token →
Authorization: Bearer <token>. - API key → the header the OpenAPI spec declares (e.g.,
X-API-Key: <key>, orAuthorization: ApiKey <key>— theHTTPServiceConfig.authin the TS prior art shows the three auth types:bearer,apiKey,basic). - Basic auth →
Authorization: Basic <credentials>.
The credential comes from Capabilities, which was populated by the
dispatch path from the HandlerRegistration.capabilities bundle
(ADR-022 §6), which was populated by the assembly layer from the vault
(ADR-014). The handler never reads std::env::var. This is the
spec-level invariant: no handler reads outbound credentials from any
source other than OperationContext.capabilities.
Error Fidelity (ADR-023, http-adapters.md §"Error Fidelity")
from_openapi maps OpenAPI non-2xx response status codes to
ErrorDefinitions (ADR-023 §5). The normative rule (review #002 W20):
from_openapi must not produce error codes that collide with the five
protocol-level codes (NOT_FOUND, FORBIDDEN, INVALID_INPUT,
INTERNAL, TIMEOUT). The adapter prefixes imported error codes with
HTTP_ and the status number:
// OpenAPI: 404: { schema: NotFoundError }
// → ErrorDefinition { code: "HTTP_404", http_status: Some(404), schema: NotFoundError }
Acceptance Criteria
FromOpenAPIstruct withspec: OpenAPISpec,config: HttpServiceConfigOperationAdapterimpl:async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>- Parses OpenAPI doc (
paths,components,$refresolution) - Parse failure →
AdapterError::SchemaParse - For each
(path, method, operation): constructsHandlerRegistration spec.name=operationId(or generated${method}_${path_parts})spec.namespace=config.namespacespec.op_type=Query/Mutation/Subscription(detected from method + response content type)spec.visibility=Internal(ADR-015)spec.input_schema/output_schemafrom OpenAPI parameters/responsesspec.error_schemasfrom non-2xx OpenAPI responses withHTTP_<status>prefix (ADR-023)spec.access_control=AccessControl::default()provenance=FromOpenAPI,composition_authority: None,scoped_env: None(ADR-022)capabilities= credentials for the external endpoint (injected at registration)- Forwarding handler builds outbound HTTP request (path params, query, headers, body)
- Forwarding handler sends via
SharedHttpClient(the shared client) Query/Mutation: parses response body (JSON/text/binary), wraps inResponseEnvelopeSubscription(text/event-stream): streamscall.respondedfrom SSE chunks, thencall.completed- HTTP error (non-2xx): maps to declared
ErrorDefinitionby status code, returnsCallError - No-env-vars: handler reads
context.capabilities.get("<service>"), neverstd::env::var(ADR-014) - Bearer/ApiKey/Basic auth injection from
Capabilities HttpAuthSchemeenum withBearer,ApiKey { header_name },BasicHttpServiceConfigwithnamespace,base_url,auth,default_headers- Unit test: parse a minimal OpenAPI doc → one
HandlerRegistration - Unit test: parse failure →
AdapterError::SchemaParse - Unit test:
operationIdabsent → generated name${method}_${path_parts} - Unit test: GET →
Query, POST →Mutation, SSE response →Subscription - Unit test: error response 404 →
ErrorDefinition { code: "HTTP_404", http_status: Some(404) } - Unit test: forwarding handler injects Bearer token from
context.capabilities - Integration test: forwarding handler calls external endpoint via
SharedHttpClient - Integration test: SSE response streams
call.respondedevents - Integration test: no
std::env::varreads in the forwarding handler cargo test -p alknet-httpsucceedscargo clippy -p alknet-http --all-targetssucceeds with no warnings
References
- docs/architecture/crates/http/http-adapters.md — from_openapi (full spec)
- docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md — ADR-017 §5 (OperationAdapter trait)
- docs/architecture/decisions/015-privilege-model-and-authority-context.md — ADR-015 (Internal visibility)
- docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md — ADR-022 (leaf provenance)
- docs/architecture/decisions/023-operation-error-schemas.md — ADR-023 (HTTP_ prefix, error fidelity)
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md — ADR-014 (no env vars)
- /workspace/@alkdev/operations/src/from_openapi.ts — TypeScript prior art (parsing, SSE, auth headers, createHTTPOperation, parseSSEFrames)
Notes
from_openapi is the no-env-vars credential injection point. The forwarding handler reads context.capabilities, not std::env::var — this is the spec-level invariant (ADR-014). The handler is opaque to CallAdapter (Arc); alknet-call never sees reqwest. The error codes are prefixed HTTP_ to avoid collision with protocol-level codes (ADR-023, review #002 W20). The OpenAPISpec type is shared with to_openapi (coordinate on the type); the shape is not (from_openapi consumes per-operation-paths, to_openapi produces the 5-endpoint gateway doc). The TS prior art (@alkdev/operations/src/ from_openapi.ts) shows the parsing patterns (resolveRef, buildInputSchema, buildOutputSchema, detectOperationType, createHTTPOperation, parseSSEFrames) — the SSE normalization patterns stay referenced, the client construction anti-patterns (env-var config, hand-rolled retry) are discarded.
Summary
Implemented FromOpenAPI in src/adapters/from_openapi.rs. OpenAPISpec/HttpServiceConfig/ HttpAuthScheme types, $ref resolution, OperationAdapter::import() producing HandlerRegistration bundles (Internal visibility, FromOpenAPI provenance, leaf per ADR-022, HTTP_ error codes per ADR-023). Reqwest forwarding handlers read credentials from OperationContext.capabilities (no-env-vars ADR-014) via SharedHttpClient. JSON/text/binary response branching, SSE subscription streaming via parseSSEFrames, Bearer/ApiKey/Basic auth injection. 98 new tests (unit + integration with in-process TCP echo/capturing servers). Build/clippy/test all clean. 111 total tests pass.