Skip to content
Draft
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 .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
21 changes: 21 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
104 changes: 101 additions & 3 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,81 @@ 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,
}

/// 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 +633,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 +720,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 Expand Up @@ -765,9 +854,9 @@ mod tests {
#[cfg(unix)]
use super::is_reachable_unix_socket;
use super::{
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayJwtConfig, detect_driver,
docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
podman_socket_responds,
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayInterceptorFailurePolicy,
GatewayJwtConfig, detect_driver, docker_host_unix_socket_path, is_unix_socket,
podman_socket_candidates_from_env, podman_socket_responds,
};
#[cfg(unix)]
use std::io::{Read as _, Write as _};
Expand Down Expand Up @@ -833,6 +922,15 @@ mod tests {
assert_eq!(cfg.ttl_secs, 0);
}

#[test]
fn gateway_interceptor_failure_policy_rejects_ignore() {
let err =
serde_json::from_value::<GatewayInterceptorFailurePolicy>(serde_json::json!("ignore"))
.unwrap_err();

assert!(err.to_string().contains("unknown variant `ignore`"));
}

#[test]
fn grpc_rate_limit_requires_positive_pair() {
assert!(Config::new(None).grpc_rate_limit().is_none());
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
Loading
Loading