Skip to content

feat(supervisor): add HTTP response pre-return middleware evaluation #2691

Description

@pimlock

User story

As an operator, I want attached supervisor middleware to inspect, transform, or block HTTP responses before the sandbox receives them.

Problem statement

Supervisor middleware supports HTTP_REQUEST/PRE_CREDENTIALS, but it has no HTTP_RESPONSE/PRE_RETURN hook. OpenShell therefore forwards upstream responses without running attached response policy. Asynchronous inspection cannot enforce this hook because it cannot recall bytes already delivered to the sandbox.

Parent: #1733.

Impact / why this matters

Operators cannot apply generic response controls such as PII detection, redaction, secret scanning, or compliance filtering in the configured middleware chain. The current workaround requires another proxy and loses OpenShell's policy order, failure behavior, and audit context.

Proposed design

Add an HTTP_RESPONSE/PRE_RETURN binding and a hook-specific bidirectional inspection API. The API models HTTP response semantics directly rather than reusing the request contract.

Each selected stage receives a preflight with the final response status, safe headers, request context, stage-local body-mode choices, and supervisor limits. It may skip, inspect, or block_delivery. An inspecting stage selects headers-only processing, one bounded whole-body value, or normalized response-body units. Every body unit has a stage-local sequence and end_of_stream marks the final result-bearing unit.

Body-inspecting stages that complete normally receive one normalized trailer event, including when the trailer set is empty, and return one trailer result. V1 can modify or remove existing trailer names but cannot create a name absent from the event.

block_delivery is a successful policy decision, not a middleware failure. Before response commitment, OpenShell returns its canonical 403 middleware_denied response. After commitment, OpenShell aborts downstream delivery. The upstream request has already run, so blocking its response does not reject or roll back that request.

STREAM_BYTES is unit-local in V1: every result accounts for its complete input unit, and middleware cannot retain input across units. skip_remaining finalizes the current unit and ends inspection for that stage while later stages continue. Middleware failures still follow on_error. Fail-open continues from the last input OpenShell still owns; fail-closed returns a clean error before commitment and aborts after commitment.

Policy attachment remains unchanged. Attaching middleware activates every operation and phase advertised by its manifest.

This issue includes the raw hook protocol and one response-capable example. A middleware-author SDK or callback adapter is follow-up work.

The build-plan comment defines the complete contract and implementation details.

Acceptance criteria

  • An attached service can advertise HTTP_RESPONSE/PRE_RETURN and execute it through HttpResponsePreReturn.Evaluate using the finalized V1 message names.
  • Preflight supports skip, inspect, and block_delivery. Result-envelope diagnostics apply to every action and invalid diagnostics follow on_error.
  • OpenShell derives permitted_body_modes from the original final response head and each stage's effective payload limit. Different stages may receive different lists.
  • HTTP request and response middleware can write permitted end-to-end headers without an x-openshell-middleware- namespace restriction. Both paths use one shared validator and atomic applicator with direction-specific protected fields.
  • V1 supports headers-only inspection, bounded complete-body inspection, and normalized lockstep byte streaming.
  • Every normally completed body inspection receives one final result-bearing unit with end_of_stream = true, including a present empty sequence-1 unit for an empty body-capable response.
  • Every body input receives exactly one ordered pass_through, transform, block_delivery, or skip_remaining result with the matching stage-local sequence.
  • STREAM_BYTES is unit-local. Every result fully accounts for the current input, and V1 does not permit cross-unit retention.
  • skip_remaining ends body and trailer events only for that stage. The current and later units continue through later stages.
  • A normally completed whole-body or streaming stage receives one trailer event after its final body result, including for an empty trailer set, and returns one trailer result.
  • V1 trailer results may modify or remove names present in the trailer event. They cannot introduce a missing name.
  • OpenShell does not expose HTTP/1 transfer chunks or socket-read boundaries to middleware.
  • OpenShell repairs downstream framing after transformation and excludes protocol upgrades from generic response-body processing.
  • OpenShell relays interim 1xx responses unchanged and evaluates only the final non-1xx response head.
  • Before commitment, block_delivery returns the canonical 403 middleware_denied; HEAD receives the same headers and content length without a body. After commitment, OpenShell aborts delivery and does not reuse upstream.
  • Before commitment, fail-closed middleware failure returns the canonical 502 response_delivery_failed; HEAD receives the same headers and content length without a body. After commitment, fail-closed aborts delivery.
  • STREAM_BYTES inputs are at most min(64 KiB, max_payload_bytes). Each result uses the effective binding timeout and the existing 30-second per-unit chain cap. Streaming has no total response-lifetime deadline.
  • Whole-body capacity failures use whole_body_over_capacity. A dedicated whole-body accumulation deadline and its configuration remain required before the response dispatcher lands.
  • Shared terminal reasons distinguish sandbox and upstream disconnects.
  • Tests cover ordered and mixed-profile chains, bodyless responses, content-length, chunked and close-delimited bodies, replacement, whole-body overflow, non-identity content encoding, partial responses, original no-transform, stale integrity metadata, trailers, block delivery, unit-local streaming, skip remaining, timeouts, directional disconnects, and policy reload.
  • OCSF events omit response content, protected headers, and free-form middleware reasons.

Alternatives considered

Use one universal inspection RPC

HTTP requests, HTTP responses, and WebSocket messages have different heads, boundaries, mutation rules, commitment points, and denial behavior. A universal event union would move those rules out of the interface and into prose and runtime validation. The implementation may share flow-control machinery, but the public API remains hook-specific.

Observe responses asynchronously

This preserves latency but permits partial disclosure, so it cannot enforce PRE_RETURN.

Add only a unary buffered response RPC

This cannot support SSE, long-lived responses, or later rolling-window processing without introducing another API.

Reuse EvaluateHttpRequest

Requests and responses differ in status, headers, trailers, framing, mutation authority, and commitment. They need separate contracts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:supervisorProxy and routing-path workstate:acceptedA maintainer decided OpenShell should pursue this issuetopic:l7Application-layer policy and inspection work

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions