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
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.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ serde_yml = "0.0.12"
toml = "0.8"
apollo-parser = "0.8.5"
tower-mcp-types = "0.12.0"
regex = "1"

# HTTP client
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }
Expand Down
8 changes: 8 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ config path. A gateway-global policy can override sandbox-scoped policy. The
sandbox supervisor polls for config revisions and hot-reloads dynamic policy
when the policy engine accepts the update.

External supervisor middleware registration is operator-owned gateway
configuration. At startup the gateway connects to each service, validates its
described bindings and operator body limit, and rejects duplicate binding IDs.
Before persisting a policy, the gateway asks each selected implementation to
validate its config. The effective sandbox config contains only the registered
services required by that policy; supervisors invoke those services directly on
the request path.

Provider credential expiry is enforced during gateway-to-sandbox credential
resolution and again by the sandbox placeholder resolver. This keeps expired
credentials from resolving even when a running sandbox still has retained
Expand Down
10 changes: 10 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ matchers; generic JSON-RPC rules match only the method.
JSON-RPC responses and server-to-client MCP messages on response or SSE streams
are relayed but are not currently parsed for policy enforcement.

For admitted HTTP requests, the proxy can run an ordered supervisor middleware
chain before credential injection. Host selectors choose the chain independently
of the network rule that admitted the request. Built-ins run in-process;
operator-registered services are called directly from the supervisor
over the common middleware gRPC contract. The gateway validates external
service capabilities and policy-owned config before delivery. Supervisors keep
the last-known-good service registry when a live config reload fails.

`https://inference.local` is special. It bypasses OPA network policy and is
handled by the inference interception path:

Expand Down Expand Up @@ -169,6 +177,8 @@ quickly.
- If gateway config polling fails, the sandbox keeps its last-known-good policy.
- If a live policy update is invalid, the supervisor rejects it and keeps the
current policy.
- If an operator-run middleware call fails, the selected config's `on_error`
behavior decides whether to deny the request or continue without that stage.
- Existing raw byte streams are connection scoped. Dynamic policy changes apply
to new connections or the next parsed HTTP request where the proxy can safely
re-evaluate.
Expand Down
41 changes: 30 additions & 11 deletions crates/openshell-core/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};

use crate::proto::{
DenialSummary, GetDraftPolicyRequest, GetInferenceBundleRequest, GetInferenceBundleResponse,
GetSandboxConfigRequest, GetSandboxProviderEnvironmentRequest, IssueSandboxTokenRequest,
NetworkActivitySummary, PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest,
ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest,
SubmitPolicyAnalysisResponse, UpdateConfigRequest, inference_client::InferenceClient,
open_shell_client::OpenShellClient,
GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest,
IssueSandboxTokenRequest, NetworkActivitySummary, PolicyChunk, PolicySource, PolicyStatus,
RefreshSandboxTokenRequest, ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy,
SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, UpdateConfigRequest,
inference_client::InferenceClient, open_shell_client::OpenShellClient,
};
use crate::sandbox_env;
use miette::{IntoDiagnostic, Result, WrapErr};
Expand Down Expand Up @@ -573,19 +573,36 @@ pub async fn fetch_policy(endpoint: &str, sandbox_id: &str) -> Result<Option<Pro
fetch_policy_with_client(&mut client, sandbox_id).await
}

/// Fetch sandbox policy using an existing client connection.
async fn fetch_policy_with_client(
/// Fetch the complete effective sandbox configuration, including external
/// middleware registrations required by the policy.
pub async fn fetch_sandbox_config(
endpoint: &str,
sandbox_id: &str,
) -> Result<GetSandboxConfigResponse> {
debug!(endpoint = %endpoint, sandbox_id = %sandbox_id, "Connecting to OpenShell server");
let mut client = connect(endpoint).await?;
fetch_sandbox_config_with_client(&mut client, sandbox_id).await
}

async fn fetch_sandbox_config_with_client(
client: &mut OpenShellClient<AuthedChannel>,
sandbox_id: &str,
) -> Result<Option<ProtoSandboxPolicy>> {
let response = client
) -> Result<GetSandboxConfigResponse> {
client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.to_string(),
})
.await
.into_diagnostic()?;
.map(tonic::Response::into_inner)
.into_diagnostic()
}

let inner = response.into_inner();
/// Fetch sandbox policy using an existing client connection.
async fn fetch_policy_with_client(
client: &mut OpenShellClient<AuthedChannel>,
sandbox_id: &str,
) -> Result<Option<ProtoSandboxPolicy>> {
let inner = fetch_sandbox_config_with_client(client, sandbox_id).await?;

// version 0 with no policy means the sandbox was created without one.
if inner.version == 0 && inner.policy.is_none() {
Expand Down Expand Up @@ -711,6 +728,7 @@ pub struct SettingsPollResult {
/// When `policy_source` is `Global`, the version of the global policy revision.
pub global_policy_version: u32,
pub provider_env_revision: u64,
pub supervisor_middleware_services: Vec<crate::proto::SupervisorMiddlewareService>,
}

pub struct ProviderEnvironmentResult {
Expand Down Expand Up @@ -755,6 +773,7 @@ impl CachedOpenShellClient {
settings: inner.settings,
global_policy_version: inner.global_policy_version,
provider_env_revision: inner.provider_env_revision,
supervisor_middleware_services: inner.supervisor_middleware_services,
})
}

Expand Down
14 changes: 14 additions & 0 deletions crates/openshell-core/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,22 @@ pub mod inference {
}
}

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

pub use datamodel::v1::*;
pub use inference::v1::*;
pub use middleware::v1::*;
pub use openshell::*;
pub use sandbox::v1::*;
pub use test::ObjectForTest;
3 changes: 3 additions & 0 deletions crates/openshell-policy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ license.workspace = true
repository.workspace = true

[dependencies]
glob = { workspace = true }
openshell-core = { path = "../openshell-core", default-features = false }
openshell-supervisor-middleware = { path = "../openshell-supervisor-middleware" }
prost-types = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yml = { workspace = true }
Expand Down
Loading
Loading