mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-04 08:28:19 +08:00
* feat(middleware): define HTTP response pre-return interface Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): clarify HTTP response interface Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * refactor(middleware): align response result actions Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * refactor(middleware): expose response reason codes Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * refactor(middleware): share session end reasons Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * feat(middleware)!: finalize HTTP response pre-return contract Replace the separate body_end event with HttpResponseBodyUnit.end_of_stream. Every body-inspecting stage receives exactly one flagged unit, which may be empty; a zero-byte body is one empty flagged unit and OpenShell never reads ahead to set the flag. Defer response trailers from V1 and reserve their field numbers. HTTP/1.0 clients and Content-Length bodies cannot carry trailers and that behavior was undefined. Add HttpResponsePreflight.permitted_body_modes, computed once from the original upstream head so every stage sees the same list, and make an unlisted selection a failure rather than a downgrade. Add the block_delivery preflight action as a successful decision enforced regardless of on_error. Expose Content-Length, Content-Encoding, and Content-Range read-only in preflight. Cap STREAM_BYTES input units at half of max_payload_bytes and permit deferring bytes across replacements only for fail_closed bindings, surfaced as deferral_permitted. Split PEER_DISCONNECT into DOWNSTREAM_DISCONNECT and UPSTREAM_DISCONNECT and attribute WebSocket relay failures by direction instead of a generic peer error. Compile the content-guard example in lint and branch checks so proto renames cannot break it silently. BREAKING CHANGE: WebSocketSessionEndReason and WebSocketSessionEnd are replaced by the shared MiddlewareSessionEndReason and MiddlewareSessionEnd. NORMAL_CLOSE is now NORMAL, UPSTREAM_REJECTED is now UPSTREAM_FAILURE, and PEER_DISCONNECT is split into DOWNSTREAM_DISCONNECT and UPSTREAM_DISCONNECT. Enum numbers are unchanged so binary wire compatibility is preserved; generated symbols and JSON names change. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): describe skip as opting out of inspection Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * feat(middleware): add body-phase block_delivery and skip_remaining actions Body results may now stop delivery or opt out of inspecting the rest of the response after a prefix. One HttpResponseBlockDelivery message is shared by preflight and body results and documents the difference between blocking before and after head commitment. Drop the field reservations, since nothing in this contract has shipped, and renumber session_end to close the gap. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * refactor(middleware): share HTTP body leaf messages across directions HttpBodyUnit, HttpBodyPassThrough, HttpBodyTransform, HttpBodySkipRemaining, and HttpBodyMode carry no response-specific semantics, so name them for reuse by the streaming request hook. Envelopes, results, preflight, and block_delivery stay response-specific because commitment semantics differ. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * refactor(middleware): keep HTTP body leaf messages response-specific Reverts the shared HttpBody* naming. A direction-specific payload such as a response-only semantic mode would otherwise add unreachable variants to the other direction or force a source-breaking fork after 0.1.0. The streaming request hook defines its own HttpRequestBody* messages and copies the shape; SDKs present a direction-neutral body handler over both. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): simplify response proto comments Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * fix(middleware): reject undispatched response bindings Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): simplify phase field comment Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * refactor(middleware): rename HTTP response preflight result Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * feat(middleware): add HTTP response trailer results Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): define response block delivery Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): define stage-local response body modes Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): define final response body units Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): define streaming response deferral Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): defer whole-body accumulation timeout Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): align response result diagnostics Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * test(middleware): cover upstream WebSocket disconnect Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * fix(middleware): keep response streams unit-local Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> * docs(middleware): trim disconnect compatibility note Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com> --------- Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
711 lines
30 KiB
Protocol Buffer
711 lines
30 KiB
Protocol Buffer
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
syntax = "proto3";
|
|
|
|
package openshell.middleware.v1;
|
|
|
|
import "google/protobuf/empty.proto";
|
|
import "google/protobuf/struct.proto";
|
|
|
|
// SupervisorMiddleware discovers and configures one operator-run middleware.
|
|
// It evaluates HTTP requests and WebSocket messages before credentials.
|
|
// Phase-specific services share the same registration.
|
|
service SupervisorMiddleware {
|
|
// Describe returns the service manifest and declared bindings.
|
|
rpc Describe(google.protobuf.Empty) returns (MiddlewareManifest);
|
|
|
|
// ValidateConfig checks service-specific configuration for one binding.
|
|
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse);
|
|
|
|
// EvaluateHttpRequest returns an allow, deny, or mutation decision for one
|
|
// buffered HTTP request.
|
|
rpc EvaluateHttpRequest(HttpRequestEvaluation) returns (HttpRequestResult);
|
|
|
|
// EvaluateWebSocketSession opens one ordered, phase-specific stream for a
|
|
// single middleware stage and WebSocket upgrade attempt. The current
|
|
// implementation supports client-to-upstream text messages at
|
|
// PRE_CREDENTIALS; PRE_RETURN is reserved for upstream-to-client messages.
|
|
// A request may go unanswered when the session terminates. For every opened
|
|
// stage stream, OpenShell attempts at most one session_end before closing the
|
|
// stream when its transport is still writable.
|
|
rpc EvaluateWebSocketSession(stream WebSocketSessionEvent)
|
|
returns (stream WebSocketSessionEventResult);
|
|
}
|
|
|
|
// HttpResponsePreReturn evaluates one response for one middleware stage before
|
|
// OpenShell returns it to the sandbox.
|
|
service HttpResponsePreReturn {
|
|
// Evaluate starts with preflight and may continue with selected body units
|
|
// and trailers. A body unit marked end_of_stream ends body inspection, not
|
|
// the event stream. Trailers and one best-effort session_end may follow.
|
|
rpc Evaluate(stream HttpResponseEvent)
|
|
returns (stream HttpResponseEventResult);
|
|
}
|
|
|
|
// MiddlewareManifest describes one middleware service and its bindings.
|
|
message MiddlewareManifest {
|
|
// Human-readable middleware service name used only for diagnostics. This is
|
|
// not required to match an operator-owned registration name.
|
|
string name = 1;
|
|
// Release version of the middleware service implementation, used for
|
|
// diagnostics.
|
|
string service_version = 2;
|
|
// Bindings exposed by this middleware service.
|
|
repeated MiddlewareBinding bindings = 3;
|
|
// Exact JWT audience this service verifies on inbound OpenShell calls.
|
|
// After authenticated Describe succeeds, OpenShell rejects the registration
|
|
// unless this matches the operator-configured audience. A strict verifier may
|
|
// reject an incorrect audience before returning this manifest. Empty skips
|
|
// this post-authentication consistency check.
|
|
string expected_audience = 4;
|
|
}
|
|
|
|
// MiddlewareBinding declares one operation and phase supported by a service.
|
|
message MiddlewareBinding {
|
|
// Supported operation.
|
|
SupervisorMiddlewareOperation operation = 1;
|
|
// Supported phase.
|
|
SupervisorMiddlewarePhase phase = 2;
|
|
// Maximum request body, WebSocket message, or response body unit/replacement.
|
|
// Required for payload-bearing operations.
|
|
uint64 max_payload_bytes = 3;
|
|
// Optional binding-specific RPC timeout. Empty uses the operator-configured
|
|
// service timeout, or the 500ms platform default when that is also omitted.
|
|
// A non-empty value may shorten but cannot extend the operator timeout.
|
|
// Values use an integer with an `ms` or `s` suffix and must be between
|
|
// 10ms and 30s.
|
|
string timeout = 4;
|
|
}
|
|
|
|
// ValidateConfigRequest contains one policy configuration to validate.
|
|
message ValidateConfigRequest {
|
|
// Service-specific policy configuration.
|
|
google.protobuf.Struct config = 1;
|
|
// Built-in middleware name or operator-owned registration name.
|
|
string middleware_name = 2;
|
|
}
|
|
|
|
// ValidateConfigResponse reports whether a policy configuration is accepted.
|
|
message ValidateConfigResponse {
|
|
// True when the service accepts the configuration.
|
|
bool valid = 1;
|
|
// Human-readable validation failure reason. Empty when valid is true.
|
|
string reason = 2;
|
|
}
|
|
|
|
// HttpRequestEvaluation contains one buffered HTTP request to evaluate.
|
|
message HttpRequestEvaluation {
|
|
// Evaluation phase selected for this request.
|
|
SupervisorMiddlewarePhase phase = 1;
|
|
// Sandbox and request identity available to the supervisor.
|
|
// The encoded context is limited to 4 KiB.
|
|
RequestContext context = 2;
|
|
// Validated service-specific policy configuration.
|
|
// The encoded configuration is limited to 64 KiB.
|
|
google.protobuf.Struct config = 3;
|
|
// Destination and HTTP request target.
|
|
// The encoded target is limited to 32 KiB.
|
|
HttpRequestTarget target = 4;
|
|
// HTTP request headers before OpenShell injects credentials, in wire
|
|
// order. Repeated header names are preserved as separate entries. Protected
|
|
// credential, routing, framing, and hop-by-hop headers are omitted.
|
|
// At most 128 lines and 64 KiB of encoded headers are included.
|
|
repeated HttpHeader headers = 5;
|
|
// Buffered request body, limited to 4 MiB. Empty for a bodyless request.
|
|
bytes body = 6;
|
|
// Built-in middleware name or operator-owned registration name.
|
|
string middleware_name = 7;
|
|
}
|
|
|
|
// HttpHeader is one HTTP header line.
|
|
message HttpHeader {
|
|
// Lowercased header name.
|
|
string name = 1;
|
|
// Header value with surrounding whitespace trimmed.
|
|
string value = 2;
|
|
}
|
|
|
|
// One ordered response event. A stream starts with preflight, may continue with
|
|
// body units ending in end_of_stream, may then include trailers, and may end
|
|
// with one best-effort session_end.
|
|
message HttpResponseEvent {
|
|
oneof event {
|
|
// Initial response head and request context.
|
|
HttpResponsePreflight preflight = 1;
|
|
// Next normalized body unit.
|
|
HttpResponseBodyUnit body = 2;
|
|
// Normalized trailers after the final body result.
|
|
HttpResponseTrailers trailers = 4;
|
|
// Optional terminal notification.
|
|
MiddlewareSessionEnd session_end = 3;
|
|
}
|
|
}
|
|
|
|
// Each preflight, body, and trailers event requires one ordered result.
|
|
// session_end has no result.
|
|
message HttpResponseEventResult {
|
|
oneof result {
|
|
// Result for preflight.
|
|
HttpResponsePreflightResult preflight_result = 1;
|
|
// Result for the next body unit.
|
|
HttpResponseBodyResult body_result = 2;
|
|
// Result for response trailers.
|
|
HttpResponseTrailersResult trailers_result = 3;
|
|
}
|
|
}
|
|
|
|
// HttpResponsePreflight exposes the current final response head to one stage.
|
|
message HttpResponsePreflight {
|
|
// Request identity. request_id links request and response evaluations.
|
|
// Limited to 4 KiB encoded.
|
|
RequestContext context = 1;
|
|
// Admitted request target with a redacted query. Limited to 32 KiB encoded.
|
|
HttpRequestTarget target = 2;
|
|
// Final non-informational upstream status. Upgrades are not evaluated.
|
|
uint32 status_code = 3;
|
|
// Response headers after prior stages, in wire order. Repeated names remain
|
|
// separate. Credential, routing, and hop-by-hop headers are omitted.
|
|
// Content-Length, Content-Encoding, and Content-Range retain their read-only
|
|
// upstream values. OpenShell may recompute or remove Content-Length later.
|
|
// Limited to 128 lines and 64 KiB encoded.
|
|
repeated HttpHeader headers = 4;
|
|
// Built-in middleware name or operator-owned registration name.
|
|
string middleware_name = 5;
|
|
// Validated service configuration. Limited to 64 KiB encoded.
|
|
google.protobuf.Struct config = 6;
|
|
// Effective minimum of platform, registration, and binding limits. Applies to
|
|
// whole-body input/replacement and each stream input/replacement. Stream
|
|
// inputs use at most min(64 KiB, max_payload_bytes).
|
|
uint64 max_payload_bytes = 7;
|
|
// Modes derived independently for this stage. OpenShell first determines
|
|
// response-shape eligibility from the original final response head, then
|
|
// applies this stage's effective max_payload_bytes. Different stages may
|
|
// receive different lists. HEADERS_ONLY is always present and is the only
|
|
// mode for bodyless, partial, encoded, or no-transform responses. For an
|
|
// otherwise eligible response, a known body larger than this stage's limit
|
|
// omits WHOLE_BODY_BYTES. An eligible unknown-length response may select
|
|
// WHOLE_BODY_BYTES and later fail with whole_body_over_capacity according to
|
|
// this stage's on_error. STREAM_BYTES is omitted when
|
|
// max_payload_bytes is zero. Selecting an unlisted mode fails according to
|
|
// on_error.
|
|
repeated HttpResponseBodyMode permitted_body_modes = 8;
|
|
}
|
|
|
|
// Selects skip, inspect, or block. Diagnostic fields apply to every action.
|
|
// Invalid diagnostics make the entire result a middleware failure handled
|
|
// according to on_error.
|
|
message HttpResponsePreflightResult {
|
|
oneof action {
|
|
// Deliver unchanged without invoking on_error.
|
|
HttpResponsePreflightSkip skip = 1;
|
|
// Inspect with the selected body mode and mutations.
|
|
HttpResponsePreflightInspect inspect = 2;
|
|
// Prevent delivery to the sandbox.
|
|
HttpResponseBlockDelivery block_delivery = 7;
|
|
}
|
|
// Service diagnostic, never sent to the sandbox or security logs. Maximum
|
|
// 4 KiB.
|
|
string reason = 3;
|
|
// Optional audit code using the HttpRequestResult.reason_code format and
|
|
// 64-byte maximum. Returned to the sandbox only for block_delivery.
|
|
string reason_code = 4;
|
|
// Up to 32 audit-safe findings, each limited to 4 KiB encoded.
|
|
repeated Finding findings = 5;
|
|
// Non-secret diagnostic metadata, limited to 64 entries and 32 KiB.
|
|
map<string, string> metadata = 6;
|
|
}
|
|
|
|
// Ends this stage successfully without body inspection.
|
|
message HttpResponsePreflightSkip {}
|
|
|
|
// Blocks delivery as a successful decision regardless of on_error. OpenShell
|
|
// evaluates results in policy order. Once it accepts a valid block, it stops
|
|
// later middleware evaluation and ends every still-writable opened stage with
|
|
// MIDDLEWARE_DENIAL. A failure handled earlier may already have stopped
|
|
// evaluation, so a later block does not override it. An invalid block result
|
|
// is a middleware failure handled according to on_error. The upstream request
|
|
// has already run; blocking its response does not reject or roll back that
|
|
// request.
|
|
//
|
|
// Before response commitment, including at preflight and during
|
|
// WHOLE_BODY_BYTES, OpenShell replaces the upstream response with the canonical
|
|
// 403 Forbidden middleware-denial response. Its JSON body has
|
|
// error = "middleware_denied" and includes a validated reason_code when the
|
|
// result supplies one. OpenShell never returns the free-form reason or writes it
|
|
// to security logs. For HEAD, OpenShell sends the canonical response headers
|
|
// and Content-Length but no body. It closes the downstream connection after the
|
|
// denial response.
|
|
//
|
|
// After response commitment, including during STREAM_BYTES, OpenShell aborts
|
|
// downstream delivery. It does not inject an error body, a terminating chunk,
|
|
// or an error trailer. OpenShell does not reuse the upstream connection.
|
|
message HttpResponseBlockDelivery {}
|
|
|
|
// Selects body inspection and response-header mutations.
|
|
message HttpResponsePreflightInspect {
|
|
// Required mode from permitted_body_modes. Invalid values fail according to
|
|
// on_error.
|
|
HttpResponseBodyMode body_mode = 1;
|
|
// Ordered mutations applied atomically before the next stage. Only visible
|
|
// end-to-end headers may change. Routing, credential, framing, coding, range,
|
|
// and hop-by-hop headers are protected; integrity headers may only be removed.
|
|
// Limited to 64 operations, 32 KiB of name/value data, and 64 KiB encoded.
|
|
repeated HeaderMutation header_mutations = 2;
|
|
}
|
|
|
|
// Controls which response-body units a stage receives.
|
|
enum HttpResponseBodyMode {
|
|
// Invalid value handled according to on_error.
|
|
HTTP_RESPONSE_BODY_MODE_UNSPECIFIED = 0;
|
|
// Inspect only the response head.
|
|
HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY = 1;
|
|
// Buffer the normalized body as one final unit before committing the head.
|
|
// Input and replacement must fit max_payload_bytes. Capacity failures use
|
|
// whole_body_over_capacity and follow this stage's on_error.
|
|
HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES = 2;
|
|
// Receive normalized units ending with end_of_stream. Each input is at most
|
|
// min(64 KiB, max_payload_bytes), and each replacement must fit
|
|
// max_payload_bytes. Each result fully accounts for its input unit; V1 does
|
|
// not permit retaining input across units. The full body may exceed the
|
|
// limit. STREAM_BYTES has no total response-lifetime deadline.
|
|
HTTP_RESPONSE_BODY_MODE_STREAM_BYTES = 3;
|
|
}
|
|
|
|
// One normalized body unit. Boundaries have no transport or application
|
|
// meaning.
|
|
message HttpResponseBodyUnit {
|
|
// Contiguous and stage-local, starting at 1.
|
|
uint64 sequence = 1;
|
|
oneof payload {
|
|
// Bytes without transfer framing. A body-capable response with no body bytes
|
|
// has present empty data in sequence 1. STREAM_BYTES input size is at most
|
|
// min(64 KiB, max_payload_bytes). A unit may be shorter to preserve
|
|
// flushing.
|
|
bytes data = 2;
|
|
}
|
|
// Marks the final body unit. Every normally completed body inspection receives
|
|
// exactly one. For a body-capable response with no body bytes, this is the
|
|
// empty sequence-1 unit. OpenShell does not read ahead, so it may send an empty
|
|
// final unit after the last nonempty unit. Trailers and session_end may
|
|
// follow. A stage ended by skip_remaining, block, or failure receives no
|
|
// later final unit.
|
|
bool end_of_stream = 3;
|
|
}
|
|
|
|
// Result for one body unit. Units are processed in lockstep; V1 does not
|
|
// support ownership transfer or cross-unit retention. Diagnostic fields apply
|
|
// to every action. Invalid diagnostics make the entire result a middleware
|
|
// failure handled according to on_error. OpenShell retains the current input
|
|
// until it validates the result, so fail-open can continue from the last input
|
|
// OpenShell still owns.
|
|
message HttpResponseBodyResult {
|
|
// Must match the next unit. Zero, gaps, duplicates, and regressions fail.
|
|
uint64 sequence = 1;
|
|
// Exactly one explicit action is required.
|
|
oneof action {
|
|
// Forward the input unit unchanged.
|
|
HttpResponseBodyPassThrough pass_through = 2;
|
|
// Replace the complete input unit.
|
|
HttpResponseBodyTransform transform = 3;
|
|
// Stop delivery. See HttpResponseBlockDelivery.
|
|
HttpResponseBlockDelivery block_delivery = 8;
|
|
// Finalize this unit and stop inspecting.
|
|
HttpResponseBodySkipRemaining skip_remaining = 9;
|
|
}
|
|
// Service diagnostic, never sent to the sandbox or security logs. Maximum
|
|
// 4 KiB.
|
|
string reason = 4;
|
|
// Optional audit code using the HttpRequestResult.reason_code format and
|
|
// 64-byte maximum. When OpenShell accepts block_delivery before response
|
|
// commitment, it includes this code in the canonical denial response. It is
|
|
// never returned after commitment.
|
|
string reason_code = 5;
|
|
// Up to 32 audit-safe findings, each limited to 4 KiB encoded.
|
|
repeated Finding findings = 6;
|
|
// Non-secret diagnostic metadata, limited to 64 entries and 32 KiB.
|
|
map<string, string> metadata = 7;
|
|
}
|
|
|
|
// Preserves the input unit.
|
|
message HttpResponseBodyPassThrough {}
|
|
|
|
// Finalizes this unit and ends the stage. This stage receives no later body or
|
|
// trailer events. The current and later units continue through other stages.
|
|
// For WHOLE_BODY_BYTES, this equals its nested action.
|
|
message HttpResponseBodySkipRemaining {
|
|
// Exactly one action for the current unit.
|
|
oneof current {
|
|
// Forward the current unit unchanged.
|
|
HttpResponseBodyPassThrough pass_through = 1;
|
|
// Replace the current unit.
|
|
HttpResponseBodyTransform transform = 2;
|
|
}
|
|
}
|
|
|
|
// Replaces the complete input unit.
|
|
message HttpResponseBodyTransform {
|
|
// Required replacement, limited to max_payload_bytes. Present empty data
|
|
// deletes the input unit. The replacement fully accounts for this input unit;
|
|
// middleware must not retain input bytes for a later unit in V1.
|
|
oneof replacement {
|
|
// Normalized replacement bytes.
|
|
bytes data = 1;
|
|
}
|
|
}
|
|
|
|
// The current normalized response trailers in wire order. Repeated names stay
|
|
// as separate fields. A stage that completes WHOLE_BODY_BYTES or STREAM_BYTES
|
|
// receives exactly one trailers event after its final body result, including
|
|
// when this set is empty. SKIP, HEADERS_ONLY, semantically bodyless responses,
|
|
// and stages ended by block, failure, or skip_remaining receive no trailers.
|
|
message HttpResponseTrailers {
|
|
repeated HttpHeader headers = 1;
|
|
}
|
|
|
|
// Applies ordered trailer mutations atomically. An empty mutation list
|
|
// preserves the current trailers. A write may target only a case-insensitive
|
|
// name present in the trailers event; V1 cannot create a trailer name. Removal
|
|
// of an absent name is a no-op. Credential, routing, framing, coding, range,
|
|
// hop-by-hop, and connection-nominated fields are protected. Diagnostic fields
|
|
// apply whether mutations are empty or nonempty. Invalid diagnostics or
|
|
// mutations make the entire result a middleware failure handled according to
|
|
// on_error.
|
|
message HttpResponseTrailersResult {
|
|
// At most 64 operations, 32 KiB of validated name/value data, and 64 KiB
|
|
// encoded are accepted.
|
|
repeated HeaderMutation trailer_mutations = 1;
|
|
// Service diagnostic, never sent to the sandbox or security logs. Maximum
|
|
// 4 KiB.
|
|
string reason = 2;
|
|
// Optional audit code using the HttpRequestResult.reason_code format and
|
|
// 64-byte maximum. Never sent to the sandbox.
|
|
string reason_code = 3;
|
|
// Up to 32 audit-safe findings, each limited to 4 KiB encoded.
|
|
repeated Finding findings = 4;
|
|
// Non-secret diagnostic metadata, limited to 64 entries and 32 KiB.
|
|
map<string, string> metadata = 5;
|
|
}
|
|
|
|
// Stable reason OpenShell ended a middleware stage stream.
|
|
enum MiddlewareSessionEndReason {
|
|
// Invalid reason.
|
|
MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED = 0;
|
|
// Evaluation completed.
|
|
MIDDLEWARE_SESSION_END_REASON_NORMAL = 1;
|
|
// The sandbox peer disconnected.
|
|
MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT = 2;
|
|
// A policy reload replaced the active middleware chain.
|
|
MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD = 3;
|
|
// A stage denied the operation or blocked the response.
|
|
MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL = 4;
|
|
// A selected stage failed.
|
|
MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE = 5;
|
|
// A proxied or middleware protocol was violated.
|
|
MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR = 6;
|
|
// Evaluation was canceled for another reason.
|
|
MIDDLEWARE_SESSION_END_REASON_CANCELLATION = 7;
|
|
// Upstream rejected or failed before a valid response or upgrade.
|
|
MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE = 8;
|
|
// Network policy denied the operation.
|
|
MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL = 9;
|
|
// The stage successfully declined inspection during preflight.
|
|
MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED = 10;
|
|
// Upstream disconnected after a valid response or upgrade.
|
|
MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT = 11;
|
|
}
|
|
|
|
// Best-effort terminal notification. A stage receives at most one and sends no
|
|
// result.
|
|
message MiddlewareSessionEnd {
|
|
// Terminal reason. Producers never send UNSPECIFIED.
|
|
MiddlewareSessionEndReason reason = 1;
|
|
// Set only for PROTOCOL_ERROR. Missing or unknown details mean a generic
|
|
// protocol error.
|
|
MiddlewareSessionProtocolError protocol_error = 2;
|
|
}
|
|
|
|
// Details for a protocol-error session end.
|
|
message MiddlewareSessionProtocolError {
|
|
oneof domain {
|
|
// WebSocket protocol violation.
|
|
WebSocketProtocolError web_socket = 1;
|
|
// Middleware event/result protocol violation.
|
|
MiddlewareExchangeProtocolError middleware_exchange = 2;
|
|
}
|
|
}
|
|
|
|
// WebSocket protocol error details, reserved for future categories.
|
|
message WebSocketProtocolError {}
|
|
|
|
// Middleware exchange error details, reserved for future categories.
|
|
message MiddlewareExchangeProtocolError {}
|
|
|
|
// Supervisor operation selected for middleware evaluation.
|
|
enum SupervisorMiddlewareOperation {
|
|
SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED = 0;
|
|
SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST = 1;
|
|
SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE = 2;
|
|
SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE = 3;
|
|
}
|
|
|
|
// Ordered phase within a supervisor operation.
|
|
enum SupervisorMiddlewarePhase {
|
|
SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED = 0;
|
|
SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS = 1;
|
|
SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN = 2;
|
|
}
|
|
|
|
// WebSocketSessionEvent is one ordered event in a stage-local stream.
|
|
// Message sequence numbers identify logical messages session-wide. A stage
|
|
// receives a strictly increasing subset of those numbers; gaps are valid when
|
|
// session messages are not delivered to that stage.
|
|
message WebSocketSessionEvent {
|
|
oneof event {
|
|
WebSocketPreflight preflight = 1;
|
|
WebSocketSessionStart session_start = 2;
|
|
WebSocketMessage message = 3;
|
|
MiddlewareSessionEnd session_end = 4;
|
|
}
|
|
}
|
|
|
|
// WebSocketPreflight lets a service decline this upgrade before OpenShell
|
|
// contacts upstream. It deliberately excludes query data, arbitrary request
|
|
// headers, and message payloads.
|
|
message WebSocketPreflight {
|
|
string session_id = 1;
|
|
SupervisorMiddlewarePhase phase = 2;
|
|
RequestContext context = 3;
|
|
// Admitted HTTP WebSocket-upgrade target. The method is GET, query is always
|
|
// empty, and path never includes a query string.
|
|
HttpRequestTarget target = 4;
|
|
repeated string requested_subprotocols = 5;
|
|
// Built-in middleware name or operator-owned registration name.
|
|
string middleware_name = 6;
|
|
google.protobuf.Struct config = 7;
|
|
}
|
|
|
|
// WebSocketSessionStart reports bounded metadata known only after the
|
|
// upstream 101 response validates. Empty selected_subprotocol means none.
|
|
message WebSocketSessionStart {
|
|
string selected_subprotocol = 1;
|
|
}
|
|
|
|
// WebSocketMessage contains one complete reconstructed logical message.
|
|
message WebSocketMessage {
|
|
// Session-global sequence starting at 1. Values delivered to one stage must
|
|
// strictly increase but need not be contiguous. Reject zero, duplicates, and
|
|
// regressions; accept gaps.
|
|
uint64 sequence = 1;
|
|
// One complete logical payload. Protobuf string decoding enforces UTF-8 for
|
|
// text messages. Raw frame mechanics are never exposed. Limited to 4 MiB by
|
|
// the platform and the binding-specific cap.
|
|
oneof payload {
|
|
string text = 2;
|
|
bytes binary = 3;
|
|
}
|
|
}
|
|
|
|
// WebSocketPreflightAction is the service's one-time scoping decision.
|
|
enum WebSocketPreflightAction {
|
|
// Invalid response value handled according to the policy failure mode.
|
|
WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED = 0;
|
|
// Inspect this session after the upstream accepts the upgrade.
|
|
WEB_SOCKET_PREFLIGHT_ACTION_INSPECT = 1;
|
|
// Voluntarily decline inspection without denying the upgrade. This is a
|
|
// successful decision and does not engage on_error.
|
|
WEB_SOCKET_PREFLIGHT_ACTION_SKIP = 2;
|
|
// Authoritatively deny the upgrade before upstream contact. This is a
|
|
// successful decision and is enforced regardless of on_error.
|
|
WEB_SOCKET_PREFLIGHT_ACTION_DENY = 3;
|
|
}
|
|
|
|
message WebSocketPreflightDecision {
|
|
WebSocketPreflightAction action = 1;
|
|
// Free-form service diagnostic. OpenShell never exposes this to the
|
|
// workload or security logs. Limited to 4 KiB before discarding.
|
|
string reason = 2;
|
|
// Optional stable machine-readable code for a deny decision. Because
|
|
// preflight runs before the HTTP upgrade completes, OpenShell may return
|
|
// this code to the requester. Codes follow the same format and 64-byte
|
|
// maximum as HttpRequestResult.reason_code.
|
|
string reason_code = 3;
|
|
// Audit-safe findings produced during preflight. At most 32 findings of at
|
|
// most 4 KiB encoded each are accepted.
|
|
repeated Finding findings = 4;
|
|
// Non-secret service-defined metadata included in diagnostics. At most 64
|
|
// entries and 32 KiB of combined key/value data are accepted.
|
|
map<string, string> metadata = 5;
|
|
}
|
|
|
|
// WebSocketMessageResult contains the decision and optional replacement for
|
|
// one message. A replacement must use the same variant as the input payload.
|
|
message WebSocketMessageResult {
|
|
// Must exactly match the sequence of the corresponding WebSocketMessage.
|
|
uint64 sequence = 1;
|
|
Decision decision = 2;
|
|
// Absence preserves the input unchanged. Oneof presence distinguishes an
|
|
// empty replacement from no replacement, and string decoding enforces UTF-8.
|
|
oneof replacement {
|
|
string text = 3;
|
|
bytes binary = 4;
|
|
}
|
|
// Free-form service diagnostic. OpenShell never exposes this to the
|
|
// workload or security logs. Limited to 4 KiB before discarding.
|
|
string reason = 5;
|
|
// Optional stable machine-readable code for OCSF only. Unlike the HTTP
|
|
// reason_code, this value is never put in a WebSocket close frame.
|
|
string reason_code = 6;
|
|
repeated Finding findings = 7;
|
|
map<string, string> metadata = 8;
|
|
}
|
|
|
|
// WebSocketSessionEventResult is an evaluation result for a preflight or message
|
|
// event. Session start and end events do not produce results.
|
|
message WebSocketSessionEventResult {
|
|
oneof result {
|
|
WebSocketPreflightDecision preflight_decision = 1;
|
|
WebSocketMessageResult message_result = 2;
|
|
}
|
|
}
|
|
|
|
// RequestContext identifies the sandbox request being evaluated.
|
|
message RequestContext {
|
|
// Request id used to correlate middleware and supervisor logs.
|
|
string request_id = 1;
|
|
// Sandbox id that originated the request.
|
|
string sandbox_id = 2;
|
|
// Workload process that originated the request, when available.
|
|
Process originating_process = 3;
|
|
// Sandbox name that originated the request. For display and logging only.
|
|
// Names are workspace-scoped and may be reused for different sandbox
|
|
// instances, so consumers must use sandbox_id for authorization, persistence,
|
|
// durable correlation, and identity.
|
|
string sandbox_name = 4;
|
|
// Workspace the sandbox belongs to. For display and logging only; see the
|
|
// sandbox_name guidance above.
|
|
string workspace = 5;
|
|
}
|
|
|
|
// HttpRequestTarget describes the admitted HTTP destination and request target.
|
|
message HttpRequestTarget {
|
|
// Request scheme, such as "http", "https", "ws", or "wss".
|
|
string scheme = 1;
|
|
// Destination hostname selected by network policy.
|
|
string host = 2;
|
|
// Destination TCP port.
|
|
uint32 port = 3;
|
|
// HTTP request method.
|
|
string method = 4;
|
|
// Request path without the query string.
|
|
string path = 5;
|
|
// Raw request query string without the leading question mark.
|
|
string query = 6;
|
|
}
|
|
|
|
// Process identifies a workload process and its executable ancestry.
|
|
message Process {
|
|
// Executable path for the originating process.
|
|
string binary = 1;
|
|
// Process id within the sandbox.
|
|
uint32 pid = 2;
|
|
// Executable paths for ancestor processes, nearest parent first.
|
|
repeated string ancestors = 3;
|
|
}
|
|
|
|
// Decision controls whether OpenShell continues processing the current
|
|
// evaluation unit.
|
|
enum Decision {
|
|
// Invalid response value handled according to the policy failure mode.
|
|
DECISION_UNSPECIFIED = 0;
|
|
// Continue processing the current request or message and apply any returned
|
|
// mutations.
|
|
DECISION_ALLOW = 1;
|
|
// Reject the current request or message. The operation-specific result
|
|
// defines the enclosing protocol behavior.
|
|
DECISION_DENY = 2;
|
|
}
|
|
|
|
// Finding is an audit-safe observation produced during evaluation.
|
|
message Finding {
|
|
// Stable, service-defined finding type.
|
|
string type = 1;
|
|
// Human-readable finding label that does not contain request content.
|
|
string label = 2;
|
|
// Number of matching observations represented by this finding.
|
|
uint32 count = 3;
|
|
// Service-defined confidence level.
|
|
string confidence = 4;
|
|
// Service-defined severity level.
|
|
string severity = 5;
|
|
}
|
|
|
|
// ExistingHeaderAction controls how a header write behaves when the
|
|
// case-insensitive header name is already present. Every action writes the
|
|
// value when the header is absent.
|
|
enum ExistingHeaderAction {
|
|
EXISTING_HEADER_ACTION_UNSPECIFIED = 0;
|
|
// Add another field value without changing existing values.
|
|
EXISTING_HEADER_ACTION_APPEND = 1;
|
|
// Remove every existing value, then add the new value.
|
|
EXISTING_HEADER_ACTION_OVERWRITE = 2;
|
|
// Leave the existing values unchanged.
|
|
EXISTING_HEADER_ACTION_SKIP = 3;
|
|
}
|
|
|
|
// WriteHeader proposes one header value and defines collision behavior.
|
|
message WriteHeader {
|
|
string name = 1;
|
|
string value = 2;
|
|
ExistingHeaderAction on_existing = 3;
|
|
}
|
|
|
|
// RemoveHeader removes every value for a case-insensitive header name.
|
|
message RemoveHeader {
|
|
string name = 1;
|
|
}
|
|
|
|
// HeaderMutation is one ordered HTTP header operation.
|
|
message HeaderMutation {
|
|
oneof operation {
|
|
WriteHeader write = 1;
|
|
RemoveHeader remove = 2;
|
|
}
|
|
}
|
|
|
|
// HttpRequestResult contains the decision and optional request mutations.
|
|
message HttpRequestResult {
|
|
// Allow or deny decision for this request.
|
|
Decision decision = 1;
|
|
// Free-form service diagnostic. OpenShell does not relay this text into
|
|
// denied responses or security logs. Limited to 4 KiB before discarding.
|
|
string reason = 2;
|
|
// Replacement request body when has_body is true. Limited to 4 MiB.
|
|
bytes body = 3;
|
|
// True when body should replace the request body, including with an empty body.
|
|
bool has_body = 4;
|
|
// Ordered request-header mutations applied before the next middleware and
|
|
// before forwarding. Writes and removals may target visible end-to-end
|
|
// request headers, but credential, routing, framing, and hop-by-hop headers
|
|
// are always protected. Written values cannot contain OpenShell credential
|
|
// placeholder syntax. A violating result is a middleware failure handled
|
|
// according to the policy failure mode. At most 64 operations, 32 KiB of
|
|
// validated name/value data, and 64 KiB encoded are accepted.
|
|
repeated HeaderMutation header_mutations = 5;
|
|
// Audit-safe findings produced during evaluation. For operator-run services,
|
|
// OpenShell logs platform-owned fields derived from the operator-owned
|
|
// registration name rather than service-provided type, label, confidence,
|
|
// or metadata text.
|
|
// At most 32 findings of at most 4 KiB encoded each are accepted per stage.
|
|
// A policy selects at most 10 stages, so one chain retains at most 320.
|
|
repeated Finding findings = 6;
|
|
// Non-secret service-defined metadata included in diagnostics. At most 64
|
|
// entries and 32 KiB of combined key/value data are accepted.
|
|
map<string, string> metadata = 7;
|
|
// Optional stable machine-readable code for a deny decision. Codes must
|
|
// start with a lowercase ASCII letter and contain only lowercase ASCII
|
|
// letters, digits, and underscores, with a maximum length of 64 bytes.
|
|
// OpenShell may return this code to the requester, unlike free-form reason.
|
|
string reason_code = 8;
|
|
}
|