Files
OpenShell/proto/supervisor_middleware.proto
2026-09-21 23:20:40 -07:00

602 lines
22 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/struct.proto";
import "google/protobuf/duration.proto";
import "extension.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(MiddlewareDescribeRequest) returns (MiddlewareManifest);
// ValidateConfig checks service-specific configuration for one binding.
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse);
// 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);
}
message MiddlewareDescribeRequest {
// Gateway or supervisor protocol metadata. Middleware must reject unmet
// requirements before accepting traffic.
openshell.extension.v1.PeerMetadata gateway = 1;
}
// HttpRequestPreCredentials begins after network-policy admission and before
// OpenShell injects credentials. A STREAM stage may remain active while
// OpenShell forwards already-approved units upstream.
service HttpRequestPreCredentials {
// EvaluateHttp uses the negotiated two-mode HTTP protocol. The distinct
// method path prevents old and new peers from decoding incompatible stream
// messages as though they shared a contract.
rpc EvaluateHttp(stream HttpEvent) returns (stream HttpResult);
}
// HttpResponsePreReturn evaluates one response for one middleware stage before
// OpenShell returns it to the sandbox.
service HttpResponsePreReturn {
// EvaluateHttp uses the same body lifecycle as request evaluation. Runtime
// support and per-message eligibility still determine the offered modes.
rpc EvaluateHttp(stream HttpEvent) returns (stream HttpResult);
}
// 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;
// Deprecated diagnostic compatibility field. Use
// extension.implementation_version.
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;
// Supervisor-middleware protocol metadata. Required for negotiation.
openshell.extension.v1.PeerMetadata extension = 5;
}
// MiddlewareBinding declares one operation and phase supported by a service.
message MiddlewareBinding {
reserved 4;
reserved "timeout";
// 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. An HTTP binding with no supported
// body modes may leave this zero because it can only Continue or Reject at
// preflight.
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 must be between 10ms and 30s.
google.protobuf.Duration request_timeout = 104;
// HTTP body protocol version implemented by this binding. HTTP request and
// response bindings must advertise version 1. Other operations leave this
// unset. OpenShell rejects missing and unsupported versions at activation.
uint32 http_protocol_version = 5;
// HTTP body modes this implementation can select. OpenShell intersects this
// set with runtime, policy, and message eligibility before every preflight.
// Empty declares a preflight-only HTTP binding that can Continue, optionally
// with header mutations, or Reject without receiving body events.
repeated HttpBodyMode supported_http_body_modes = 6;
}
// 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;
}
// 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 event shared by request and response evaluation. Preflight is
// first. Inspect is followed by Begin and exactly one selected body lifecycle.
// session_end is best effort and has no result.
message HttpEvent {
oneof event {
HttpPreflight preflight = 1;
HttpBegin begin = 2;
HttpInputChunk input_chunk = 3;
HttpInputEnd input_end = 4;
HttpBufferedBody buffered_body = 5;
MiddlewareSessionEnd session_end = 6;
}
}
// One ordered result shared by request and response evaluation. Unknown and
// unset alternatives are protocol errors, never implicit success.
message HttpResult {
oneof result {
HttpPreflightResult preflight_result = 1;
HttpBufferedResult buffered_result = 2;
HttpOutputStart output_start = 3;
HttpOutputChunk output_chunk = 4;
HttpFinish finish = 5;
HttpReject reject = 6;
}
}
enum HttpBodyMode {
HTTP_BODY_MODE_UNSPECIFIED = 0;
// One complete normalized body and one terminal result. OpenShell retains
// bounded input in RAM and never spills to disk.
HTTP_BODY_MODE_BUFFERED = 1;
// Independent ordered input and output streams. Middleware owns all working
// storage and returns every selected output byte.
HTTP_BODY_MODE_STREAM = 2;
}
message HttpPreflight {
oneof head {
HttpRequestPreflightHead request = 1;
HttpResponsePreflightHead response = 2;
}
// Intersection of service capabilities, runtime support, policy, and message
// eligibility. Empty permits only Continue or Reject.
repeated HttpBodyMode permitted_body_modes = 3;
// Offered modes allowed to mutate otherwise writable headers after body
// inspection. Empty in the initial public rollout.
repeated HttpBodyMode late_header_modes = 4;
HttpBodyLimits limits = 5;
// Declared length of the original admitted HTTP representation. An earlier
// stage may replace the body, so later stages must enforce their selected
// mode limit against the body events they actually receive.
optional uint64 declared_input_bytes = 6;
}
// Request head exposed after admission and before credential injection.
message HttpRequestPreflightHead {
RequestContext context = 1;
HttpRequestTarget target = 2;
repeated HttpHeader headers = 3;
string middleware_name = 4;
google.protobuf.Struct config = 5;
}
// Current final response head exposed before delivery to the sandbox.
message HttpResponsePreflightHead {
RequestContext context = 1;
HttpRequestTarget target = 2;
uint32 status_code = 3;
repeated HttpHeader headers = 4;
string middleware_name = 5;
google.protobuf.Struct config = 6;
}
// Limits are positive when present. Queue limits include transport adapter
// queues, not only application channels. Omitted total limits do not make any
// individual queue unbounded.
message HttpBodyLimits {
uint64 max_chunk_bytes = 1;
uint64 max_buffered_body_bytes = 2;
uint64 max_input_queue_bytes = 3;
uint64 max_input_queue_messages = 4;
uint64 max_output_queue_bytes = 5;
uint64 max_output_queue_messages = 6;
optional uint64 max_total_input_bytes = 7;
optional uint64 max_total_output_bytes = 8;
google.protobuf.Duration idle_timeout = 9;
google.protobuf.Duration session_timeout = 10;
}
message HttpPreflightResult {
oneof decision {
HttpContinue continue_without_body = 1;
HttpInspect inspect = 2;
}
// Validated before the next stage sees the head.
repeated HeaderMutation header_mutations = 3;
MiddlewareDiagnostics diagnostics = 4;
}
// Successful terminal decision before any body delivery. The body bypasses
// this stage but remains eligible for later stages.
message HttpContinue {}
message HttpInspect {
oneof mode {
HttpBufferedMode buffered = 1;
HttpStreamMode stream = 2;
}
}
message HttpBufferedMode {
// Positive and no larger than the offered max_buffered_body_bytes. The cap
// applies separately to input and replacement.
uint64 max_body_bytes = 1;
}
// STREAM transfers responsibility for all output at accepted preflight.
message HttpStreamMode {}
message HttpBegin {}
// BUFFERED receives exactly one complete body, including an empty body.
message HttpBufferedBody {
bytes data = 1;
repeated HttpHeader visible_trailers = 2;
}
message HttpBufferedResult {
oneof body {
HttpUnchanged unchanged = 1;
// Present empty bytes delete the body.
bytes replacement = 2;
}
repeated HeaderMutation header_mutations = 3;
repeated HeaderMutation trailer_mutations = 4;
MiddlewareDiagnostics diagnostics = 5;
}
// Successful BUFFERED pass-through. This is not failure recovery.
message HttpUnchanged {}
message HttpInputChunk {
// Nonempty normalized bytes. Boundaries have no transport or application
// meaning.
bytes data = 1;
}
message HttpInputEnd {
// Sent exactly once after input, including for an empty body.
repeated HttpHeader visible_trailers = 1;
}
message HttpOutputStart {
// Sent exactly once before output bytes. Mutations require the selected mode
// to appear in late_header_modes.
repeated HeaderMutation header_mutations = 1;
// When present, OpenShell derives framing and validates actual output length.
optional uint64 output_body_bytes = 2;
}
message HttpOutputChunk {
// Nonempty normalized bytes. Input and output cardinality are independent.
bytes data = 1;
}
// STREAM terminal success. Finish is valid only after InputEnd was consumed.
message HttpFinish {
repeated HeaderMutation trailer_mutations = 1;
MiddlewareDiagnostics diagnostics = 2;
}
// Explicit terminal denial at preflight or during body processing.
message HttpReject {
MiddlewareDiagnostics diagnostics = 1;
}
message MiddlewareDiagnostics {
// Service text is operator-only and never copied to denied responses or
// security logs.
string reason = 1;
// Optional stable audit-safe code.
string reason_code = 2;
repeated Finding findings = 3;
map<string, string> metadata = 4;
}
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 {
reserved 3;
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 MiddlewareDiagnostics.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 = 4;
// Workspace the sandbox belongs to. For display and logging only; see the
// sandbox 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;
}
}