Repository navigation
feat(middleware): non-blocking observe binding for HTTP request and response traffic #3782
Description
User Story
As a platform operator running agents under OpenShell, I need to attach a service that receives the request and response bodies of every model call after the call completes, for audit and conversation-level analysis. The service must not be able to delay, deny or alter any call, and its availability must not affect the sandbox.
Consumers with this requirement already in this repo's issues: post-completion audit for SOC2 / FedRAMP (@jordigilh, #1733) and a request that middleware be able to report what it could not observe (@eeee2345, #1733). The third is ours: a conversation-diligence instrument that re-asks each party to an agent-to-agent exchange what it understood, from a copy of its context, and compares the accounts.
Disclosure: I work at Mossrake, which builds that instrument. The binding serves any post-completion consumer; we will bind to it when it lands. Runtime interface statement: https://github.com/mossrake/meaning-slip/blob/main/meaning-slip-tracker-runtime-interface-statement.pdf. Framework paper: https://github.com/mossrake/meaning-slip/blob/main/meaning-slip-framework.pdf.
Problem Statement
Supervisor middleware has two bindings, HTTP_REQUEST / PRE_CREDENTIALS and HTTP_RESPONSE / PRE_RETURN. Both are gates: the relay waits for each stage's result before forwarding. There is no binding for a consumer that only receives traffic. The documented alternative is a middleware that allows everything and reports findings, which puts a non-enforcing consumer on the critical path with the timeouts, failure handling and payload caps of an enforcing one.
Impact / Why This Matters
Without this feature, users must run a post-completion consumer as a pass-through gating middleware, and remove Accept-Encoding at the request stage on every call, since compressed responses cannot be inspected.
This results in:
- One middleware round trip per response unit on the path to the sandbox. Streaming responses flush small units, so this is per flush.
- A choice between
fail_closed, where a consumer outage is a sandbox outage, andfail_open, where it emits a High-severity bypass finding. - Lost coverage: request bodies over 4 MiB follow
on_error, andWHOLE_BODY_BYTESis unusable for streaming. - A 500 ms per-stage, 30 s per-chain time budget that a binding cannot extend.
This matters because the consumers are audit and governance functions. They either accept latency and alert noise on production traffic, or run outside OpenShell as a separate proxy and lose the correlation with policy decisions. RFC 0009 rejected an audit-only mode as insufficient for prompt injection and exfiltration; that applies to gates, and this proposal leaves the gating bindings unchanged.
Proposed Design
A middleware advertises an observe binding for the HTTP request and response operations in its Describe manifest, and the operator attaches its config as is done today. No new policy syntax is proposed. OpenShell then:
- Never waits for an observe stage. Requests are delivered after they go upstream; response units after they go to the sandbox. Sandbox-visible latency is unchanged whether the stage is fast, slow or absent.
- Accepts only findings and metadata from it. Mutations, deny and block are ignored and counted.
- Treats unavailability as a coverage gap, not a failure.
on_errordoes not apply; missed deliveries are recorded at informational severity. - Delivers the same material the gating stages receive, with the same
request_id, including compressed and over-cap bodies. - Bounds delivery: drops rather than buffers without limit, and reports the count dropped per request.
- Emits the usual OCSF event per delivery (body hash, duration,
request_id, no body).
The consumer's own outbound calls are ordinary egress; running it in a sandbox (#2684) puts them under policy.
Acceptance Criteria
- An operator can attach an observe-bound middleware per endpoint with no new policy syntax.
- Measured sandbox-visible latency on a streamed inference response is unchanged whether the stage is attached, stopped, or responding slower than any configured middleware timeout.
- The stage receives the same request and response material as gating stages, including compressed and over-cap bodies.
- A mutation, deny or block from the stage has no effect on traffic.
- Stopping the stage produces no sandbox-visible error and no High-severity bypass finding.
- Policy validation warns on
on_errorfor an observe-only config. - Docs describe the binding, and the "observe without blocking" guidance refers to it.
Alternatives Considered
- Pass-through
PRE_RETURNmiddleware. Works today; costs are listed above. - An audit flag on existing bindings. Changes what a result may do, not whether the relay waits.
- The OCSF stream. It carries hashes, not bodies.
- A separate proxy outside OpenShell. A second component on the live path, with no correlation to policy decisions.
The observe binding is the only option in which the consumer cannot affect traffic.
Agent Investigation
- Surveyed
proto/supervisor_middleware.proto,crates/openshell-supervisor-middleware/, RFC 0009 and the supervisor-middleware docs on a clone ofmain - Found that response units are processed in lockstep:
response.rsprocess_units_fromawaits each stage's result per 64 KiB unit before forwarding, so a pass-through stage adds a round trip per flush - Found that
on_error, the 4 MiB request cap, the 500 ms / 30 s timeouts and the no-compressed-bodies rule all apply to a pass-through stage as they do to an enforcing one - Found an existing case of recording absence as coverage rather than failure (
binding_not_selected_coverageinwebsocket.rs), which the proposed binding would follow - An observe stage that reuses the existing evaluation messages and adds one phase value would leave the policy model and SDKs unchanged; the per-flush cost is inferred from the loop, not measured
Checklist
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request
Activity
c-1k commentedon Oct 3, 2026
Adding a third consumer for this binding: post-call budget reconciliation, which RFC 0009 already anticipates (README L437/L548: "Budget-style middleware that needs post-call status, final route/model, content length, or token usage needs a later metadata-only notification hook such as
HttpResponse/completed").Disclosure: I work on usertrust (Apache-2.0), an SDK that puts a budget hold on a ledger before each governed model call, settles or voids it after, and issues a hash-chained receipt per call. We're building it as OpenShell supervisor middleware:
HTTP_REQUEST / PRE_CREDENTIALS(gate): estimate cost → place hold → allow, or deny withbudget_exceeded. This has to stay a gate, since a budget that can't refuse isn't one.The observe binding proposed here fits the second half, with one requirement that differs from the audit consumers: for money, a dropped delivery is not just a coverage gap. A hold that never sees its completion stays open. It reserves budget nobody spent, and if it's later voided blindly it can under-charge a call that did run. So we'd ask that, for each
request_idthat passed the gate, the observe stream guarantees one terminal record: either the completion (status, final route/model, usage when the provider reports it), or an explicit "not delivered / dropped" marker. A reconciler can then settle or void deterministically instead of guessing from a timeout. The dropped-count in point 5 is most of the way there; it would just need to be perrequest_idand terminal.Happy to build against whatever shape lands, and to contribute test coverage for the reconciliation case.
github-actions commentedon Oct 25, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 14 days if there is no further activity. Comment or remove the state:stale label to keep it open.
zanetworker commentedon Oct 25, 2026
+1, and commenting to keep this from being closed as stale. An observe-only binding would let a service build a per-sandbox history of model and API calls (for audit, or as input to later policy decisions) without sitting in the request path, so its outage can't block the sandbox.