Repository navigation
feat(supervisor): add streaming HTTP request middleware evaluation #2431
Description
Activity
- addedtopic:l7Application-layer policy and inspection workApplication-layer policy and inspection workarea:supervisorProxy and routing-path workProxy and routing-path work
on Jul 22, 2026 - added a parent issue
on Jul 22, 2026 This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
- addedstate:staleInactive item at risk of automatic closure.Inactive item at risk of automatic closure.
on Aug 6, 2026 - removedstate:staleInactive item at risk of automatic closure.Inactive item at risk of automatic closure.
on Sep 18, 2026 🏗️ build-plan
Summary
Replace the current unary and WIP four-mode HTTP body contracts with the approved two-mode, fail-closed protocol. PR #3450 will contain only the generic hook, runtime, migration, ordinary middleware consumers, tests, and documentation. SigV4 and Git signing move to two later built-in middleware PRs.
Issue type:
feat
Complexity: High
Confidence: MediumThis intentionally breaks the pre-0.1 HTTP middleware API and closes #3307. It advances #2431, but does not close it because inspected HTTP/2 remains separate work.
Stack and scope
- PR refactor(middleware)!: adopt two-mode HTTP body protocol #3450: shared HTTP hook and runtime.
- SigV4 built-in PR based on refactor(middleware)!: adopt two-mode HTTP body protocol #3450.
- Git-signing built-in PR based on the SigV4 branch.
The base PR will restore the existing inline SigV4 implementation from
main. It will remove the WIP SigV4 relocation, publicPOST_CREDENTIALSphase, and external Git-signing example. The later Git signer will be a trusted built-in, not a remote example.Ownership and boundaries
proto/supervisor_middleware.proto: shared HTTP event/result contract, binding capabilities, and protocol version.openshell-supervisor-middleware: negotiation, validation, bounded BUFFERED handling, independent STREAM input/output pumps, chain composition, diagnostics, cancellation, and transport adapters.openshell-supervisor-network: HTTP/1 request and response integration, commitment boundaries, body-aware policy gates, and bounded delivery. It must not spool middleware input or output to disk.openshell-policy*: migration validation for HTTPfail_open.- Built-ins and examples: only regex and content-guard migration in this PR.
- Existing inline SigV4 stays in
openshell-supervisor-networkuntil the stacked SigV4 PR.
Implementation plan
- Replace request and response body messages with one shared lifecycle: Preflight, Begin, BUFFERED body/result, STREAM input/output, Finish, and Reject. Continue is the header-only success path. Preserve context, admitted target, config, authentication, diagnostics, findings, metadata, trailers, cancellation, and
MiddlewareSessionEnd. - Remove request/response sequence numbers, per-unit results, ownership acknowledgements, finalization counters,
skip_remaining,OWNED_STREAM_BYTES, and lockstepSTREAM_BYTES. Unknown or unset oneofs, unoffered modes, premature Finish, and RPC close without a terminal success fail closed. - Add an explicit HTTP protocol version and supported body modes to
MiddlewareBinding. Use a new HTTP evaluation RPC method path so old and new response contracts cannot decode each other accidentally. Reject missing or unsupported HTTP capabilities before activation. - Implement request BUFFERED with bounded RAM and STREAM with independent, bounded input and output pumps. OpenShell keeps no replay original and creates no middleware tempfile. Enforce chunk limits, queue byte/message limits, total limits when present, idle/session deadlines, output-length checks, and cancellation.
- Compose STREAM and BUFFERED stages without deadlock. A later BUFFERED stage may keep consuming earlier output while it holds delivery. This base rollout will not offer late header mutations; the SigV4 stack will enable the trusted finalizer path. Later stages therefore see the final validated preflight head.
- Remove
RequestBodySpool,MiddlewareRequestBody::Spool, ownership draining, and generic whole-chain tempfile paths. Keep body-aware GraphQL, JSON-RPC, and MCP checks before exposure by using the existing bounded in-memory policy gate or rejecting an incompatible combination. - Use the same schema for responses. In refactor(middleware)!: adopt two-mode HTTP body protocol #3450, advertise Continue and BUFFERED only. Preserve bodyless, HEAD, encoded, range, no-transform, protected-header, framing, trailer, commitment, and cancellation behavior. Do not relabel the old lockstep response runner as duplex STREAM.
- Make HTTP middleware failures mandatory fail-closed. Keep the stored
on_errorfield for migration. Accept empty or explicitfail_closed; rejectfail_openwith an actionable error when a selected implementation advertises an HTTP request or response binding. Preserve WebSocket-onlyfail_openbehavior. Gateway interceptors are unchanged. - Restore inline SigV4 ownership and LocalStack coverage from
main. Remove the WIP built-in SigV4 move, its dependencies and docs,post_credentials.rs, and the public post-credentials phase. Removeexamples/supervisor-middleware-git-signing/**and its task entries. - Update regex, content guard, RFC 0009, architecture, published middleware and policy docs, and the related public skills. Run the agent-infrastructure consistency checks required by the maintenance map.
Verification
- Protocol negotiation, old/new rejection, unknown/unset events, unoffered modes, and diagnostics bounds.
- BUFFERED empty, unchanged, replacement, deletion, overflow, unknown length, trailers, and forbidden late headers.
- STREAM arbitrary input/output cardinality, output before EOF, delayed output start, empty input/output, premature or missing Finish, RPC close, and length mismatch.
- Slow readers and writers, simultaneous backpressure, queue bounds, cancellation, disconnect, policy reload, early upstream response, and idle/session timeout.
- STREAM to STREAM, STREAM to BUFFERED, BUFFERED to STREAM, and multiple BUFFERED stages.
- Fixed-length, chunked, trailers,
Expect: 100-continue, bodyless requests, and failure before versus after commitment. - GraphQL, JSON-RPC, and MCP validation before later-stage or upstream exposure.
- HTTP
fail_openmigration rejection and WebSocket-only compatibility. - Response Continue/BUFFERED eligibility and proof that response STREAM is not offered.
- No generic middleware tempfile or recovery-original path.
- Inline SigV4 and LocalStack parity after the split.
- Focused crate tests while iterating, then
mise run test,mise run pre-commit,mise run ci, and the relevant Docker proxy E2E lane.
Risks and decisions
- Response STREAM is intentionally deferred until the response relay has the same independent-pump semantics. This is a visible pre-0.1 contract change and will be documented.
- The shared attachment-level
on_errorfield means a service that advertises both HTTP and WebSocket cannot usefail_open; a WebSocket-only service can. - Queue ceilings and timeout defaults will use existing platform limits unless live tests show they need a narrower bound.
- The signer stacks may add trusted late-header authority later. The public remote hook remains credential-blind.
Issue disposition
- PR refactor(middleware)!: adopt two-mode HTTP body protocol #3450:
Closes #3307andPart of #2431. - Do not close feat(supervisor): add streaming HTTP request middleware evaluation #2431 until inspected HTTP/2 and per-stream lifecycle requirements are complete.
Revision 2: replace the superseded four-mode design, remove signer work from the base PR, and define the three-PR stack.
Revision 1: initial streaming request hook plan.This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
- addedstate:staleInactive item at risk of automatic closure.Inactive item at risk of automatic closure.
on Oct 3, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsPlanning
Problem Statement
Parent: #1733
The v1 HTTP request hook,
SupervisorMiddleware.EvaluateHttpRequest, is unary. The supervisor buffers the whole request body, builds oneHttpRequestEvaluation, and sends it to middleware. Request and replacement bodies are capped at 4 MiB.That works for small, full-body inspection. It does not work for:
HTTP/2 support (#2426) also needs middleware to run per logical HTTP stream, not on raw connection bytes. The streaming gRPC part of #2166 needs method, metadata, body, duration, rate, and cancellation controls without making middleware implement HTTP/2 framing.
Status
PR #4359 delivers the middleware contract half of this issue: the v2 HTTP hooks, for both requests and responses, over HTTP/1.x. The HTTP/2 half is still open. Today h2c and other traffic the supervisor can't parse reach v2 middleware only as an allow-or-refuse decision, described below.
Design (as implemented in #4359)
Contract
Two new bidirectional streaming RPCs share one set of event and result messages:
SupervisorMiddleware.EvaluateHttpRequestV2forHTTP_REQUEST_V2atPRE_CREDENTIALS.SupervisorMiddleware.EvaluateHttpResponseV2forHTTP_RESPONSE_V2atPRE_RETURN.The supervisor opens one stream per middleware stage and HTTP message. A request and its response use separate streams.
Each stream starts with a preflight carrying the head (target, headers, and status for responses), request context, the policy
config, the permitted body modes, the modes OpenShell removed and why, the limits, and the declared body length when known. The stage answers with one of:continue_without_body, with optional header mutations. The stage is done and later stages still see the body.inspectwith BUFFERED or STREAM.reject.The supervisor runs every stage's preflight in chain order before any stage gets a body byte.
Body modes
buffered_bodywith the full body and trailers, up to the stage's payload limit (max 4 MiB). The stage returnsunchangedor areplacement, plus late header and trailer mutations. Nothing goes out until the whole chain approves.input_chunkevents (64 KiB max each) and oneinput_endwith trailers. The stage writes its own output:output_start, any number ofoutput_chunk, thenfinish. Input and output chunk counts are independent. No total size or time limit. A stage that stalls for 30 seconds fails.For responses, the supervisor removes modes it can't honour and says why:
BODYLESS,PARTIAL,NO_TRANSFORM,ENCODED,TRUNCATION_UNDETECTABLE,OPEN_ENDED,OVER_LIMIT. For requests it removes BUFFERED when the declared length is over the stage limit.Request delivery
Request output streams to the upstream while the sandbox is still uploading. The supervisor withholds output (up to 4 MiB) and sends it with the head when the route needs the whole body: a BUFFERED stage, SigV4 signing, request body credential rewrite, the forward proxy, JSON-RPC/MCP/GraphQL endpoints, HTTP/1.0, or
Upgrade. Body-aware endpoints re-check every replaced body against policy before the next stage or the upstream sees it. Credentials are injected after all middleware mutations.If a STREAM stage fails after the head reached the upstream, the supervisor closes that upstream connection. It never replays a partly forwarded request.
Traffic the supervisor can't inspect
For each entry that selects a connection the supervisor can't show as HTTP, and whose service binds
HTTP_REQUEST_V2, the supervisor opens a request exchange with anuninspectablepreflight. Reasons areTLS_SKIP,H2C,UNSUPPORTED_TUNNEL,RAW_TCP, andSQL_PASSTHROUGH. No body mode is offered.continue_without_bodyallows the connection and anything else refuses it. That's why a policy can now attach v2 request middleware totls: skipendpoints.Failure behaviour
v2 hooks always fail closed. The gateway rejects
on_error: fail_openon entries whose service runs v2 hooks. Requests get403withmiddleware_deniedormiddleware_failed. Responses get403/502before the head is committed, and an aborted delivery plus a high-severity finding after.Version selection and compatibility
We dropped the capability-advertisement idea from the original proposal. The binding operation picks the hook version. Gateways and supervisors that predate v2 don't know
HTTP_REQUEST_V2/HTTP_RESPONSE_V2and refuse the service atDescribe, so a v2 service never gets called through a v1 RPC.We also dropped the unary-to-stream adapter and the single shared chain runner:
middleware_hook_versions_mixed.Remaining work: HTTP/2
#2426 tracks the transport gap: the L7 proxy only negotiates HTTP/1.1, so HTTP/2 needs
tls: skipand skips L7 inspection. With v2 hooks, middleware can at least refuse that traffic, but it can't see it.The v2 contract carries no HTTP/1-specific framing, so an HTTP/2 request stream should map onto the same exchange: one stream per stage and logical HTTP request. What's left:
tls: skip.TRUNCATION_UNDETECTABLEcurrently removes STREAM for anything that isn't HTTP/1.1.The middleware API must not expose HTTP/2 frames, HPACK state, or stream IDs.
#2166
v2 hooks give the gRPC part of #2166 a base at the HTTP stream level: host, service, and method admission through authority and path, metadata limits, request byte, rate, duration, and concurrency limits, and cancellation and audit. gRPC message boundaries and protobuf parsing would need their own typed operation. Generic body chunks are not gRPC messages.
WebSocket middleware is tracked by #2428 and works the same under both hook versions.
Alternatives Considered
Advertise capabilities in the manifest. The original proposal. We picked binding operations instead. Old peers reject unknown operations, so there is no silent fallback, and there is nothing extra to negotiate.
Run v1 through an adapter on one shared runner. More moving parts in the supervisor to keep alive an API we plan to remove. Separate engines plus a no-mixing rule are simpler and easier to delete in 0.2.0.
Replace v1 right away. v1 was already public in 0.1.x. Deprecation with a migration guide gives services a release to move.
Add an HTTP/2-specific middleware API. Same request concepts as HTTP/1.x. Two APIs would duplicate chaining, transformation, and failure rules and leak transport details.
Keep buffering every request. Can't handle large uploads, long-lived streams, or headers-only processing.
Definition of Done
Covered by #4359. Check these off when it merges:
e2e:middleware-http-v2suite cover v2 over HTTP/1.x. The v1 e2e still passes.Still open:
tls: skip.Non-Goals