Files
alknet/tasks/http/server/gateway-endpoints.md
glm-5.2 e855c8c7eb docs(http): decompose alknet-http spec into 19 implementation tasks
Break the alknet-http architecture spec into atomic, dependency-ordered
tasks in tasks/http/, following the taskgraph frontmatter conventions
used by the call/core/vault crates.

Tasks span 7 phases across 5 module subdirectories (server/, gateway/,
client/, adapters/, websocket/):
- Phase 0: crate-init (foundation)
- Phase 1: gateway-dispatch-spine, error-mapping, shared-http-client
  (shared infrastructure)
- Phase 2: http-adapter, bearer-auth-middleware, gateway-endpoints,
  healthz-decoy (HTTP server surface)
- Phase 3: to-openapi (OpenAPI gateway projection)
- Phase 4: from-openapi (OpenAPI adapter, reqwest forwarding)
- Phase 5: dispatcher-transport-abstraction, upgrade-handler,
  connection-overlay (WebSocket browser bidirectional path)
- Phase 6: from-mcp, to-mcp (MCP adapters, feature-gated)
- Phase 7: review-http, review-websocket, review-mcp, review-http-final
  (quality checkpoints)

The gateway-dispatch-spine task implements the thin shared core
recommended by the gateway-factoring research (concrete struct, not a
trait). The dispatcher-transport-abstraction task is a cross-crate
change to alknet-call (exposes EventEnvelope-level dispatch API for
non-QUIC transports) — the highest-risk task. WebTransport/h3 is
deferred per ADR-044 and has no tasks; from_wss is out of scope.

Validated: 19 tasks, no cycles, 8 parallel generations, critical path
length 8 (through the WebSocket strand).
2026-07-01 07:11:17 +00:00

10 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
http/server/gateway-endpoints Implement 5 gateway endpoints (search/schema/call/batch/subscribe) — axum route handlers pending
http/server/http-adapter
http/gateway/gateway-dispatch-spine
http/gateway/error-mapping
http/server/bearer-auth-middleware
broad medium component implementation

Description

Implement the 5 fixed gateway endpoints in src/server/gateway_routes.rs. These are the sole invoke path over HTTP (ADR-042, ADR-047): an HTTP client invokes an operation via POST /call with { "operation": "/fs/readFile", "input": {...} }, discovers what it can call via the AccessControl-filtered GET /search, and learns an operation's shape via GET /schema. There is no per-operation POST /{service}/{op} direct-call surface — the gateway is the invoke path (ADR-047 supersedes ADR-036's direct-call surface).

The 5 endpoints (http-server.md §"HTTP-to-call dispatch", http-adapters.md §"The gateway endpoint set")

Endpoint Call protocol HTTP method Purpose
/search services/list GET List/search operations (AccessControl-filtered). Names + descriptions.
/schema services/schema GET Get an operation's full OperationSpec.
/call call.requested (Query/Mutation) POST Invoke an operation. Flat JSON body { operation, input }.
/batch multiple call.requested POST Invoke multiple operations. Array of { operation, input }.
/subscribe call.requested (Subscription) POST (SSE) Invoke a streaming operation. Body { operation, input }; response text/event-stream.

POST /call dispatch (http-server.md §"HTTP-to-call dispatch")

  1. The axum route handler reads the JSON body { "operation": "/fs/readFile", "input": {...} }.
  2. Resolves the caller's identity from the Authorization: Bearer header (via the shared bearer_auth_middleware — stashed in extensions as ResolvedIdentity).
  3. Calls GatewayDispatch::invoke(identity, operation, input) — the shared dispatch spine (the gateway-dispatch-spine task). This builds the root OperationContext (internal: false, forwarded_for: None) and dispatches through OperationRegistry::invoke().
  4. The response (ResponseEnvelope) is serialized as the HTTP response body (JSON). Errors map to HTTP status codes via the error-mapping task (call_error_to_http_response).

Internal operations (ADR-015) return 404 (NOT_FOUND) — the gateway dispatches only External operations, and the caller discovers which External operations it can call via the AccessControl-filtered /search endpoint. This is the per-caller API surface property: an HTTP client cannot stub its toe on a path for an operation it can't call, because there is no per-operation path — /search tells it what it can call, /call invokes it, and the AccessControl check runs on /call regardless.

GET /search (AccessControl-filtered discovery)

Dispatches services/list through GatewayDispatch::invoke() with the resolved caller identity. The services/list handler (already in OperationRegistry) filters by AccessControl::check(identity) — the client sees only the operations it is authorized to call. Returns operation names + descriptions (not full schemas). Query parameters for filtering/searching are a two-way-door extension (the v1 shape is "list all I can call"; search/filter sugar is additive).

GET /schema

Dispatches services/schema through GatewayDispatch::invoke() with the resolved caller identity. Returns the operation's full OperationSpec (input/output JSON Schemas, error schemas). The AccessControl check runs (an unauthorized caller gets FORBIDDEN, not the schema).

POST /batch

Follows the same dispatch path as /call with an array of { operation, input } pairs (OQ-14). batch is a loop over GatewayDispatch::invoke() in the gateway (research §6 open question #3 — confirm batch is genuinely just a loop, no shared batch-specific state, no transactional semantics). Returns an array of results (or errors), one per entry, in order.

POST /subscribe (SSE streaming projection)

A Subscription operation invoked via the gateway's POST /subscribe endpoint projects its call.responded stream as Server-Sent Events. The request body is { operation, input } (the same flat JSON shape as /call); the response is text/event-stream (negotiated via Accept: text/event-stream on the POST). The axum route handler:

  • Sets Content-Type: text/event-stream.
  • For each call.responded event, writes an SSE data: frame (the event's output serialized as JSON).
  • On call.completed, closes the SSE stream (normal end).
  • On call.aborted, closes the stream with an SSE error event.
  • On HTTP client disconnect (detected as the response writer closing), sends call.aborted for the in-flight subscription, which cascades to descendants per ADR-016.

This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket (websocket.md), the subscription projects directly onto the WS connection — call.responded events as binary WS messages, no SSE framing. WebTransport (h3, deferred per ADR-044) would project onto WebTransport bidirectional streams.

One-directional projection (http-server.md §"One-directional projection")

The HTTP/1.1 + HTTP/2 surface is a lossy, one-directional projection of the call protocol. HTTP is request/response: the client initiates, the server responds. The call protocol is bidirectional — both sides can initiate calls. The HTTP projection carries only the client→server call direction; the server→client call direction has no HTTP expression. Subscription streaming is the one partial exception — the server streams call.responded frames back over the SSE response — but even there, the call is client-initiated; only the results flow server→client. WebSocket restores the bidirectional call model for browsers (the websocket/ tasks).

Constraints

  • The gateway is the sole invoke path over HTTP (ADR-042, ADR-047). No per-operation POST /{service}/{op} direct-call surface.
  • External operations only. Internal operations return 404 on the gateway's /call, matching the call protocol's NOT_FOUND.
  • Bearer-only auth. Via the shared bearer_auth_middleware.
  • No secret material in HTTP responses. Capabilities are used for outbound calls (from_openapi), never serialized into HTTP response bodies (ADR-014).

Acceptance Criteria

  • POST /call route handler reads { operation, input } JSON body
  • /call resolves identity via ResolvedIdentity extractor (shared middleware)
  • /call dispatches via GatewayDispatch::invoke(identity, operation, input)
  • /call response is ResponseEnvelope serialized as JSON
  • /call errors mapped via call_error_to_http_response (error-mapping task)
  • Internal op on /call404 NOT_FOUND
  • External op with AccessControl restrictions + unauthorized → 403 FORBIDDEN
  • External op with AccessControl restrictions + no identity → 401
  • GET /search dispatches services/list via GatewayDispatch::invoke
  • /search results are AccessControl::check(identity)-filtered
  • /search returns operation names + descriptions (not full schemas)
  • GET /schema dispatches services/schema via GatewayDispatch::invoke
  • /schema returns the operation's full OperationSpec
  • /schema for unauthorized op → 403 FORBIDDEN
  • POST /batch dispatches an array of { operation, input } via loop over invoke
  • /batch returns an array of results (or errors), one per entry, in order
  • POST /subscribe sets Content-Type: text/event-stream
  • /subscribe writes call.responded events as SSE data: frames
  • /subscribe closes stream on call.completed
  • /subscribe writes SSE error event on call.aborted
  • /subscribe sends call.aborted on HTTP client disconnect (ADR-016 cascade)
  • No per-operation POST /{service}/{op} direct-call surface (ADR-047)
  • No secret material in HTTP response bodies (ADR-014)
  • Integration test: /call round-trip (External op → 200 + JSON body)
  • Integration test: /call Internal op → 404
  • Integration test: /call unauthorized → 403
  • Integration test: /call unauthenticated + restricted op → 401
  • Integration test: /search returns only AccessControl-allowed ops
  • Integration test: /schema returns full spec for authorized op
  • Integration test: /batch returns array of results in order
  • Integration test: /subscribe streams SSE events until completed
  • Integration test: /subscribe client disconnect → abort cascade
  • cargo test -p alknet-http succeeds
  • cargo clippy -p alknet-http --all-targets succeeds with no warnings

References

  • docs/architecture/crates/http/http-server.md — HTTP-to-call dispatch, SSE projection, one-directional projection
  • docs/architecture/crates/http/http-adapters.md — The gateway endpoint set, per-caller API surface
  • docs/architecture/decisions/042-openapi-gateway-pattern.md — ADR-042 (5 fixed gateway endpoints)
  • docs/architecture/decisions/047-remove-direct-call-http-surface.md — ADR-047 (gateway is sole invoke path)
  • docs/architecture/decisions/015-privilege-model-and-authority-context.md — ADR-015 (Internal → 404)
  • docs/architecture/decisions/016-abort-cascade-for-nested-calls.md — ADR-016 (disconnect → abort cascade)
  • docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md — ADR-014 (no secrets in responses)

Notes

The 5 gateway endpoints are the sole HTTP invoke path (ADR-047). The /call handler delegates to GatewayDispatch::invoke (the shared spine); the error mapping is the error-mapping task; the auth is the shared bearer-auth-middleware. /subscribe is the SSE streaming projection — the one to_openapi-specific piece that does not go through the shared spine's request/response invoke (research §6 open question #5 — /subscribe is to_openapi-owned, not in the shared core). /batch is a loop over invoke (research §6 open question #3 — confirm no batch-specific shared state). The one-directional projection is a structural property of HTTP; WebSocket restores bidirectionality for browsers.

Summary

To be filled on completion