Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Build output
/target/
e2e/rust/target/
target/
debug/
release/

Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ Operators can configure a gateway-wide gRPC request rate limit. The limit is
applied only to gRPC API traffic after protocol multiplexing; health, metrics,
and local sandbox-service HTTP routes are not rate limited by this control.

Gateway interceptors run in one middleware layer on the `openshell.v1.OpenShell`
gRPC service after authentication and before tonic dispatches to individual
handlers. At startup the gateway calls each configured interceptor's `Describe`
RPC, validates declared bindings against the compiled OpenShell descriptor set,
and builds an immutable execution plan. Unary OpenShell requests that are not
streaming, supervisor-facing, read-only, or introspection methods are decoded
through the descriptor set into protobuf JSON, evaluated through configured
phases, and re-encoded before the handler sees the request. This keeps
interception centralized: adding an interceptable unary RPC does not require
method-specific gateway instrumentation.

Supported auth modes:

| Mode | Use |
Expand Down
11 changes: 11 additions & 0 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,7 @@ pub async fn sandbox_create(
}),
name: name.unwrap_or_default().to_string(),
labels: labels.clone(),
annotations: HashMap::new(),
};

let response = match client.create_sandbox(request).await {
Expand Down Expand Up @@ -3353,10 +3354,15 @@ pub async fn sandbox_list(
fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
let meta = sandbox.metadata.as_ref();
let labels = meta.map_or_else(|| serde_json::json!({}), |m| serde_json::json!(m.labels));
let annotations = meta.map_or_else(
|| serde_json::json!({}),
|m| serde_json::json!(m.annotations),
);
serde_json::json!({
"id": sandbox.object_id(),
"name": sandbox.object_name(),
"labels": labels,
"annotations": annotations,
"resource_version": meta.map_or(0, |m| m.resource_version),
"created_at": format_epoch_ms(meta.map_or(0, |m| m.created_at_ms)),
"phase": phase_name(sandbox.phase()),
Expand Down Expand Up @@ -3809,6 +3815,7 @@ async fn auto_create_provider(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.to_string(),
credentials: discovered.credentials.clone(),
Expand Down Expand Up @@ -3851,6 +3858,7 @@ async fn auto_create_provider(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.to_string(),
credentials: discovered.credentials.clone(),
Expand Down Expand Up @@ -4690,6 +4698,7 @@ pub async fn provider_create_with_options(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.clone(),
credentials: credential_map,
Expand Down Expand Up @@ -5621,6 +5630,7 @@ pub async fn provider_update(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: String::new(),
credentials: credential_map,
Expand Down Expand Up @@ -9519,6 +9529,7 @@ mod tests {
resource_version: 42,
created_at_ms: 1_234_567_890_000,
labels,
annotations: std::collections::HashMap::new(),
};

let provider = Provider {
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ impl TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.to_string(),
credentials: HashMap::new(),
Expand Down Expand Up @@ -349,6 +350,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: existing_metadata.created_at_ms,
labels: existing_metadata.labels,
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: existing.r#type,
credentials: merge(existing.credentials, provider.credentials),
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-cli/tests/provider_commands_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 1,
annotations: HashMap::new(),
}),
spec: None,
status: None,
Expand Down Expand Up @@ -602,6 +603,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: existing_metadata.created_at_ms,
labels: existing_metadata.labels,
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: existing.r#type,
credentials: merge(existing.credentials, provider.credentials),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
..Sandbox::default()
};
Expand All @@ -108,6 +109,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
..Sandbox::default()
};
Expand Down Expand Up @@ -368,6 +370,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
..Sandbox::default()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: 0,
labels: std::collections::HashMap::new(),
resource_version: 0,
annotations: std::collections::HashMap::new(),
}),
..Default::default()
}),
Expand Down
90 changes: 90 additions & 0 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ pub struct Config {
/// Gateway user authentication behavior.
pub auth: GatewayAuthConfig,

/// Disabled-by-default gateway interceptor service configs.
pub gateway_interceptors: Vec<GatewayInterceptorConfig>,

/// mTLS user authentication configuration. When enabled, a verified TLS
/// client certificate can authenticate CLI/SDK callers as a
/// `Principal::User`. This is for local single-user gateways only;
Expand Down Expand Up @@ -494,6 +497,82 @@ pub struct GatewayAuthConfig {
pub allow_unauthenticated_users: bool,
}

/// One configured gateway interceptor service.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct GatewayInterceptorConfig {
/// Operator-assigned instance name used in logs and config overrides.
pub name: String,
/// Interceptor gRPC endpoint. Supports `http://`, `https://`, and
/// `unix://` endpoints.
pub grpc_endpoint: String,
/// Deterministic service ordering. Lower values run first.
#[serde(default)]
pub order: i32,
/// Default failure policy for this configured service.
#[serde(default)]
pub failure_policy: Option<GatewayInterceptorFailurePolicy>,
/// RFC-style timeout string such as `500ms` or `2s`.
#[serde(default)]
pub timeout: Option<String>,
/// Maximum accepted encoded `Evaluate` response size.
#[serde(default)]
pub max_response_bytes: Option<usize>,
/// Maximum JSON patches accepted from one evaluation result.
#[serde(default)]
pub max_patches: Option<usize>,
/// Optional binding overrides. Overrides may disable bindings or narrow
/// phases/selectors declared by the interceptor service.
#[serde(default)]
pub bindings: Vec<GatewayInterceptorBindingOverride>,
}

/// Failure behavior when an interceptor evaluation cannot produce a valid
/// result.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GatewayInterceptorFailurePolicy {
FailClosed,
FailOpen,
Ignore,
}

