Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/branch-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ jobs:
OPENSHELL_TELEMETRY_ENABLED: "false"
run: |
cargo nextest run --profile ci --workspace --features openshell-server/test-support
cargo test --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml

- name: Verify telemetry can be compiled out
run: |
Expand Down
27 changes: 27 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ host selectors choose the chain independently of the network rule that admitted
the request. Policy-local map keys identify configs, while built-in names or
operator-owned registration names identify implementations.

The configured-literal content-guard example shares matching semantics across
request bodies, complete response bodies, and client WebSocket text messages.
It requires whole-body response inspection and returns a middleware failure
when that mode is unavailable.

Built-ins run in-process against a borrowed view of the chain's current HTTP
request state. Operator services retain the bounded protobuf/gRPC contract, and
the remote adapter materializes an owned HTTP evaluation only when a request
Expand All @@ -187,6 +192,28 @@ middleware registry validates implementation-owned config. The generic
registry and chain runner live in `openshell-supervisor-middleware`; first-party
implementations live in `openshell-supervisor-middleware-builtins`.

Valid HTTP that cannot fit the response middleware protocol, including non-UTF-8
header values or an oversized preflight envelope, fails each selected stage
according to its `on_error` policy. An all-fail-open chain relays the original
bytes; a fail-closed stage prevents delivery. The relay validates HTTP syntax
and protected trailer declarations before allowing this bypass.

The same selected chain can inspect the matching final HTTP response before it
returns to the workload. Response stages select header-only, whole-body, or
streaming mode independently. The relay preserves upstream framing for a
header-only chain and owns normalized downstream framing only when body bytes
can change. Whole-body stages delay commitment and share one non-resetting,
120-second accumulation deadline per response, defined in the response relay.
Body stages receive a final body result and then one trailer exchange;
trailer mutations can only change or remove
existing, non-protected names. Intentional blocks return the canonical 403
before commitment and abort delivery without injected bytes after commitment. Streaming input units flush after bounded coalescing even within a
content-length body or transfer chunk. Coalescing cancels only input acquisition;
deadline transitions and downstream writes finish outside those timeouts.
The response runtime caps aggregate retained body data across stages and pending
output at 8 MiB. A transformation that exceeds the budget follows its stage's
failure policy, preserving its input when failing open.

The supervisor installs policy and middleware registry changes as one runtime
generation and preserves the last-known-good generation if preparation fails.
Policy-only updates reuse the connected registry, so an external middleware
Expand Down
34 changes: 23 additions & 11 deletions crates/openshell-supervisor-middleware/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@

pub mod headers;
mod remote;
mod response;
mod websocket;

pub use response::{
HttpResponseDiagnostics, HttpResponseFinish, HttpResponseInvocation,
HttpResponseInvocationOutcome, HttpResponseMiddlewareFailure, HttpResponsePreflightInput,
HttpResponsePreflightOutcome, HttpResponseSession, MAX_HTTP_RESPONSE_RETAINED_BODY_BYTES,
MAX_HTTP_RESPONSE_STREAM_UNIT_BYTES,
};

pub use websocket::{
WebSocketCoverage, WebSocketCoverageState, WebSocketInvocation, WebSocketInvocationOutcome,
WebSocketMessageAdmission, WebSocketMessageOutcome, WebSocketMessageType,
Expand Down Expand Up @@ -626,6 +634,16 @@ impl MiddlewareDispatch {
Self::Grpc(service) => service.open_websocket_session(receiver).await,
}
}

async fn open_http_response_pre_return(
&self,
receiver: tokio::sync::mpsc::Receiver<openshell_core::proto::HttpResponseEvent>,
) -> std::result::Result<HttpResponseResultStream, tonic::Status> {
match self {
Self::InProcess(service) => service.open_http_response_pre_return(receiver).await,
Self::Grpc(service) => service.open_http_response_pre_return(receiver).await,
}
}
}

struct MiddlewareServiceState {
Expand Down Expand Up @@ -831,6 +849,7 @@ fn validate_payload_limit(source: &str, binding: &MiddlewareBinding) -> Result<u
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SupportedBinding {
HttpPreCredentials,
HttpResponsePreReturn,
WebSocketPreCredentials,
}

Expand All @@ -846,9 +865,7 @@ fn supported_binding(source: &str, binding: &MiddlewareBinding) -> Result<Suppor
(
Some(SupervisorMiddlewareOperation::HttpResponse),
Some(SupervisorMiddlewarePhase::PreReturn),
) => Err(miette!(
"{source} advertises HTTP_RESPONSE/PRE_RETURN, which is not yet supported"
)),
) => Ok(SupportedBinding::HttpResponsePreReturn),
(
Some(SupervisorMiddlewareOperation::WebsocketMessage),
Some(SupervisorMiddlewarePhase::PreCredentials),
Expand Down Expand Up @@ -3686,7 +3703,7 @@ mod tests {
}

#[test]
fn manifest_rejects_http_response_pre_return_binding_until_dispatch_is_available() {
fn manifest_accepts_http_response_pre_return_binding_when_dispatch_is_available() {
let registration = external_registration(4096);
let manifest = MiddlewareManifest {
name: "example/response".into(),
Expand All @@ -3700,13 +3717,8 @@ mod tests {
expected_audience: String::new(),
};

let error = validate_external_manifest(&registration, &manifest, 4096, false)
.expect_err("HTTP response pre-return binding must remain unavailable");
assert!(
error
.to_string()
.contains("HTTP_RESPONSE/PRE_RETURN, which is not yet supported")
);
validate_external_manifest(&registration, &manifest, 4096, false)
.expect("HTTP response pre-return binding is supported");
}

#[test]
Expand Down
8 changes: 8 additions & 0 deletions crates/openshell-supervisor-middleware/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ impl GrpcMiddlewareService {
) -> std::result::Result<WebSocketResponseStream, Status> {
self.service.open_websocket_session(receiver).await
}

/// Open a remote HTTP response pre-return stream through the gRPC adapter.
pub async fn open_http_response_pre_return(
&self,
receiver: tokio::sync::mpsc::Receiver<HttpResponseEvent>,
) -> std::result::Result<HttpResponseResultStream, Status> {
self.service.open_http_response_pre_return(receiver).await
}
}

#[derive(Clone)]
Expand Down
Loading
Loading