/// Configured override for a manifest binding.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct GatewayInterceptorBindingOverride {
/// Binding id from the interceptor manifest.
#[serde(default)]
pub id: Option<String>,
/// Full selector form: `openshell.v1.OpenShell/CreateSandbox`.
#[serde(default)]
pub rpc: Option<String>,
/// Structured selector service, e.g. `openshell.v1.OpenShell`.
#[serde(default)]
pub service: Option<String>,
/// Structured selector method, e.g. `CreateSandbox`.
#[serde(default)]
pub method: Option<String>,
/// Narrowed phase set.
#[serde(default)]
pub phases: Option<Vec<GatewayInterceptorPhaseConfig>>,
/// Disable the selected binding.
#[serde(default)]
pub disabled: bool,
/// Binding-specific failure policy override.
#[serde(default)]
pub failure_policy: Option<GatewayInterceptorFailurePolicy>,
}

/// Config file phase names.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum GatewayInterceptorPhaseConfig {
ModifyOperation,
Validate,
PostCommit,
}

const fn default_jwks_ttl_secs() -> u64 {
3600
}
Expand Down Expand Up @@ -555,6 +634,7 @@ impl Config {
tls,
oidc: None,
auth: GatewayAuthConfig::default(),
gateway_interceptors: Vec::new(),
mtls_auth: MtlsAuthConfig::default(),
gateway_jwt: None,
database_url: String::new(),
Expand Down Expand Up @@ -641,6 +721,16 @@ impl Config {
self
}

/// Set configured gateway interceptors.
#[must_use]
pub fn with_gateway_interceptors<I>(mut self, interceptors: I) -> Self
where
I: IntoIterator<Item = GatewayInterceptorConfig>,
{
self.gateway_interceptors = interceptors.into_iter().collect();
self
}

/// Return the effective gRPC rate limit, if fully configured and enabled.
#[must_use]
pub fn grpc_rate_limit(&self) -> Option<(u64, Duration)> {
Expand Down
5 changes: 3 additions & 2 deletions crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ pub mod telemetry;
pub mod time;

pub use config::{
ComputeDriverKind, Config, GatewayAuthConfig, GatewayJwtConfig, MtlsAuthConfig, OidcConfig,
TlsConfig,
ComputeDriverKind, Config, GatewayAuthConfig, GatewayInterceptorBindingOverride,
GatewayInterceptorConfig, GatewayInterceptorFailurePolicy, GatewayInterceptorPhaseConfig,
GatewayJwtConfig, MtlsAuthConfig, OidcConfig, TlsConfig,
};
pub use error::{ComputeDriverError, Error, Result};
pub use metadata::{GetResourceVersion, ObjectId, ObjectLabels, ObjectName, SetResourceVersion};
Expand Down
17 changes: 17 additions & 0 deletions crates/openshell-core/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,24 @@ pub mod inference {
}
}

#[allow(
clippy::all,
clippy::pedantic,
clippy::nursery,
unused_qualifications,
rust_2018_idioms
)]
pub mod gateway_interceptor {
pub mod v1 {
include!(concat!(
env!("OUT_DIR"),
"/openshell.gateway_interceptor.v1.rs"
));
}
}

pub use datamodel::v1::*;
pub use gateway_interceptor::v1::*;
pub use inference::v1::*;
pub use openshell::*;
pub use sandbox::v1::*;
Expand Down
30 changes: 30 additions & 0 deletions crates/openshell-gateway-interceptors/Cargo.toml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is still a draft, but what about creating this at crates/opeshell-hooks/gateway-interceptors (the supervisor middleware can be at crates/openshell-hooks/supervisor-middleware).

Another option: crates/openshell-extensions/...?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts @pimlock?

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "openshell-gateway-interceptors"
description = "Gateway interceptor framework for OpenShell"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
openshell-core = { path = "../openshell-core", default-features = false }

base64 = { workspace = true }
hyper-util = { workspace = true, features = ["client", "http1", "http2", "tokio"] }
json-patch = "1.4"
metrics = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tonic = { workspace = true, features = ["channel", "tls-native-roots"] }
tower = { workspace = true }
tracing = { workspace = true }

[lints]
workspace = true
Loading
Loading